import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.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 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/models/content_category.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/home_feed.dart'; import 'package:fruitshorts/core/data/models/watch_progress.dart'; import 'package:fruitshorts/core/data/repositories/category_repository.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/widgets/remote_image.dart'; import '../support/fake_catalog.dart'; import '../support/fake_interaction_repositories.dart'; import '../support/fake_notice_repository.dart'; import '../support/widget_player.dart'; void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets( 'should_show_all_dramas_in_two_column_sliver_grid_without_continue_watching', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp(tester, categoryRepository: _GridCategoryRepository()); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.text('Continue Watching'), findsNothing); expect(find.text('Recommended'), findsOneWidget); for (final key in const [ Key('discover-search-button'), Key('discover-profile'), ]) { expect(tester.getSize(find.byKey(key)), const Size(36, 36)); expect( tester .widget( find.descendant( of: find.byKey(key), matching: find.byType(Icon), ), ) .size, 18, ); } expect(find.text('For You'), findsNothing); final grid = tester.widget( find.byKey(const Key('home-all-dramas-grid')), ); final delegate = grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; expect(delegate.crossAxisCount, 2); expect(delegate.crossAxisSpacing, 10); expect(delegate.mainAxisSpacing, 10); expect(delegate.mainAxisExtent, 164); expect( tester .widget( find.byKey(const Key('home-all-dramas-section')), ) .padding, const EdgeInsets.symmetric(horizontal: 14), ); expect(find.byType(GridView), findsNothing); expect(find.byType(CustomScrollView), findsOneWidget); expect(find.byType(RemoteImage), findsWidgets); expect( tester .getSize(find.byKey(const Key('home-drama-cover-city-lights-demo'))) .height, 120, ); expect( tester .widget( find.byKey(const Key('home-drama-title-city-lights-demo')), ) .maxLines, 1, ); expect( tester .widget( find.byKey(const Key('home-drama-genre-city-lights-demo')), ) .maxLines, 1, ); expect(tester.takeException(), isNull); }, ); testWidgets('should_update_grid_when_category_is_selected', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp(tester, categoryRepository: _GridCategoryRepository()); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsWidgets); expect(find.text('City Lights & Velvet Nights'), findsWidgets); await tester.tap(find.text('Romance')); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsNothing); expect(find.text('City Lights & Velvet Nights'), findsWidgets); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsNothing, ); expect( find.byKey(const Key('home-drama-card-city-lights-demo')), findsOneWidget, ); }); testWidgets('should_keep_empty_messages_when_feed_has_no_dramas', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp( tester, categoryRepository: _GridCategoryRepository( discoverFeed: const HomeFeed(), ), ); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.text('No matching dramas.'), findsOneWidget); expect(find.byKey(const Key('empty-state-content')), findsOneWidget); await tester.tap(find.text('Romance')); await tester.pumpAndSettle(); expect(find.text('No dramas in this category yet.'), findsOneWidget); }); testWidgets('should_show_retry_when_category_feed_fails', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp(tester, categoryRepository: _FailingCategoryRepository()); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.text('Categories could not be loaded.'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); expect(find.byKey(const Key('empty-state-icon')), findsOneWidget); final icon = tester.getCenter(find.byKey(const Key('empty-state-icon'))); final feed = tester.getRect(find.byType(CustomScrollView)); expect(icon.dx, closeTo(feed.center.dx, 1)); expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing); }); testWidgets('should_fit_two_column_grid_on_compact_screen', (tester) async { tester.view.physicalSize = const Size(320, 568); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp(tester, categoryRepository: _GridCategoryRepository()); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.byKey(const Key('home-drama-card-city-lights-demo')), 180, scrollable: find.byType(Scrollable).last, ); await tester.pumpAndSettle(); expect(find.text('City Lights & Velvet Nights'), findsWidgets); expect(tester.takeException(), isNull); }); testWidgets('should_filter_trending_grid_when_search_text_changes', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp(tester, categoryRepository: _GridCategoryRepository()); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsOneWidget, ); expect( find.byKey(const Key('home-drama-card-city-lights-demo')), findsOneWidget, ); await tester.enterText(find.byKey(const Key('discover-search')), 'velvet'); await tester.pumpAndSettle(); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsNothing, ); expect( find.byKey(const Key('home-drama-card-city-lights-demo')), findsOneWidget, ); await tester.enterText(find.byKey(const Key('discover-search')), ''); await tester.pumpAndSettle(); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsOneWidget, ); }); testWidgets('should_show_continue_watching_only_when_a_record_exists', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await _pumpApp( tester, categoryRepository: _GridCategoryRepository(), history: InMemoryWatchHistoryRepository( seed: const [ WatchProgress( dramaId: 'city-lights-demo', episodeId: 'city-lights-demo-1', position: Duration(seconds: 42), duration: Duration(minutes: 2), ), ], ), ); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('discover-continue-watching')), findsOneWidget); expect(find.byKey(const Key('discover-continue-play')), findsOneWidget); expect(find.text('City Lights & Velvet Nights'), findsWidgets); expect(find.byKey(const Key('discover-quick-filter')), findsOneWidget); expect(find.byKey(const Key('discover-quick-charts')), findsOneWidget); expect(find.byKey(const Key('discover-quick-new')), findsOneWidget); expect(find.byKey(const Key('discover-quick-watchlist')), findsOneWidget); }); } Future _pumpApp( WidgetTester tester, { required CategoryRepository categoryRepository, WatchHistoryRepository? history, }) async { final catalog = FakeCatalogRepository(); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, categoryRepository: categoryRepository, watchHistoryRepository: history ?? InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), ), ); await tester.pumpAndSettle(); } class _GridCategoryRepository implements CategoryRepository { _GridCategoryRepository({HomeFeed? discoverFeed}) : discoverFeed = discoverFeed ?? _defaultDiscoverFeed; final HomeFeed discoverFeed; @override Future> fetchCategories() async => const [ ContentCategory(id: '7', name: 'Romance'), ]; @override Future fetchDiscoverFeed() async => discoverFeed; } class _FailingCategoryRepository implements CategoryRepository { @override Future> fetchCategories() async => const [ ContentCategory(id: '7', name: 'Romance'), ]; @override Future fetchDiscoverFeed() async { throw StateError('discover feed unavailable'); } } final _defaultDiscoverFeed = HomeFeed( sections: [ FeedSection(code: 'hot', title: 'Hot', dramas: demoDramas), FeedSection( code: 'category-7', title: 'Romance', dramas: [ Drama( id: 'city-lights-demo', title: 'City Lights & Velvet Nights', synopsis: 'City Lights & Velvet Nights synopsis', posterUrl: '/files/city-lights-demo.png', genre: 'Urban Romance', detailLoaded: true, ), ], ), ], );