// Author: fruit.com.cn import 'dart:async'; import 'dart:typed_data'; 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')), findsOneWidget); expect(find.byKey(const Key('home-grid-filter')), findsOneWidget); expect( tester.getSize(find.byKey(const Key('home-grid-filter'))), const Size(AppDimens.tapTarget, AppDimens.homeSecondaryFilterVisualSize), ); expect( tester.getSize(find.byKey(const Key('home-secondary-filter-visual'))), const Size.square(AppDimens.homeSecondaryFilterVisualSize), ); expect( tester.getCenter(find.byKey(const Key('home-grid-filter'))).dy, closeTo(tester.getCenter(find.byKey(const Key('home-tag-hot'))).dy, 0.1), ); final filterIcon = tester.widget( find.byKey(const Key('home-secondary-filter-icon')), ); expect( filterIcon.size, const Size.square(AppDimens.homeSecondaryFilterIcon), ); final filterMaterial = tester.widget( find .ancestor( of: find.byKey(const Key('home-grid-filter')), matching: find.byType(Material), ) .first, ); expect(filterMaterial.color, AppColors.transparent); 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 secondaryTag = tester.widget( find.descendant( of: find.byKey(const Key('home-tag-hot')), matching: find.text('Hot'), ), ); expect(secondaryTag.style?.fontSize, AppDimens.homeSecondaryTagFont); expect(secondaryTag.style?.fontWeight, FontWeight.w600); expect(secondaryTag.style?.color, AppColors.textTertiary); final unselectedTagChip = tester.widget( find.byKey(const Key('home-tag-animated-hot')), ); expect( (unselectedTagChip.decoration! as BoxDecoration).color, AppColors.transparent, ); 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)); final tagStart = tester.getTopLeft(find.byKey(const Key('home-tag-hot'))); expect(tagStart.dx, pageStart.dx); expect( tester.getSize(find.byKey(const Key('home-tag-hot'))).height, AppDimens.homeSecondaryTagHeight, ); final tagGap = tester.getTopLeft(find.byKey(const Key('home-tag-uplifting'))).dx - tester.getBottomRight(find.byKey(const Key('home-tag-hot'))).dx; expect(tagGap, closeTo(AppDimens.homeSecondaryTagGap, 0.1)); await tester.tap(find.byKey(const Key('home-tag-hot'))); await tester.pump(); final selectedTagChip = tester.widget( find.byKey(const Key('home-tag-animated-hot')), ); expect( (selectedTagChip.decoration! as BoxDecoration).color, AppColors.contentSurface, ); expect( (selectedTagChip.decoration! as BoxDecoration).borderRadius, BorderRadius.circular(AppDimens.homeSecondaryTagRadius), ); expect( tester.getSize(find.byKey(const Key('home-tag-hot'))).height, AppDimens.homeSecondaryTagHeight, ); final selectedTag = tester.widget( find.descendant( of: find.byKey(const Key('home-tag-hot')), matching: find.text('Hot'), ), ); expect(selectedTag.style?.color, AppColors.navSelected); expect(selectedTag.style?.fontWeight, FontWeight.w600); 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, isTrue, ); }); testWidgets('should_render_new_feed_structure_with_formatted_counts', ( tester, ) async { const drama = Drama( id: 'feed-drama', title: 'Feed Drama', synopsis: 'synopsis', posterUrl: '', genre: 'Romance', category: 'Urban', totalEpisodes: 8, freeEpisodes: 8, viewCount: 419000, likeCount: 1006, favoriteCount: 32000, commentCount: 12, ); 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: 'Ignored Episode Title', 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: () {}, onMore: () {}, ), ), ), ), ); await tester.pumpAndSettle(); expect( tester.widget(find.byKey(const Key('home-feed-title'))).data, 'Feed Drama', ); 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.text('Episode 2'), findsOneWidget); expect(find.textContaining('41.9万'), findsOneWidget); 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( tester.widget(find.byKey(const Key('rail-comments-count'))).data, '12', ); String railAsset(Key key) { final image = tester.widget( find.descendant(of: find.byKey(key), matching: find.byType(Image)), ); return (image.image as AssetImage).assetName; } expect(railAsset(const Key('rail-save')), 'assets/icon/rail/rail_save.png'); expect( railAsset(const Key('rail-comments')), 'assets/icon/rail/rail_comment.png', ); expect(railAsset(const Key('rail-like')), 'assets/icon/rail/rail_like.png'); expect( railAsset(const Key('rail-share')), 'assets/icon/rail/rail_share.png', ); expect( tester .widget( find.descendant( of: find.byKey(const Key('rail-share')), matching: find.byType(Image), ), ) .colorBlendMode, BlendMode.srcIn, ); 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); }); 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 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: () {}, onSeek: (value) => seeked = value, ), ), ), ), ); await tester.pumpAndSettle(); final bar = tester.getRect(find.byKey(const Key('home-feed-full-series'))); expect(bar.height, 44); expect( tester .widget(find.byKey(const Key('home-feed-full-series-bg'))) .color, const Color(0x4D000000), ); expect( tester.getRect(find.textContaining('Watch full series')).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(24), ); 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( 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-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(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() {} }