// 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/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/auth_api.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/data/services/auth_session_store.dart'; import 'package:fruitshorts/core/data/services/favorite_controller.dart'; import 'package:fruitshorts/core/platform/haptics.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/widgets/app_pull_to_refresh.dart'; import 'package:fruitshorts/core/widgets/empty_state.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart'; import 'package:fruitshorts/pages/favorites/favorites_page.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:provider/provider.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_http_adapter.dart'; import '../support/fake_catalog.dart'; import '../support/fake_category.dart'; import '../support/fake_haptics.dart'; import '../support/fake_interaction_repositories.dart'; import '../support/fake_notice_repository.dart'; import '../support/widget_player.dart'; Drama _card(String id, {String synopsis = 'synopsis'}) => Drama( id: id, title: 'Drama $id', synopsis: synopsis, posterUrl: '/files/$id.png', totalEpisodes: 12, ); void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets('should_render_favorite_cards_and_visible_remove_action', ( tester, ) async { final auth = await _auth(signedIn: true); final favorites = FavoriteController( InMemoryFavoriteRepository( saved: {'signal', 'city'}, cards: [_card('signal'), _card('city')], ), ); await _pumpPage(tester, auth: auth, favorites: favorites); expect(find.byKey(const Key('favorites-page')), findsOneWidget); expect(find.byKey(const Key('favorites-back')), findsOneWidget); expect( tester.widget(find.byKey(const Key('favorites-count'))).data, '2 favorites', ); expect(find.text('Long press a card to remove'), findsNothing); expect(find.text('Drama signal'), findsOneWidget); expect(find.text('Drama city'), findsOneWidget); final title = tester.widget( find.byKey(const Key('favorite-title-signal')), ); expect(title.style?.color, AppColors.onSurface); expect(title.style?.fontSize, 15); final synopsis = tester.widget( find.byKey(const Key('favorite-synopsis-signal')), ); expect(synopsis.data, 'synopsis'); expect(synopsis.maxLines, 2); expect(synopsis.overflow, TextOverflow.ellipsis); expect(synopsis.style?.color, AppColors.muted); expect(synopsis.style?.fontSize, 12); expect( tester.getSize(find.byKey(const Key('favorite-cover-signal'))), const Size(80, 106), ); expect(find.byKey(const Key('favorite-remove-signal')), findsOneWidget); expect( tester.getSize(find.byKey(const Key('favorite-remove-signal'))), const Size(28, 28), ); expect(find.byIcon(Icons.star_rounded), findsNWidgets(2)); expect(find.byIcon(Icons.favorite_rounded), findsNothing); expect(find.byType(AppPullToRefresh), findsOneWidget); expect( tester.widget(find.byType(ListView)).physics, AppPullToRefresh.physics, ); await tester.drag(find.byType(ListView), const Offset(0, 240)); await tester.pump(); expect(find.byKey(const Key('pull-refresh-indicator')), findsOneWidget); expect(find.text('Refreshing…'), findsNothing); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('favorite-remove-signal'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('favorite-card-signal')), findsNothing); }); testWidgets('should_hide_blank_synopsis_without_changing_card_height', ( tester, ) async { final auth = await _auth(signedIn: true); final longSynopsis = List.filled(12, '很长的剧情简介').join(); final favorites = FavoriteController( InMemoryFavoriteRepository( saved: {'with-synopsis', 'without-synopsis'}, cards: [ _card('with-synopsis', synopsis: longSynopsis), _card('without-synopsis', synopsis: ' '), ], ), ); await _pumpPage(tester, auth: auth, favorites: favorites); final withSynopsis = tester.widget( find.byKey(const Key('favorite-synopsis-with-synopsis')), ); expect(withSynopsis.maxLines, 2); expect(withSynopsis.overflow, TextOverflow.ellipsis); expect( find.byKey(const Key('favorite-synopsis-without-synopsis')), findsNothing, ); expect( tester.getSize(find.byKey(const Key('favorite-card-with-synopsis'))), tester.getSize(find.byKey(const Key('favorite-card-without-synopsis'))), ); }); testWidgets('should_keep_card_and_show_failure_when_remove_fails', ( tester, ) async { final auth = await _auth(signedIn: true); final repository = InMemoryFavoriteRepository( saved: {'signal'}, cards: [_card('signal')], )..failRemove = true; final favorites = FavoriteController(repository); final haptics = FakeHaptics(); await _pumpPage(tester, auth: auth, favorites: favorites, haptics: haptics); await tester.tap(find.byKey(const Key('favorite-remove-signal'))); await tester.pumpAndSettle(); expect(haptics.lightImpacts, 1); expect(find.text('Drama signal'), findsOneWidget); expect( find.text('Could not update favorites. Please retry.'), findsOneWidget, ); }); testWidgets('should_show_empty_state_when_favorite_list_is_empty', ( tester, ) async { final auth = await _auth(signedIn: true); final favorites = FavoriteController(InMemoryFavoriteRepository()); await _pumpPage(tester, auth: auth, favorites: favorites); expect(find.byKey(const Key('favorites-empty')), findsOneWidget); expect(find.byType(EmptyState), findsOneWidget); final viewport = tester.getRect( find.byKey(const Key('favorites-empty-scroll')), ); final content = tester.getRect( find.byKey(const Key('empty-state-content')), ); expect(content.center.dx, closeTo(viewport.center.dx, 0.1)); expect(content.center.dy, closeTo(viewport.center.dy, 0.1)); }); testWidgets('should_show_retry_when_favorite_list_fails', (tester) async { final auth = await _auth(signedIn: true); final repository = InMemoryFavoriteRepository()..failPage = true; final favorites = FavoriteController(repository); await _pumpPage(tester, auth: auth, favorites: favorites); expect(find.byKey(const Key('favorites-error')), findsOneWidget); expect(find.byKey(const Key('favorites-retry')), findsOneWidget); }); testWidgets('should_show_device_favorites_when_signed_out', (tester) async { final auth = await _auth(signedIn: false); final repository = InMemoryFavoriteRepository( saved: {'signal'}, cards: [_card('signal')], ); final favorites = FavoriteController(repository); await _pumpPage(tester, auth: auth, favorites: favorites); expect(find.byKey(const Key('auth-gate-panel')), findsNothing); expect(find.text('Drama signal'), findsOneWidget); expect(repository.pageCalls, 1); }); testWidgets('should_open_player_when_favorite_card_is_tapped', ( tester, ) async { await _auth(signedIn: true); final catalog = FakeCatalogRepository(); final drama = catalog.dramas.first; await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, categoryRepository: FakeCategoryRepository(catalog: catalog), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository( saved: {drama.id}, cards: [drama], ), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-mine'))); await tester.pumpAndSettle(); await tester.ensureVisible(find.byKey(const Key('mine-favorites'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('mine-favorites'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(Key('favorite-card-${drama.id}'))); await tester.pumpAndSettle(); expect(find.byType(PlayerPage), findsOneWidget); expect(find.byType(DramaDetailPage), findsNothing); }); } Future _auth({required bool signedIn}) async { if (signedIn) { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({ 'auth.accessToken': 'test-access', 'auth.refreshToken': 'test-refresh', 'auth.user': '{"uid":"1","nickname":"Viewer","avatarUrl":""}', }); } final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: FakeHttpAdapter( (_) async => FakeHttpResponse('{"code":0,"message":"ok","data":null}'), ), ); final auth = AuthController(AuthApi(client), AuthSessionStore()); await auth.load(); return auth; } Future _pumpPage( WidgetTester tester, { required AuthController auth, required FavoriteController favorites, FakeHaptics? haptics, }) async { await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: auth), ChangeNotifierProvider.value(value: favorites), Provider.value(value: haptics ?? FakeHaptics()), ], child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const FavoritesPage(), ), ), ); await tester.pumpAndSettle(); }