Files
ReelShort/test/following/following_page_test.dart
T

683 lines
22 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_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.followingTabFont);
expect(tab.style.fontWeight, FontWeight.w600);
expect(tab.style.color, AppColors.textPrimary);
final editIcon = tester.widget<Icon>(find.byIcon(Icons.edit_outlined));
expect(editIcon.size, AppDimens.followingEditIconSize);
expect(editIcon.color, AppColors.textTabInactive);
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.cardTitleFont);
expect(title.style?.fontWeight, FontWeight.w500);
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.text('Unfollow the selected dramas?'), findsOneWidget);
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
expect(dialog.contentPadding, const EdgeInsets.all(20));
expect(dialog.actionsPadding, const EdgeInsets.fromLTRB(20, 0, 20, 20));
expect(
tester
.getSize(
find.descendant(
of: find.byType(AlertDialog),
matching: find.byType(TextButton),
),
)
.height,
32,
);
expect(
tester
.getSize(
find.descendant(
of: find.byType(AlertDialog),
matching: find.byType(FilledButton),
),
)
.height,
32,
);
await tester.tap(
find.descendant(
of: find.byType(AlertDialog),
matching: find.byType(FilledButton),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-select-the-signal-demo')),
findsNothing,
);
expect(find.byIcon(Icons.edit_outlined), findsOneWidget);
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);
});
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();
await tester.tap(
find.descendant(
of: find.byType(AlertDialog),
matching: find.byType(FilledButton),
),
);
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.byIcon(Icons.edit_outlined), findsOneWidget);
expect(favorites.removeManyCalls, 1);
});
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);
});
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);
});
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')), findsOneWidget);
expect(find.byKey(const Key('following-favorites-empty')), findsNothing);
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.w600);
expect(historyTab.style.color, AppColors.textPrimary);
});
testWidgets('should_open_home_from_add_card', (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.tap(find.byKey(const Key('following-add-card')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('home-grid')), findsOneWidget);
});
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.byIcon(Icons.edit_outlined), findsOneWidget);
});
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,
);
expect(tester.takeException(), isNull);
});
}
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;
}