// Author: fruit.com.cn import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/gestures.dart'; 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/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/playback_thumbnail_service.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/theme/app_theme.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/home/home_feed_channel.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:video_player/video_player.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'; void main() { testWidgets('should_render_three_column_explorer_grid_on_home', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final catalog = FakeCatalogRepository( banners: const [ FeedBanner( id: 'banner-1', imageUrl: '/files/banner.jpg', linkType: 'DRAMA', linkValue: 'the-signal-demo', ), ], ); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, categoryRepository: FakeCategoryRepository( catalog: catalog, categories: const [ ContentCategory(id: 'costume', name: '古装'), ContentCategory(id: 'community', name: '社群'), ], ), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-grid')), findsOneWidget); expect(find.byKey(const Key('home-grid-cards')), findsOneWidget); final grid = tester.widget( find.byKey(const Key('home-grid-cards')), ); final delegate = grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; expect(delegate.crossAxisCount, 3); expect(delegate.crossAxisSpacing, AppDimens.homeGridGap); expect(delegate.mainAxisSpacing, AppDimens.homeGridRowGap); expect(find.byKey(const Key('home-section-featured')), findsOneWidget); expect(find.byKey(const Key('home-section-new')), findsOneWidget); expect(find.byKey(const Key('home-section-categories')), findsOneWidget); expect(find.byKey(const Key('home-section-ranking')), findsOneWidget); expect(find.byKey(const Key('home-section-category-古装')), findsOneWidget); expect(find.byKey(const Key('home-section-category-社群')), findsOneWidget); expect(find.byKey(const Key('home-secondary-tags')), findsNothing); expect(find.byKey(const Key('home-grid-filter')), findsNothing); final searchBox = find.descendant( of: find.byKey(const Key('home-search-entry')), matching: find.byType(Container), ); expect(tester.getSize(searchBox).height, AppDimens.searchHeight); expect( (tester.widget(searchBox).decoration! as BoxDecoration) .borderRadius, BorderRadius.circular(AppDimens.searchRadius), ); final placeholder = tester.widget( find.text('Search dramas, actors, genres...'), ); expect(placeholder.style?.fontSize, AppDimens.searchPlaceholderFont); expect(placeholder.style?.color, AppColors.textSecondary); final selectedPrimary = tester.widget( find.byKey(const Key('home-primary-label-featured')), ); final unselectedPrimary = tester.widget( find.byKey(const Key('home-primary-label-newReleases')), ); expect( selectedPrimary.style.fontSize, AppDimens.homePrimaryTabSelectedFont, ); expect(selectedPrimary.style.fontWeight, FontWeight.w700); expect(selectedPrimary.style.color, AppColors.navSelected); expect(selectedPrimary.duration, AppDimens.homePrimaryTabAnimationDuration); expect(unselectedPrimary.style.fontSize, AppDimens.homePrimaryTabFont); expect(unselectedPrimary.style.fontWeight, FontWeight.w400); expect(unselectedPrimary.style.color, AppColors.textTabInactive); expect( find.descendant( of: find.byKey(const Key('home-section-featured')), matching: find.byType(Container), ), findsNothing, ); final primaryStart = tester.getTopLeft(find.text('Featured')); final pageStart = tester.getTopLeft(searchBox); expect(primaryStart.dx, pageStart.dx); final primaryGap = tester.getTopLeft(find.text('New')).dx - tester.getBottomRight(find.text('Featured')).dx; expect(primaryGap, closeTo(AppDimens.homePrimaryCategoryGap, 0.1)); expect(find.byType(PageView), findsNothing); expect(tester.takeException(), isNull); }); testWidgets('should_show_immersive_player_and_open_playlist_in_recommend', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); 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, categoryRepository: FakeCategoryRepository(catalog: catalog), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-grid')), findsNothing); expect(find.byKey(const Key('recommend-page')), findsOneWidget); expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); expect(find.byType(PlayerPage), findsOneWidget); expect(tester.widget(find.byType(PlayerPage)).homeMode, isTrue); expect(find.byKey(const Key('recommend-section-hits')), findsNothing); await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); await tester.tap( find.byKey(const Key('recommend-card-hits-the-signal-demo')), ); await tester.pumpAndSettle(); expect( tester.widget(find.byType(PlayerPage).last).homeMode, isFalse, ); }); testWidgets('should_render_new_feed_structure_with_formatted_counts', ( tester, ) async { const drama = Drama( id: 'feed-drama', title: 'Feed Drama', synopsis: 'A long synopsis follows the heroine through a dangerous city, ' 'a hidden identity, and a final choice that changes everyone around ' 'her forever.', posterUrl: '', genre: 'Romance', category: 'Urban', totalEpisodes: 8, freeEpisodes: 8, viewCount: 419000, likeCount: 1006, favoriteCount: 32000, commentCount: 12, ); var episodesOpened = 0; await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: drama, episode: const Episode( id: 'feed-episode', number: 2, title: 'Episode 2', isFree: true, ), controller: null, locked: false, liked: true, saved: false, visible: true, boosting: false, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, likeCount: 1006, favoriteCount: 32000, commentCount: 12, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onEpisodes: () => episodesOpened++, onMore: () {}, ), ), ), ), ); await tester.pumpAndSettle(); final seriesTitle = tester.widget( find.byKey(const Key('home-feed-series-title')), ); expect(seriesTitle.data, 'Feed Drama'); expect(find.text('Episode 2'), findsNothing); expect(seriesTitle.style?.fontSize, 19); expect(seriesTitle.style?.fontWeight, FontWeight.w600); expect(seriesTitle.style?.color, AppColors.textPrimary); final seriesChevron = tester.widget( find.descendant( of: find.byKey(const Key('home-feed-series-name')), matching: find.byIcon(Icons.chevron_right_rounded), ), ); expect(seriesChevron.size, 20); expect(seriesChevron.color, AppColors.textPrimary); expect( tester.getTopLeft(find.byIcon(Icons.chevron_right_rounded)).dx - tester .getTopRight(find.byKey(const Key('home-feed-series-title'))) .dx, 4, ); expect(find.byKey(const Key('home-feed-info-card')), findsNothing); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-tags')), findsOneWidget); expect(find.byKey(const Key('home-feed-synopsis-more')), findsOneWidget); await tester.tap(find.byKey(const Key('home-feed-synopsis-more'))); await tester.pump(); expect( tester.widget(find.byKey(const Key('home-feed-synopsis'))).maxLines, isNull, ); expect( tester .widget(find.byKey(const Key('home-feed-episode-progress'))) .data, 'Episode 2 / 8 total', ); expect(find.byKey(const Key('home-feed-meta-pills')), findsNothing); expect(find.byKey(const Key('home-feed-ranking-pill')), findsNothing); expect(find.byKey(const Key('home-feed-view-pill')), findsNothing); expect(find.text('Romance Urban'), findsOneWidget); final genreText = tester.widget(find.text('Romance Urban')); expect(genreText.style?.fontSize, 13); expect(genreText.style?.color, AppColors.overlayLabel); final episodeProgress = tester.widget( find.byKey(const Key('home-feed-episode-progress')), ); expect(episodeProgress.maxLines, 1); expect(episodeProgress.softWrap, isFalse); expect(episodeProgress.style?.fontSize, 13); expect(episodeProgress.style?.color, AppColors.overlayLabel); expect( tester .getTopLeft(find.byKey(const Key('home-feed-episode-progress'))) .dx - tester.getTopRight(find.text('Romance Urban')).dx, 8, ); expect( tester.widget(find.byKey(const Key('rail-like-count'))).data, '1006', ); expect( tester.widget(find.byKey(const Key('rail-save-count'))).data, '3.2万', ); expect( find.descendant( of: find.byKey(const Key('rail-like')), matching: find.byIcon(Icons.favorite_rounded), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('rail-save')), matching: find.byIcon(Icons.bookmark_border_rounded), ), findsOneWidget, ); expect(find.byKey(const Key('rail-episodes')), findsOneWidget); expect( find.descendant( of: find.byKey(const Key('rail-episodes')), matching: find.byIcon(Icons.layers_rounded), ), findsOneWidget, ); expect(find.byKey(const Key('rail-comments')), findsNothing); expect(find.byKey(const Key('rail-share')), findsNothing); expect(find.text('Comments'), findsNothing); expect( tester.getRect(find.byKey(const Key('home-feed-action-rail'))).right, closeTo( tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 8, 0.1, ), ); expect(find.byKey(const Key('progress-loading')), findsNothing); expect( find.byKey(const Key('home-feed-progress-inactive')), findsOneWidget, ); expect(find.byKey(const Key('home-feed-progress-thumb')), findsNothing); await tester.tap(find.byKey(const Key('rail-episodes'))); expect(episodesOpened, 1); }); testWidgets('should_show_static_track_without_seek_when_feed_is_locked', ( tester, ) async { var scrubStarts = 0; var seeks = 0; await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'locked-feed', title: 'Locked Feed', synopsis: 'synopsis', posterUrl: '', ), episode: const Episode( id: 'locked-feed-1', number: 1, title: 'Locked', isFree: false, locked: true, ), controller: null, locked: true, liked: false, saved: false, visible: true, boosting: false, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onMore: () {}, onScrubStart: () => scrubStarts++, onSeek: (_) => seeks++, ), ), ), ), ); expect(find.byKey(const Key('progress-loading')), findsNothing); expect(find.byKey(const Key('progress-loading-highlight')), findsNothing); expect( find.byKey(const Key('home-feed-progress-inactive')), findsOneWidget, ); expect( tester .getSize(find.byKey(const Key('home-feed-progress-inactive'))) .height, 2, ); await tester.drag( find.byKey(const Key('home-feed-progress')), const Offset(120, 0), ); await tester.pumpAndSettle(); expect(scrubStarts, 0); expect(seeks, 0); }); testWidgets('should_show_static_track_when_feed_video_is_uninitialized', ( tester, ) async { final controller = ValueNotifier( const VideoPlayerValue.uninitialized(), ); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'loading-feed', title: 'Loading Feed', synopsis: 'synopsis', posterUrl: '', ), episode: const Episode( id: 'loading-feed-1', number: 1, title: 'Loading', isFree: true, ), controller: controller, locked: false, liked: false, saved: false, visible: true, boosting: false, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onMore: () {}, onSeek: (_) {}, ), ), ), ), ); expect(find.byKey(const Key('progress-loading')), findsNothing); expect( find.byKey(const Key('home-feed-progress-inactive')), findsOneWidget, ); }); testWidgets('should_seek_and_open_player_from_feed_actions', (tester) async { var opened = 0; Duration? seeked; final scrubVisibility = []; final thumbnailService = _nullThumbnailService(); addTearDown(thumbnailService.dispose); final controller = ValueNotifier( VideoPlayerValue( duration: const Duration(seconds: 100), position: const Duration(seconds: 10), isInitialized: true, ), ); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'action-drama', title: 'Action Drama', synopsis: 'synopsis', posterUrl: '', totalEpisodes: 8, viewCount: 1200, ), episode: const Episode( id: 'action-episode', number: 1, title: 'Episode 1', isFree: true, ), controller: controller, locked: false, liked: false, saved: false, visible: true, boosting: false, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () => opened++, onMore: () {}, onScrubStart: () {}, onScrubVisibilityChanged: scrubVisibility.add, onSeek: (value) => seeked = value, thumbnailService: thumbnailService, ), ), ), ), ); await tester.pumpAndSettle(); final bar = tester.getRect(find.byKey(const Key('home-feed-full-series'))); expect(bar.height, 44); final overlayRect = tester.getRect( find.byKey(const Key('home-feed-gesture')), ); expect(bar.left, closeTo(overlayRect.left + 14, 0.1)); expect(bar.width, closeTo(overlayRect.width * 0.7, 1)); expect( tester.getCenter(find.byKey(const Key('home-feed-progress-track'))).dy - bar.bottom, greaterThanOrEqualTo(20), ); final buttonBackground = tester .widget( find.byKey(const Key('home-feed-full-series-bg')), ) .decoration as BoxDecoration; expect((buttonBackground.gradient! as LinearGradient).colors, [ AppColors.actionRed, AppColors.blue, ]); expect( tester.getRect(find.text('Watch all')).center.dy, closeTo(bar.center.dy, 2), ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(6, 6), ); expect( tester.getSize(find.byKey(const Key('home-feed-progress'))).height, greaterThanOrEqualTo(32), ); expect( tester .widget(find.byKey(const Key('home-feed-progress'))) .dragStartBehavior, DragStartBehavior.down, ); final idleTrackCenter = tester .getRect(find.byKey(const Key('home-feed-progress-track'))) .center .dy; expect( tester .getRect(find.byKey(const Key('home-feed-progress-thumb'))) .center .dy, closeTo( tester .getRect(find.byKey(const Key('home-feed-progress-track'))) .center .dy, 0.5, ), ); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); await gesture.moveBy(const Offset(120, 0)); await tester.pump(); expect(scrubVisibility.last, isTrue); expect(find.byKey(const Key('home-feed-series-title')), findsOneWidget); expect(find.byKey(const Key('rail-save')), findsOneWidget); expect( tester .widget( find.byKey(const Key('home-feed-content-opacity')), ) .opacity, 0, ); expect( tester .widget( find.byKey(const Key('home-feed-content-ignore')), ) .ignoring, isTrue, ); expect(find.byKey(const Key('scrub-preview-time-bubble')), findsOneWidget); final bubbleRect = tester.getRect( find.byKey(const Key('scrub-preview-time-bubble')), ); expect(bubbleRect.left, greaterThanOrEqualTo(12)); expect( bubbleRect.right, lessThanOrEqualTo( tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 12, ), ); expect( tester .getRect(find.byKey(const Key('home-feed-progress-track'))) .center .dy - bubbleRect.bottom, closeTo(16, 0.1), ); final bubbleContainer = tester.widget( find.descendant( of: find.byKey(const Key('scrub-preview-time-bubble')), matching: find.byType(Container), ), ); expect( (bubbleContainer.decoration! as BoxDecoration).color, AppColors.timeBubbleScrim, ); expect( bubbleContainer.padding, const EdgeInsets.symmetric(horizontal: 6, vertical: 3), ); await gesture.moveBy(const Offset(-600, 0)); await tester.pump(); expect( tester.getRect(find.byKey(const Key('scrub-preview-time-bubble'))).left, closeTo(12, 0.1), ); await gesture.moveBy(const Offset(1200, 0)); await tester.pump(); expect( tester.getRect(find.byKey(const Key('scrub-preview-time-bubble'))).right, closeTo( tester.getRect(find.byKey(const Key('home-feed-gesture'))).right - 12, 0.1, ), ); await tester.pump(const Duration(milliseconds: 130)); expect( tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(14, 14), ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 6, ); expect( tester .getRect(find.byKey(const Key('home-feed-progress-track'))) .center .dy, closeTo(idleTrackCenter, 0.1), ); expect( tester .getRect(find.byKey(const Key('home-feed-progress-thumb'))) .center .dy, closeTo( tester .getRect(find.byKey(const Key('home-feed-progress-track'))) .center .dy, 0.5, ), ); await gesture.up(); await tester.pumpAndSettle(); expect(scrubVisibility.last, isFalse); expect(find.byKey(const Key('home-feed-series-title')), findsOneWidget); expect( tester .widget( find.byKey(const Key('home-feed-content-opacity')), ) .opacity, 1, ); expect(seeked, isNotNull); expect(seeked!.inSeconds, greaterThan(0)); expect( tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), const Size(6, 6), ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 2, ); await tester.tap(find.byKey(const Key('home-feed-full-series'))); await tester.pump(); expect(opened, 1); }); testWidgets('should_debounce_feed_loading_and_keep_track_while_scrubbing', ( tester, ) async { final controller = ValueNotifier( const VideoPlayerValue( duration: Duration(seconds: 100), position: Duration(seconds: 10), isInitialized: true, ), ); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'debounce-drama', title: 'Debounce Drama', synopsis: 'synopsis', posterUrl: '', totalEpisodes: 8, ), episode: const Episode( id: 'debounce-episode', number: 1, title: 'Episode 1', isFree: true, ), controller: controller, locked: false, liked: false, saved: false, visible: true, boosting: false, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onMore: () {}, onSeek: (_) {}, ), ), ), ), ); await tester.pumpAndSettle(); controller.value = const VideoPlayerValue( duration: Duration(seconds: 100), position: Duration(seconds: 10), isInitialized: true, isBuffering: true, ); await tester.pump(const Duration(milliseconds: 200)); expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); expect(find.byKey(const Key('progress-loading')), findsNothing); await tester.pump(const Duration(milliseconds: 200)); expect(find.byKey(const Key('home-feed-progress-thumb')), findsNothing); expect(find.byKey(const Key('progress-loading')), findsOneWidget); controller.value = const VideoPlayerValue( duration: Duration(seconds: 100), position: Duration(seconds: 10), isInitialized: true, ); await tester.pump(); expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); await gesture.moveBy(const Offset(60, 0)); await tester.pump(); controller.value = const VideoPlayerValue( duration: Duration(seconds: 100), position: Duration(seconds: 10), isInitialized: true, isBuffering: true, ); await tester.pump(const Duration(milliseconds: 400)); expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); expect(find.byKey(const Key('progress-loading')), findsNothing); controller.value = const VideoPlayerValue( duration: Duration(seconds: 100), position: Duration(seconds: 10), isInitialized: true, ); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('should_show_refresh_spinner_until_refresh_finishes', ( tester, ) async { final completer = Completer(); var calls = 0; await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedHeader( l10n: AppLocalizations.of(context)!, channel: HomeFeedChannel.recommend, onChannelSelected: (_) {}, onRefresh: () { calls++; return completer.future; }, onSearch: () {}, onMore: () {}, ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('home-feed-refresh'))); await tester.pump(); expect(find.byKey(const Key('home-feed-refreshing')), findsOneWidget); await tester.tap(find.byKey(const Key('home-feed-refresh'))); await tester.pump(); expect(calls, 1); completer.complete(true); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-refreshing')), findsNothing); expect(find.byIcon(Icons.refresh_rounded), findsOneWidget); }); testWidgets('should_show_scrub_preview_card_from_home_progress', ( tester, ) async { final controller = ValueNotifier( const VideoPlayerValue( duration: Duration(seconds: 100), position: Duration(seconds: 10), isInitialized: true, ), ); final service = _nullThumbnailService(); addTearDown(controller.dispose); addTearDown(service.dispose); await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'preview-drama', title: 'Preview Drama', synopsis: 'synopsis', posterUrl: '', totalEpisodes: 8, ), episode: const Episode( id: 'preview-episode', number: 1, title: 'Episode 1', isFree: true, ), controller: controller, locked: false, liked: false, saved: false, visible: true, boosting: false, thumbnailService: service, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onMore: () {}, onScrubStart: () {}, onSeek: (_) {}, ), ), ), ), ); await tester.pumpAndSettle(); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); await gesture.moveBy(const Offset(60, 0)); await tester.pump(); expect(find.byKey(const Key('scrub-preview-card')), findsOneWidget); expect(find.byKey(const Key('scrub-preview-poster')), findsOneWidget); expect(find.textContaining('/ 01:40'), findsOneWidget); await gesture.up(); await tester.pump(); expect(find.byKey(const Key('scrub-preview-card')), findsNothing); }); testWidgets('should_seek_from_series_bar_when_overlay_is_cleared', ( tester, ) async { Duration? seeked; final controller = ValueNotifier( VideoPlayerValue( duration: const Duration(seconds: 100), position: const Duration(seconds: 10), isInitialized: true, ), ); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'clear-drama', title: 'Clear Drama', synopsis: 'synopsis', posterUrl: '', totalEpisodes: 8, ), episode: const Episode( id: 'clear-episode', number: 1, title: 'Episode 1', isFree: true, ), controller: controller, locked: false, liked: false, saved: false, visible: false, boosting: false, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onMore: () {}, onScrubStart: () {}, onSeek: (value) => seeked = value, ), ), ), ), ); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-progress')), findsOneWidget); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); await gesture.moveBy(const Offset(80, 0)); await gesture.up(); await tester.pumpAndSettle(); expect(seeked, isNotNull); expect(seeked!.inSeconds, greaterThan(0)); }); } PlaybackThumbnailService _nullThumbnailService() { return PlaybackThumbnailService( frameLoader: _NullThumbnailLoader(), sourceResolver: (_) => const PlaybackThumbnailSource(remoteUrl: 'https://example/video.mp4'), prefetchDelay: const Duration(hours: 1), ); } class _NullThumbnailLoader implements PlaybackThumbnailFrameLoader { @override Future load(PlaybackThumbnailFrameRequest request) async => null; @override void dispose() {} }