// 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/home_feed.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/widgets/app_pull_to_refresh.dart'; import 'package:fruitshorts/core/widgets/paged_local_list.dart'; import 'package:fruitshorts/pages/discover/discover_view.dart'; import 'package:fruitshorts/pages/discover/widgets/drama_grid.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_category.dart'; import '../support/fake_haptics.dart'; import '../support/fake_interaction_repositories.dart'; import '../support/widget_player.dart'; void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets('should_open_ranking_sorted_by_views', (tester) async { await _pumpTheater(tester); await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('ranking-page')), findsOneWidget); _expectUnifiedHeader(tester, const Key('ranking-back')); expect( tester .widget( find.descendant( of: find.byKey(const Key('ranking-top-1')), matching: find.byType(Text), ), ) .data, '1', ); expect( tester.widget(find.byKey(const Key('ranking-title-1'))).data, 'The Signal', ); await tester.tap(find.byKey(const Key('ranking-top-1'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-scaffold')), findsOneWidget); }); testWidgets('should_open_new_releases_sorted_by_id', (tester) async { await _pumpTheater(tester); await tester.tap(find.byKey(const Key('theater-toolbar-new'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('new-releases-page')), findsOneWidget); _expectUnifiedHeader(tester, const Key('new-releases-back')); final newPage = tester.widget( find.byKey(const Key('new-releases-page')), ); expect(newPage.backgroundColor, AppColors.ink); final badge = tester.widget( find.byKey(const Key('new-releases-hero-badge')), ); expect((badge.decoration! as BoxDecoration).color, AppColors.blue); expect(find.byKey(const Key('new-releases-hero')), findsOneWidget); expect(find.text('Vengeance Rising'), findsWidgets); expect( find.byKey(const Key('new-release-card-the-signal-demo')), findsOneWidget, ); expect( find.byKey(const Key('new-release-card-city-lights-demo')), findsOneWidget, ); await tester.tap(find.byKey(const Key('new-releases-hero'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-scaffold')), findsOneWidget); }); testWidgets('should_open_reserve_empty_state', (tester) async { await _pumpTheater(tester); await tester.tap(find.byKey(const Key('theater-toolbar-reserve'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('reserve-page')), findsOneWidget); _expectUnifiedHeader(tester, const Key('reserve-back')); expect(find.byKey(const Key('reserve-week-strip')), findsOneWidget); for (var index = 0; index < 7; index++) { expect(find.byKey(Key('reserve-day-$index')), findsOneWidget); } expect(find.byKey(const Key('reserve-empty')), findsOneWidget); for (var index = 1; index <= 3; index++) { expect(find.byKey(Key('reserve-step-$index')), findsOneWidget); } final todayIndex = DateTime.now().weekday - 1; final today = tester.widget( find.descendant( of: find.byKey(Key('reserve-day-$todayIndex')), matching: find.byType(Container), ), ); expect((today.decoration! as BoxDecoration).color, AppColors.blue); await tester.tap(find.byKey(const Key('reserve-go-new'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('new-releases-page')), findsOneWidget); }); testWidgets('should_render_uniform_rows_with_cover_badges', (tester) async { await _pumpTheater(tester); await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); await tester.pumpAndSettle(); for (var rank = 1; rank <= 3; rank++) { expect(find.byKey(Key('ranking-row-${rank - 1}')), findsOneWidget); expect( tester.getSize(find.byKey(Key('ranking-row-cover-$rank'))), const Size(96, 128), ); final badge = tester.widget( find.byKey(Key('ranking-top-$rank')), ); final decoration = badge.decoration! as BoxDecoration; final expected = switch (rank) { 1 => AppColors.brandRed, 2 => AppColors.homeCrownGold, _ => AppColors.bronze, }; expect(decoration.color, expected); } }); testWidgets('should_list_every_rank_as_row', (tester) async { await _pumpTheater(tester, dramas: _rankDramas(5)); await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); await tester.pumpAndSettle(); for (var index = 0; index < 5; index++) { expect(find.byKey(Key('ranking-row-$index')), findsOneWidget); } final rankNumber = tester.widget( find.descendant( of: find.byKey(const Key('ranking-top-4')), matching: find.byType(Text), ), ); expect(rankNumber.data, '4'); expect(rankNumber.style?.fontSize, 14); expect(rankNumber.style?.fontWeight, FontWeight.w700); final badge = tester.widget( find.byKey(const Key('ranking-top-4')), ); expect( tester.getSize(find.byKey(const Key('ranking-top-4'))), const Size(24, 24), ); expect( (badge.decoration! as BoxDecoration).borderRadius, const BorderRadius.only( topLeft: Radius.circular(AppDimens.rankingCoverRadius), bottomRight: Radius.circular(10), ), ); expect( tester.getSize(find.byKey(const Key('ranking-row-3'))), const Size(362, 128), ); expect( tester.getSize(find.byKey(const Key('ranking-row-cover-4'))), const Size(96, 128), ); }); testWidgets('should_render_single_row_when_only_one_drama', (tester) async { await _pumpTheater(tester, dramas: _rankDramas(1)); await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('ranking-top-1')), findsOneWidget); expect(find.byKey(const Key('ranking-top-2')), findsNothing); expect(find.byKey(const Key('ranking-top-3')), findsNothing); }); testWidgets('should_open_player_from_new_releases_card', (tester) async { await _pumpTheater(tester); await tester.tap(find.byKey(const Key('theater-toolbar-new'))); await tester.pumpAndSettle(); final card = find.byKey(const Key('new-release-card-the-signal-demo')); await tester.ensureVisible(card); await tester.pumpAndSettle(); await tester.tap(card); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-scaffold')), findsOneWidget); }); testWidgets('should_go_back_to_theater_from_ranking_page', (tester) async { await _pumpTheater(tester); await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('ranking-back'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('ranking-page')), findsNothing); expect(find.byKey(const Key('discover-search')), findsOneWidget); }); testWidgets('should_page_ranking_and_reset_after_refresh', (tester) async { final repository = _CountingCatalogRepository(dramas: _rankDramas(40)); await _pumpTheater(tester, catalogRepository: repository); await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); await tester.pumpAndSettle(); final list = find.byKey(const Key('ranking-list')); expect(tester.widget(list).physics, AppPullToRefresh.physics); final position = _scrollPosition(tester, const Key('ranking-list')); // jumpTo 不发滚动通知,滑到底后再拖一下才会触发加载。 position.jumpTo(position.maxScrollExtent); await tester.pump(); await tester.drag(list, const Offset(0, -60)); await tester.pump(); expect(find.byKey(const Key('ranking-row-19')), findsOneWidget); expect(find.byKey(const Key('ranking-row-20')), findsNothing); // 加载指示出现后会撑高列表,可视区外被 finder 视为 offstage,直接看状态。 expect(_loadFooter(tester).loading, isTrue); await tester.pump(const Duration(milliseconds: 299)); expect(_loadFooter(tester).loading, isTrue); await tester.pump(const Duration(milliseconds: 1)); position.jumpTo(position.maxScrollExtent); await tester.pump(); expect(find.byKey(const Key('ranking-row-39')), findsOneWidget); expect(find.byKey(const Key('ranking-row-40')), findsNothing); expect(find.byKey(const Key('ranking-no-more')), findsNothing); position.jumpTo(0); await tester.pump(); final beforeRefresh = repository.fetchCalls; await tester.drag(list, const Offset(0, 240)); await tester.pump(); await tester.pumpAndSettle(); expect(repository.fetchCalls, beforeRefresh + 1); expect(position.pixels, 0); // 刷新重置回 20 项后,滑到底再触发一次加载,应恢复 40 项。 final refreshed = _scrollPosition(tester, const Key('ranking-list')); refreshed.jumpTo(refreshed.maxScrollExtent); await tester.pumpAndSettle(); refreshed.jumpTo(refreshed.maxScrollExtent); await tester.pumpAndSettle(); expect(find.byKey(const Key('ranking-row-39')), findsOneWidget); expect(find.byKey(const Key('ranking-no-more')), findsNothing); }); testWidgets('should_page_new_releases_and_reset_after_refresh', ( tester, ) async { final repository = _CountingCatalogRepository(dramas: _rankDramas(40)); await _pumpTheater(tester, catalogRepository: repository); await tester.tap(find.byKey(const Key('theater-toolbar-new'))); await tester.pumpAndSettle(); final list = find.byKey(const Key('new-releases-list')); expect( tester.widget(list).physics, AppPullToRefresh.physics, ); // 网格懒加载后只保留可见行,页数直接看传给网格的剧目数。 List gridDramas() => tester .widget(find.byKey(const Key('new-releases-all-grid'))) .dramas; expect(gridDramas(), hasLength(20)); // jumpTo 不发滚动通知,滑到底后再拖一下才会触发加载。 final position = _scrollPosition(tester, const Key('new-releases-list')); position.jumpTo(position.maxScrollExtent); await tester.pump(); await tester.drag(list, const Offset(0, -60)); await tester.pump(); expect(find.byKey(const Key('new-releases-load-more')), findsOneWidget); await tester.pumpAndSettle(); expect(gridDramas(), hasLength(40)); position.jumpTo(0); await tester.pump(); final beforeRefresh = repository.fetchCalls; await tester.drag(list, const Offset(0, 240)); await tester.pump(); await tester.pumpAndSettle(); expect(repository.fetchCalls, beforeRefresh + 1); expect(position.pixels, 0); expect(gridDramas(), hasLength(20)); }); testWidgets('should_refresh_reserve_without_load_more', (tester) async { final repository = _CountingCatalogRepository(dramas: _rankDramas(3)); await _pumpTheater(tester, catalogRepository: repository); await tester.tap(find.byKey(const Key('theater-toolbar-reserve'))); await tester.pumpAndSettle(); final beforeRefresh = repository.fetchCalls; final list = find.byKey(const Key('reserve-list')); expect(tester.widget(list).physics, AppPullToRefresh.physics); await tester.drag(list, const Offset(0, 240)); await tester.pump(); await tester.pumpAndSettle(); expect(repository.fetchCalls, beforeRefresh + 1); expect(find.byKey(const Key('reserve-load-more')), findsNothing); expect(find.byKey(const Key('reserve-no-more')), findsNothing); }); } Future _pumpTheater( WidgetTester tester, { List? dramas, FakeCatalogRepository? catalogRepository, }) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final catalog = catalogRepository ?? FakeCatalogRepository(dramas: dramas); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, searchRepository: catalog, categoryRepository: FakeCategoryRepository(catalog: catalog), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); Navigator.of( tester.element(find.byKey(const Key('home-tabs'))), ).push(MaterialPageRoute(builder: (_) => const DiscoverPage())); await tester.pumpAndSettle(); } ScrollPosition _scrollPosition(WidgetTester tester, Key listKey) { final scrollable = find.descendant( of: find.byKey(listKey), matching: find.byType(Scrollable), ); return tester.state(scrollable.first).position; } PagedLoadFooter _loadFooter(WidgetTester tester) { return tester.widget( find.descendant( of: find.byKey(const Key('ranking-page'), skipOffstage: false), matching: find.byType(PagedLoadFooter, skipOffstage: false), ), ); } class _CountingCatalogRepository extends FakeCatalogRepository { _CountingCatalogRepository({super.dramas}); int fetchCalls = 0; @override Future fetchHomeFeed() { fetchCalls++; return super.fetchHomeFeed(); } } List _rankDramas(int count) => [ for (var index = 0; index < count; index++) Drama( id: 'rank-$index', title: 'Rank $index', synopsis: 'synopsis', posterUrl: '', totalEpisodes: 10, viewCount: (count - index) * 100, episodes: [ Episode( id: 'rank-$index-1', number: 1, title: 'Episode 1', isFree: true, ), ], ), ]; void _expectUnifiedHeader(WidgetTester tester, Key backKey) { expect(tester.getSize(find.byKey(backKey)), const Size(40, 40)); final title = tester.widget(find.byKey(const Key('subpage-title'))); expect(title.style?.fontSize, 18); expect(title.style?.fontWeight, FontWeight.w400); }