// 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/feed_page.dart'; import 'package:fruitshorts/core/data/models/home_feed.dart'; import 'package:fruitshorts/core/data/repositories/catalog_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/theme/app_dimens.dart'; import 'package:fruitshorts/pages/home/widgets/home_feed_card.dart'; import 'package:fruitshorts/pages/player/player_view.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_category.dart'; import '../support/fake_haptics.dart'; import '../support/fake_interaction_repositories.dart'; import '../support/widget_player.dart'; const _categories = [ ContentCategory(id: 'costume', name: '古装'), ContentCategory(id: 'community', name: '社群'), ContentCategory(id: 'uplifting', name: '爽剧'), ContentCategory(id: 'sweet', name: '甜宠'), ContentCategory(id: 'face-slap', name: '打脸虐渣'), ContentCategory(id: 'angst', name: '虐恋'), ]; void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets('should_render_primary_sections_and_secondary_tags_in_order', ( tester, ) async { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); final sectionKeys = [ const Key('home-section-featured'), const Key('home-section-new'), const Key('home-section-categories'), const Key('home-section-ranking'), const Key('home-section-category-古装'), const Key('home-section-category-社群'), ]; final sectionCenters = sectionKeys .map((key) => tester.getCenter(find.byKey(key)).dx) .toList(); expect(sectionCenters, orderedEquals(sectionCenters.toList()..sort())); final tagKeys = [ const Key('home-tag-hot'), const Key('home-tag-uplifting'), const Key('home-tag-sweet'), const Key('home-tag-faceSlap'), const Key('home-tag-angst'), ]; final tagCenters = tagKeys .map((key) => tester.getCenter(find.byKey(key)).dx) .toList(); expect(tagCenters, orderedEquals(tagCenters.toList()..sort())); expect(find.text('Featured'), findsOneWidget); expect(find.text('New'), findsOneWidget); expect(find.text('Categories'), findsOneWidget); expect(find.text('Charts'), findsOneWidget); expect(find.text('Costume'), findsOneWidget); expect(find.text('Community'), findsOneWidget); expect(find.byKey(const Key('mine-mini-player')), findsNothing); expect(find.byType(FloatingActionButton), findsNothing); }); testWidgets('should_hide_unmatched_content_sections', (tester) async { final repository = _PagedCatalogRepository(); await _pumpApp( tester, repository, categories: const [ContentCategory(id: 'romance', name: 'Romance')], ); expect(find.byKey(const Key('home-section-category-古装')), findsNothing); expect(find.byKey(const Key('home-section-category-社群')), findsNothing); expect(find.byKey(const Key('home-tag-uplifting')), findsNothing); }); testWidgets('should_switch_home_sections_without_pushing_pages', ( tester, ) async { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); String firstDramaId() { return tester .widget(find.byType(HomeFeedCard).first) .drama .id; } expect(find.byKey(const Key('home-grid')), findsOneWidget); expect(firstDramaId(), 'page1-0'); await tester.tap(find.byKey(const Key('home-section-new'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('new-releases-page')), findsNothing); expect(find.byKey(const Key('home-secondary-tags')), findsNothing); expect(firstDramaId(), 'page1-9'); await tester.tap(find.byKey(const Key('home-section-ranking'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('ranking-page')), findsNothing); expect(firstDramaId(), 'page1-0'); await tester.tap(find.byKey(const Key('home-section-categories'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('filter-page')), findsNothing); expect(find.byKey(const Key('home-secondary-tags')), findsNothing); expect(find.byKey(const Key('home-grid-cards')), findsOneWidget); await tester.tap(find.byKey(const Key('home-section-category-古装'))); await tester.pumpAndSettle(); expect(firstDramaId(), 'page1-0'); await tester.tap(find.byKey(const Key('home-section-category-社群'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-grid-empty')), findsOneWidget); expect(find.byKey(const Key('filter-page')), findsNothing); }); testWidgets('should_keep_scroll_position_when_switching_sections', ( tester, ) async { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); await tester.drag( find.byKey(const Key('home-grid-scroll')), const Offset(0, -320), ); await tester.pumpAndSettle(); final scrollable = find .descendant( of: find.byKey(const Key('home-grid-scroll')), matching: find.byType(Scrollable), ) .first; final before = tester.state(scrollable).position.pixels; expect(before, greaterThan(0)); await tester.tap(find.byKey(const Key('home-section-new'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('home-section-featured'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); final afterScrollable = find .descendant( of: find.byKey(const Key('home-grid-scroll')), matching: find.byType(Scrollable), ) .first; expect( tester.state(afterScrollable).position.pixels, closeTo(before, 0.1), ); }); testWidgets('should_swipe_between_primary_sections', (tester) async { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); await tester.drag( find.byKey(const Key('home-primary-page-view')), const Offset(-320, 0), ); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-secondary-tags')), findsNothing); final selected = tester.widget( find.byKey(const Key('home-primary-label-newReleases')), ); expect(selected.style.fontSize, AppDimens.homePrimaryTabSelectedFont); expect(selected.style.fontWeight, FontWeight.w700); await tester.drag( find.byKey(const Key('home-primary-page-view')), const Offset(320, 0), ); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); }); testWidgets('should_load_next_page_without_expanding_page_size', ( tester, ) async { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); expect(find.byKey(const Key('home-feed-card-page1-0')), findsOneWidget); await tester.drag( find.byKey(const Key('home-grid-scroll')), const Offset(0, -1200), ); await tester.pumpAndSettle(); expect(repository.cursors, contains('page1')); expect(repository.requestedSizes.every((size) => size <= 20), isTrue); await tester.scrollUntilVisible( find.byKey(const Key('home-feed-card-page2-1')), 260, scrollable: find .descendant( of: find.byKey(const Key('home-grid-refresh')), matching: find.byType(Scrollable), ) .first, ); expect(find.byKey(const Key('home-feed-card-page2-1')), findsOneWidget); }); testWidgets('should_open_player_when_grid_card_is_tapped', (tester) async { final repository = _PagedCatalogRepository(); await _pumpApp(tester, repository); await tester.tap(find.byKey(const Key('home-feed-card-page1-0'))); await tester.pumpAndSettle(); final player = tester.widget(find.byType(PlayerPage).last); expect(player.homeMode, isFalse); }); } Future _pumpApp( WidgetTester tester, _PagedCatalogRepository repository, { List categories = _categories, }) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: repository, categoryRepository: FakeCategoryRepository( catalog: repository, categories: categories, ), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository( cards: repository.allDramas, ), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); } class _PagedCatalogRepository implements CatalogRepository { _PagedCatalogRepository() : pageOne = List.generate(10, (index) => _drama('page1-$index')), pageTwo = List.generate(2, (index) => _drama('page2-$index')); final List pageOne; final List pageTwo; final List cursors = []; final List requestedSizes = []; List get allDramas => [...pageOne, ...pageTwo]; @override Future fetchHomeFeed() async => _feed(pageOne); @override Future fetchDiscoverFeed() async => _feed(pageOne); @override Future fetchFeed({ List types = const [], String? categoryId, String? cursor, int size = 10, }) async { requestedSizes.add(size); if (types.isNotEmpty) { return const FeedPage(dramas: []); } cursors.add(cursor); if (cursor == 'page1') { return FeedPage(dramas: pageTwo, hasMore: false); } return FeedPage(dramas: pageOne, nextCursor: 'page1', hasMore: true); } @override Future fetchDrama(String dramaId) async { return allDramas.firstWhere((drama) => drama.id == dramaId); } HomeFeed _feed(List dramas) { return HomeFeed( sections: [FeedSection(code: 'hot', title: 'Hot', dramas: dramas)], ); } } Drama _drama(String id) { final index = int.tryParse(id.split('-').last) ?? 0; return Drama( id: id, title: 'Grid Drama $id', synopsis: 'synopsis', posterUrl: '', genre: 'Romance', category: index.isEven ? '古装' : 'Urban', viewCount: (10 - index) * 10000, createdAt: DateTime(2026, 10, 1).add(Duration(days: index)), detailLoaded: true, episodes: [ Episode( id: '$id-1', number: 1, title: 'Episode 1', isFree: true, videoUrl: 'https://example.test/$id.mp4', ), ], ); }