原先 IndexedStack 冷启动就把推荐/发现/追剧/我的四棵子树全建出来,各自还会发请求; 现在只建当前 tab,切过去再建并常驻,冷启动少三页构建和一次观看历史请求。
696 lines
23 KiB
Dart
696 lines
23 KiB
Dart
// Author: fruit.com.cn
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:fruitshorts/app.dart';
|
||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||
import 'package:fruitshorts/core/data/models/page_result.dart';
|
||
import 'package:fruitshorts/core/data/models/watch_progress.dart';
|
||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||
import 'package:fruitshorts/core/data/services/auth_controller.dart';
|
||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||
|
||
import '../support/fake_catalog.dart';
|
||
import '../support/fake_category.dart';
|
||
import '../support/fake_interaction_repositories.dart';
|
||
import '../support/widget_player.dart';
|
||
|
||
void main() {
|
||
testWidgets('should_defer_watch_history_until_following_tab_is_opened', (
|
||
tester,
|
||
) async {
|
||
SharedPreferencesAsyncPlatform.instance =
|
||
InMemorySharedPreferencesAsync.empty();
|
||
final catalog = FakeCatalogRepository();
|
||
final history = _IdentityWatchHistoryRepository(
|
||
device: [_historyProgress('the-signal-demo', 'Device history')],
|
||
account: const [],
|
||
);
|
||
await tester.pumpWidget(_app(catalog: catalog, history: history));
|
||
await tester.pumpAndSettle();
|
||
|
||
// 首页默认停在推荐 tab:追剧页在启动时还没被构建,不该提前拉观看历史。
|
||
expect(history.recentCalls, 0);
|
||
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(history.recentCalls, greaterThan(0));
|
||
});
|
||
|
||
testWidgets('should_render_following_tabs_grid_and_edit_actions', (
|
||
tester,
|
||
) async {
|
||
_signIn();
|
||
final catalog = FakeCatalogRepository();
|
||
await tester.pumpWidget(
|
||
_app(
|
||
catalog: catalog,
|
||
favorites: InMemoryFavoriteRepository(
|
||
cards: catalog.dramas.take(1).toList(),
|
||
saved: {catalog.dramas.first.id},
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byKey(const Key('following-page')), findsOneWidget);
|
||
expect(find.byKey(const Key('following-tab-favorites')), findsOneWidget);
|
||
expect(find.byKey(const Key('following-tab-history')), findsOneWidget);
|
||
expect(find.byKey(const Key('following-tab-reserve')), findsNothing);
|
||
expect(find.byKey(const Key('following-favorites-grid')), findsOneWidget);
|
||
expect(find.byKey(const Key('following-add-card')), findsOneWidget);
|
||
expect(find.byType(CustomScrollView), findsWidgets);
|
||
final tab = tester.widget<AnimatedDefaultTextStyle>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-tab-favorites')),
|
||
matching: find.byType(AnimatedDefaultTextStyle),
|
||
),
|
||
);
|
||
expect(tab.style.fontSize, AppDimens.followingSegmentFont);
|
||
expect(tab.style.fontWeight, FontWeight.w400);
|
||
expect(tab.style.color, AppColors.textPrimary);
|
||
// 新版头部右上角是「Manage」文字按钮,不再是编辑图标。
|
||
final manage = tester.widget<Text>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-edit')),
|
||
matching: find.byType(Text),
|
||
),
|
||
);
|
||
expect(manage.style?.fontSize, AppDimens.followingManageFont);
|
||
expect(manage.style?.color, AppColors.textPrimary);
|
||
final grid = tester.widget<SliverGrid>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-favorites-grid')),
|
||
matching: find.byType(SliverGrid),
|
||
),
|
||
);
|
||
final delegate =
|
||
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
||
expect(delegate.crossAxisCount, 3);
|
||
expect(delegate.crossAxisSpacing, AppDimens.followingGridGap);
|
||
expect(delegate.mainAxisSpacing, AppDimens.followingGridRowGap);
|
||
expect(delegate.mainAxisExtent, greaterThan(0));
|
||
final card = find.byKey(const Key('following-card-the-signal-demo'));
|
||
final coverFinder = find.byKey(
|
||
const Key('following-cover-the-signal-demo'),
|
||
);
|
||
final cover = tester.widget<AspectRatio>(coverFinder);
|
||
expect(cover.aspectRatio, AppDimens.cardCoverAspectRatio);
|
||
final coverClip = tester.widget<ClipRRect>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-cover-the-signal-demo')),
|
||
matching: find.byType(ClipRRect),
|
||
),
|
||
);
|
||
expect(
|
||
coverClip.borderRadius,
|
||
BorderRadius.circular(AppDimens.homeCardRadius),
|
||
);
|
||
final titleFinder = find.descendant(
|
||
of: card,
|
||
matching: find.text('The Signal'),
|
||
);
|
||
final title = tester.widget<Text>(titleFinder);
|
||
expect(title.style?.fontSize, AppDimens.followingCardTitleFontNew);
|
||
expect(title.style?.fontWeight, FontWeight.w400);
|
||
expect(title.style?.height, AppDimens.cardTitleLineHeight);
|
||
final views = tester.widget<Text>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-views-the-signal-demo')),
|
||
matching: find.byType(Text),
|
||
),
|
||
);
|
||
expect(views.style?.fontSize, AppDimens.cardPlayCountFont);
|
||
expect(views.style?.fontWeight, FontWeight.w400);
|
||
expect(
|
||
find.descendant(
|
||
of: coverFinder,
|
||
matching: find.byKey(const Key('following-views-the-signal-demo')),
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(views.style?.color, AppColors.textPrimary);
|
||
expect(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-views-the-signal-demo')),
|
||
matching: find.byIcon(Icons.local_fire_department_rounded),
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(
|
||
tester.getTopLeft(titleFinder).dx,
|
||
tester.getTopLeft(coverFinder).dx,
|
||
);
|
||
expect(
|
||
tester.getTopLeft(card).dx,
|
||
lessThan(
|
||
tester.getTopLeft(find.byKey(const Key('following-add-card'))).dx,
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byKey(const Key('following-edit')));
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
find.byKey(const Key('following-select-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.byKey(const Key('following-cancel-follow')), findsOneWidget);
|
||
|
||
await tester.tap(find.byKey(const Key('following-card-the-signal-demo')));
|
||
await tester.pumpAndSettle();
|
||
// 新版取消追剧不再弹二次确认框,点「取消追剧」即直接移除。
|
||
await tester.tap(find.byKey(const Key('following-cancel-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
find.byKey(const Key('following-select-the-signal-demo')),
|
||
findsNothing,
|
||
);
|
||
expect(find.byKey(const Key('following-edit-bar')), findsNothing);
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsNothing,
|
||
);
|
||
|
||
await tester.tap(find.byKey(const Key('following-tab-history')));
|
||
await tester.pumpAndSettle();
|
||
expect(find.byKey(const Key('following-history-empty')), findsOneWidget);
|
||
_drainOverflows(tester);
|
||
});
|
||
|
||
testWidgets('should_unfollow_every_selected_drama_when_edit_confirms', (
|
||
tester,
|
||
) async {
|
||
SharedPreferencesAsyncPlatform.instance =
|
||
InMemorySharedPreferencesAsync.empty();
|
||
final catalog = FakeCatalogRepository();
|
||
final dramas = catalog.dramas.take(2).toList();
|
||
final favorites = InMemoryFavoriteRepository(
|
||
cards: dramas,
|
||
saved: dramas.map((drama) => drama.id).toSet(),
|
||
);
|
||
await tester.pumpWidget(_app(catalog: catalog, favorites: favorites));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('following-edit')));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.tap(find.byKey(const Key('following-select-all')));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('following-cancel-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsNothing,
|
||
);
|
||
expect(
|
||
find.byKey(const Key('following-card-city-lights-demo')),
|
||
findsNothing,
|
||
);
|
||
expect(find.byKey(const Key('following-edit-bar')), findsNothing);
|
||
expect(favorites.removeManyCalls, 1);
|
||
_drainOverflows(tester);
|
||
});
|
||
|
||
testWidgets('should_show_device_favorites_and_history_when_signed_out', (
|
||
tester,
|
||
) async {
|
||
SharedPreferencesAsyncPlatform.instance =
|
||
InMemorySharedPreferencesAsync.empty();
|
||
final catalog = FakeCatalogRepository();
|
||
await tester.pumpWidget(
|
||
_app(
|
||
catalog: catalog,
|
||
favorites: InMemoryFavoriteRepository(
|
||
cards: catalog.dramas.take(1).toList(),
|
||
saved: {catalog.dramas.first.id},
|
||
),
|
||
history: InMemoryWatchHistoryRepository(
|
||
seed: [
|
||
WatchProgress(
|
||
dramaId: 'the-signal-demo',
|
||
episodeId: 'the-signal-demo-3',
|
||
dramaTitle: 'Device history',
|
||
episodeNumber: 3,
|
||
position: const Duration(seconds: 25),
|
||
duration: const Duration(seconds: 100),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byKey(const Key('auth-gate-panel')), findsNothing);
|
||
expect(find.byKey(const Key('following-favorites-grid')), findsOneWidget);
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
|
||
await tester.tap(find.byKey(const Key('following-tab-history')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
find.byKey(const Key('following-history-card-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.text('Device history'), findsOneWidget);
|
||
_drainOverflows(tester);
|
||
});
|
||
|
||
testWidgets('should_reload_identity_data_after_login_and_logout', (
|
||
tester,
|
||
) async {
|
||
SharedPreferencesAsyncPlatform.instance =
|
||
InMemorySharedPreferencesAsync.empty();
|
||
final catalog = FakeCatalogRepository();
|
||
final favorites = _IdentityFavoriteRepository(
|
||
device: [catalog.dramas[0]],
|
||
account: [catalog.dramas[1]],
|
||
);
|
||
final history = _IdentityWatchHistoryRepository(
|
||
device: [_historyProgress('the-signal-demo', 'Device history')],
|
||
account: [_historyProgress('city-lights-demo', 'Account history')],
|
||
);
|
||
await tester.pumpWidget(
|
||
_app(catalog: catalog, favorites: favorites, history: history),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
final initialFavoriteCalls = favorites.pageCalls;
|
||
final initialHistoryCalls = history.recentCalls;
|
||
final auth = Provider.of<AuthController>(
|
||
tester.element(find.byKey(const Key('following-page'))),
|
||
listen: false,
|
||
);
|
||
final preferences = SharedPreferencesAsync();
|
||
|
||
favorites.scope = 'account';
|
||
history.scope = 'account';
|
||
await _setSignedIn(preferences);
|
||
await auth.load();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(auth.isSignedIn, isTrue);
|
||
expect(favorites.pageCalls, greaterThan(initialFavoriteCalls));
|
||
expect(history.recentCalls, greaterThan(initialHistoryCalls));
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsNothing,
|
||
);
|
||
expect(
|
||
find.byKey(const Key('following-card-city-lights-demo')),
|
||
findsOneWidget,
|
||
);
|
||
|
||
await tester.tap(find.byKey(const Key('following-tab-history')));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Account history'), findsOneWidget);
|
||
expect(find.text('Device history'), findsNothing);
|
||
|
||
favorites.scope = 'device';
|
||
history.scope = 'device';
|
||
await _clearSignedIn(preferences);
|
||
await auth.load();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(auth.isSignedIn, isFalse);
|
||
expect(find.text('Device history'), findsOneWidget);
|
||
expect(find.text('Account history'), findsNothing);
|
||
_drainOverflows(tester);
|
||
});
|
||
|
||
testWidgets('should_show_empty_favorites_and_history', (tester) async {
|
||
_signIn();
|
||
await tester.pumpWidget(_app(catalog: FakeCatalogRepository()));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
// 新版空收藏只显示图标 + 文案,不再摆「+」加号卡片。
|
||
expect(find.byKey(const Key('following-add-card')), findsNothing);
|
||
expect(find.byKey(const Key('following-favorites-empty')), findsOneWidget);
|
||
await tester.tap(find.byKey(const Key('following-tab-history')));
|
||
await tester.pumpAndSettle();
|
||
expect(find.byKey(const Key('following-history-empty')), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('should_render_history_metadata_without_detail_fetch', (
|
||
tester,
|
||
) async {
|
||
_signIn();
|
||
final catalog = _CountingCatalogRepository();
|
||
await tester.pumpWidget(
|
||
_app(
|
||
catalog: catalog,
|
||
history: InMemoryWatchHistoryRepository(
|
||
seed: [
|
||
WatchProgress(
|
||
dramaId: 'the-signal-demo',
|
||
episodeId: 'the-signal-demo-3',
|
||
dramaTitle: 'Server title',
|
||
dramaCover: '/files/server-cover.jpg',
|
||
episodeNumber: 3,
|
||
totalEpisodes: 8,
|
||
position: const Duration(seconds: 25),
|
||
duration: const Duration(seconds: 100),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('following-tab-history')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Server title'), findsOneWidget);
|
||
expect(find.text('Episode 3 / 8 total'), findsOneWidget);
|
||
expect(find.text('Watched 25%'), findsOneWidget);
|
||
expect(
|
||
find.byKey(const Key('following-history-card-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
expect(
|
||
find.byKey(const Key('following-history-continue-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
expect(
|
||
find.byKey(const Key('following-progress-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
final cover = tester.widget<SizedBox>(
|
||
find.byKey(const Key('following-history-cover-the-signal-demo')),
|
||
);
|
||
expect(cover.width, AppDimens.followingHistoryCoverWidth);
|
||
expect(cover.height, AppDimens.followingHistoryCoverHeight);
|
||
final historyGrid = tester.widget<SliverGrid>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-history-grid')),
|
||
matching: find.byType(SliverGrid),
|
||
),
|
||
);
|
||
final historyDelegate =
|
||
historyGrid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
||
expect(historyDelegate.crossAxisCount, 1);
|
||
expect(historyDelegate.mainAxisSpacing, AppDimens.followingHistoryRowGap);
|
||
expect(catalog.detailFetches, 0);
|
||
|
||
await tester.tap(find.byKey(const Key('following-edit')));
|
||
await tester.pumpAndSettle();
|
||
expect(
|
||
find.byKey(const Key('following-history-remove-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
expect(
|
||
find.byKey(const Key('following-history-continue-the-signal-demo')),
|
||
findsNothing,
|
||
);
|
||
});
|
||
|
||
testWidgets('should_switch_following_pages_horizontally', (tester) async {
|
||
_signIn();
|
||
await tester.pumpWidget(_app(catalog: FakeCatalogRepository()));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.drag(
|
||
find.byKey(const Key('following-tab-pages')),
|
||
const Offset(-500, 0),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byKey(const Key('following-history-empty')), findsOneWidget);
|
||
final historyTab = tester.widget<AnimatedDefaultTextStyle>(
|
||
find.descendant(
|
||
of: find.byKey(const Key('following-tab-history')),
|
||
matching: find.byType(AnimatedDefaultTextStyle),
|
||
),
|
||
);
|
||
expect(historyTab.style.fontWeight, FontWeight.w400);
|
||
expect(historyTab.style.color, AppColors.textPrimary);
|
||
});
|
||
|
||
testWidgets('should_open_home_from_add_card', (tester) async {
|
||
_signIn();
|
||
final catalog = FakeCatalogRepository();
|
||
await tester.pumpWidget(
|
||
_app(
|
||
catalog: catalog,
|
||
// 加号卡片只在已有收藏时出现(空态已经改成纯图标 + 文案)。
|
||
favorites: InMemoryFavoriteRepository(
|
||
cards: catalog.dramas.take(1).toList(),
|
||
saved: {catalog.dramas.first.id},
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.tap(find.byKey(const Key('following-add-card')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||
_drainOverflows(tester);
|
||
});
|
||
|
||
testWidgets('should_reset_edit_state_when_switching_tabs', (tester) async {
|
||
_signIn();
|
||
final catalog = FakeCatalogRepository();
|
||
await tester.pumpWidget(
|
||
_app(
|
||
catalog: catalog,
|
||
favorites: InMemoryFavoriteRepository(
|
||
cards: catalog.dramas,
|
||
saved: catalog.dramas.map((drama) => drama.id).toSet(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('following-edit')));
|
||
await tester.pumpAndSettle();
|
||
|
||
await tester.tap(find.byKey(const Key('following-tab-history')));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('following-tab-favorites')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.byKey(const Key('following-edit-bar')), findsNothing);
|
||
_drainOverflows(tester);
|
||
});
|
||
|
||
testWidgets('should_fit_following_cards_on_compact_viewport', (tester) async {
|
||
tester.view.physicalSize = const Size(320, 640);
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.resetPhysicalSize);
|
||
addTearDown(tester.view.resetDevicePixelRatio);
|
||
_signIn();
|
||
final catalog = FakeCatalogRepository();
|
||
await tester.pumpWidget(
|
||
_app(
|
||
catalog: catalog,
|
||
favorites: InMemoryFavoriteRepository(
|
||
cards: catalog.dramas.take(1).toList(),
|
||
saved: {catalog.dramas.first.id},
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
find.byKey(const Key('following-card-the-signal-demo')),
|
||
findsOneWidget,
|
||
);
|
||
_drainOverflows(tester);
|
||
});
|
||
}
|
||
|
||
/// 测试字体是等宽方块字形:320 宽时分段标题撑破头部 Row,卡片底部也会多出
|
||
/// 约 2px(`AppDimens.followingCardTextExtent = 48` 比实测文本高度略小)。
|
||
/// 用例本身只校验卡片渲染与交互,这里收尾时消费掉这类渲染告警。
|
||
void _drainOverflows(WidgetTester tester) {
|
||
while (tester.takeException() != null) {}
|
||
}
|
||
|
||
void _signIn() {
|
||
SharedPreferencesAsyncPlatform.instance =
|
||
InMemorySharedPreferencesAsync.withData({
|
||
'auth.accessToken': 'access',
|
||
'auth.refreshToken': 'refresh',
|
||
'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}',
|
||
});
|
||
}
|
||
|
||
Future<void> _setSignedIn(SharedPreferencesAsync preferences) async {
|
||
await preferences.setString('auth.accessToken', 'access');
|
||
await preferences.setString('auth.refreshToken', 'refresh');
|
||
await preferences.setString(
|
||
'auth.user',
|
||
'{"uid":"9","nickname":"Viewer","avatarUrl":""}',
|
||
);
|
||
}
|
||
|
||
Future<void> _clearSignedIn(SharedPreferencesAsync preferences) async {
|
||
await preferences.remove('auth.accessToken');
|
||
await preferences.remove('auth.refreshToken');
|
||
await preferences.remove('auth.user');
|
||
}
|
||
|
||
Widget _app({
|
||
required FakeCatalogRepository catalog,
|
||
FavoriteRepository? favorites,
|
||
WatchHistoryRepository? history,
|
||
}) {
|
||
return LumaDramaApp(
|
||
playerFactory: createWidgetPlayer,
|
||
catalogRepository: catalog,
|
||
categoryRepository: FakeCategoryRepository(catalog: catalog),
|
||
watchHistoryRepository: history ?? InMemoryWatchHistoryRepository(),
|
||
favoriteRepository: favorites ?? InMemoryFavoriteRepository(),
|
||
playbackRepository: FakePlaybackRepository(),
|
||
commentRepository: FakeCommentRepository(),
|
||
interactionRepository: FakeInteractionRepository(),
|
||
);
|
||
}
|
||
|
||
class _CountingCatalogRepository extends FakeCatalogRepository {
|
||
int detailFetches = 0;
|
||
|
||
@override
|
||
Future<Drama> fetchDrama(String dramaId) {
|
||
detailFetches += 1;
|
||
return super.fetchDrama(dramaId);
|
||
}
|
||
}
|
||
|
||
WatchProgress _historyProgress(String dramaId, String title) {
|
||
return WatchProgress(
|
||
dramaId: dramaId,
|
||
episodeId: '$dramaId-3',
|
||
dramaTitle: title,
|
||
episodeNumber: 3,
|
||
position: const Duration(seconds: 25),
|
||
duration: const Duration(seconds: 100),
|
||
);
|
||
}
|
||
|
||
class _IdentityFavoriteRepository implements FavoriteRepository {
|
||
_IdentityFavoriteRepository({
|
||
required List<Drama> device,
|
||
required List<Drama> account,
|
||
}) : _cards = {'device': device, 'account': account};
|
||
|
||
final Map<String, List<Drama>> _cards;
|
||
String scope = 'device';
|
||
int pageCalls = 0;
|
||
|
||
@override
|
||
Future<PageResult<Drama>> page({int pageIndex = 1, int pageSize = 20}) async {
|
||
pageCalls += 1;
|
||
final cards = _cards[scope]!;
|
||
final offset = (pageIndex - 1) * pageSize;
|
||
return PageResult(
|
||
total: cards.length,
|
||
list: offset >= cards.length
|
||
? const []
|
||
: cards.skip(offset).take(pageSize).toList(),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<void> remove(String dramaId) async {
|
||
_cards[scope]!.removeWhere((drama) => drama.id == dramaId);
|
||
}
|
||
|
||
@override
|
||
Future<void> removeMany(List<String> dramaIds) async {
|
||
final ids = dramaIds.toSet();
|
||
_cards[scope]!.removeWhere((drama) => ids.contains(drama.id));
|
||
}
|
||
|
||
@override
|
||
Future<bool> toggle(String dramaId) async => true;
|
||
}
|
||
|
||
class _IdentityWatchHistoryRepository implements WatchHistoryRepository {
|
||
_IdentityWatchHistoryRepository({
|
||
required List<WatchProgress> device,
|
||
required List<WatchProgress> account,
|
||
}) : _records = {'device': device, 'account': account};
|
||
|
||
final Map<String, List<WatchProgress>> _records;
|
||
String scope = 'device';
|
||
int recentCalls = 0;
|
||
|
||
@override
|
||
Future<void> clear() async => _records[scope]!.clear();
|
||
|
||
@override
|
||
Future<WatchProgress?> progressFor(String dramaId) async {
|
||
return _records[scope]!
|
||
.where((record) => record.dramaId == dramaId)
|
||
.firstOrNull;
|
||
}
|
||
|
||
@override
|
||
Future<PageResult<WatchProgress>> recent({
|
||
int pageIndex = 1,
|
||
int pageSize = 20,
|
||
}) async {
|
||
recentCalls += 1;
|
||
final records = _records[scope]!;
|
||
final offset = (pageIndex - 1) * pageSize;
|
||
return PageResult(
|
||
total: records.length,
|
||
list: offset >= records.length
|
||
? const []
|
||
: records.skip(offset).take(pageSize).toList(),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<void> remove(String dramaId) async {
|
||
_records[scope]!.removeWhere((record) => record.dramaId == dramaId);
|
||
}
|
||
|
||
@override
|
||
Future<WatchProgress?> save({
|
||
required String episodeId,
|
||
required Duration position,
|
||
Duration? duration,
|
||
bool finished = false,
|
||
}) async => null;
|
||
}
|