// 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/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/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: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_interaction_repositories.dart'; import '../support/fake_notice_repository.dart'; import '../support/widget_player.dart'; void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets('should_show_search_category_language_chips_and_drama_grid', ( tester, ) async { await _pumpTheater(tester, categoryRepository: _GridCategoryRepository()); expect(find.text('Continue Watching'), findsNothing); expect(find.byKey(const Key('discover-search')), findsOneWidget); expect(find.byKey(const Key('theater-category-chips')), findsOneWidget); expect(find.byKey(const Key('theater-language-chips')), findsNothing); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('should_filter_grid_when_category_chip_is_tapped', ( tester, ) async { await _pumpTheater(tester, categoryRepository: _GridCategoryRepository()); await tester.tap(find.byKey(const Key('theater-category-7'))); await tester.pumpAndSettle(); expect( find.byKey(const Key('home-drama-card-city-lights-demo')), findsOneWidget, ); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsNothing, ); }); testWidgets('should_keep_discover_header_fixed_when_content_scrolls', ( tester, ) async { await _pumpTheater( tester, categoryRepository: _GridCategoryRepository( discoverFeed: _tallDiscoverFeed, ), ); final headerBefore = tester.getTopLeft( find.byKey(const Key('discover-search')), ); final scrollable = find.descendant( of: find.byKey(const Key('discover-refresh')), matching: find.byType(Scrollable), ); await tester.drag(find.byType(ListView), const Offset(0, -140)); await tester.pumpAndSettle(); expect( tester.getTopLeft(find.byKey(const Key('discover-search'))), headerBefore, ); expect( tester.state(scrollable).position.pixels, greaterThan(0), ); }); testWidgets('should_show_empty_and_retry_states', (tester) async { await _pumpTheater( tester, categoryRepository: _GridCategoryRepository( discoverFeed: const HomeFeed(), ), ); expect(find.byKey(const Key('discover-empty')), findsOneWidget); await tester.pumpWidget(const SizedBox.shrink()); await _pumpTheater( tester, categoryRepository: _FailingCategoryRepository(), ); expect(find.byKey(const Key('home-categories-error')), findsOneWidget); expect(find.text('Retry'), findsOneWidget); }); testWidgets('should_show_search_results_when_text_changes', (tester) async { await _pumpTheater(tester, categoryRepository: _GridCategoryRepository()); await tester.enterText(find.byKey(const Key('discover-search')), 'velvet'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); expect( find.byKey(const Key('home-drama-card-city-lights-demo')), findsOneWidget, ); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsNothing, ); }); testWidgets('should_refresh_feed_when_pulled_down', (tester) async { final counter = _CountingCategoryRepository( _GridCategoryRepository(discoverFeed: _tallDiscoverFeed), ); await _pumpTheater(tester, categoryRepository: counter); final before = counter.categoryLoads + counter.feedLoads; await tester.drag(find.byType(ListView), const Offset(0, 320)); await tester.pump(); await tester.pump(const Duration(seconds: 1)); await tester.pumpAndSettle(); expect(counter.categoryLoads + counter.feedLoads, greaterThan(before)); expect(find.byType(RefreshProgressIndicator), findsNothing); }); } Future _pumpTheater( WidgetTester tester, { required CategoryRepository categoryRepository, }) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final catalog = FakeCatalogRepository(); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, searchRepository: catalog, categoryRepository: categoryRepository, watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-theater'))); 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, ), ], ), ], ); final _tallDiscoverFeed = HomeFeed( sections: [ FeedSection( code: 'hot', title: 'Hot', dramas: [ for (var index = 0; index < 12; index++) Drama( id: 'tall-$index', title: 'Tall Drama $index', synopsis: 'Tall Drama $index synopsis', posterUrl: '/files/tall-$index.png', genre: 'Urban Romance', detailLoaded: true, ), ], ), ], ); class _CountingCategoryRepository implements CategoryRepository { _CountingCategoryRepository(this.delegate); final CategoryRepository delegate; int categoryLoads = 0; int feedLoads = 0; @override Future> fetchCategories() { categoryLoads++; return delegate.fetchCategories(); } @override Future fetchDiscoverFeed() { feedLoads++; return delegate.fetchDiscoverFeed(); } }