// Author: fruit.com.cn 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/drama.dart'; import 'package:fruitshorts/core/data/models/play_ticket.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_colors.dart'; import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart'; import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.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() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets('should_open_fullscreen_overlay_from_discover_card', ( tester, ) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('home-drama-card-city-lights-demo'))); await tester.pumpAndSettle(); _expectFullscreenOverlay(tester); expect(find.byType(PageView), findsOneWidget); await tester.tap(find.byKey(const Key('player-more'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget); expect(find.byKey(const Key('player-more-offline')), findsOneWidget); await tester.tap(find.byKey(const Key('player-more-close'))); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('should_show_center_toggle_only_when_paused', (tester) async { await _pumpApp(tester); await _openSeriesPlayer(tester); expect(find.byKey(const Key('player-play-toggle')), findsNothing); await tester.tapAt(const Offset(80, 300)); await tester.pump(); expect(find.byKey(const Key('player-play-toggle')), findsOneWidget); expect( tester .widget( find.byKey(const Key('player-play-toggle')), ) .playing, isFalse, ); expect( find.descendant( of: find.byKey(const Key('player-play-toggle')), matching: find.byIcon(Icons.play_arrow_rounded), ), findsOneWidget, ); await tester.tap(find.byKey(const Key('player-play-toggle'))); await tester.pump(); expect(find.byKey(const Key('player-play-toggle')), findsNothing); }); testWidgets('should_keep_controls_visible_until_clear_screen_is_tapped', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); expect(find.byKey(const Key('player-controls-top')), findsOneWidget); expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); expect(find.byType(PlayerCenterToggle), findsNothing); await tester.tapAt(const Offset(80, 300)); await tester.pump(); expect(find.byType(PlayerCenterToggle), findsOneWidget); expect( tester .widget( find.byKey(const Key('player-play-toggle')), ) .playing, isFalse, ); await tester.pump(const Duration(seconds: 5)); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsOneWidget); expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); expect(find.byType(PlayerCenterToggle), findsOneWidget); await _clearFromMore(tester); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); expect(find.byKey(const Key('player-controls-bottom')), findsNothing); expect(find.byType(PlayerCenterToggle), findsOneWidget); expect( tester .widget( find.byKey(const Key('player-play-toggle')), ) .playing, isFalse, ); expect(find.byKey(const Key('player-video-tap')), findsOneWidget); expect(find.byKey(const Key('player-exit-clear-screen')), findsOneWidget); expect( tester.getSize(find.byKey(const Key('player-exit-clear-screen'))), const Size(36, 36), ); await tester.tapAt(const Offset(80, 300)); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); expect(find.byKey(const Key('player-exit-clear-screen')), findsOneWidget); expect(find.byType(PlayerCenterToggle), findsNothing); await tester.tapAt(const Offset(80, 300)); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); expect(find.byKey(const Key('player-exit-clear-screen')), findsOneWidget); expect( tester .widget( find.byKey(const Key('player-play-toggle')), ) .playing, isFalse, ); await tester.tap(find.byKey(const Key('player-exit-clear-screen'))); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsOneWidget); expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); expect(find.byType(PlayerCenterToggle), findsOneWidget); expect( tester .widget( find.byKey(const Key('player-play-toggle')), ) .playing, isFalse, ); expect(find.byKey(const Key('player-exit-clear-screen')), findsNothing); }); testWidgets('should_fit_player_header_on_320_width', (tester) async { await _pumpApp(tester, size: const Size(320, 568)); await _openSeriesPlayer(tester); expect( tester.getSize(find.byKey(const Key('player-back'))), const Size(36, 36), ); expect(tester.takeException(), isNull); }); testWidgets('should_keep_compact_bottom_panel_and_lower_rail', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); final screenHeight = tester.view.physicalSize.height; final bottom = tester.getRect( find.byKey(const Key('player-controls-bottom')), ); final rail = tester.getRect( find.byKey(const Key('player-interaction-rail')), ); expect( tester .widget(find.byKey(const Key('player-background'))) .color, AppColors.ink, ); final panel = tester.widget( find.byKey(const Key('player-controls-bottom')), ); expect((panel.decoration! as BoxDecoration).color, AppColors.card); expect(bottom.height, lessThanOrEqualTo(200)); expect(bottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); expect(rail.top, greaterThanOrEqualTo(screenHeight * 0.55 - 2)); expect(rail.bottom, lessThanOrEqualTo(bottom.top)); expect(find.byKey(const Key('player-author-note')), findsNothing); expect( tester .getSize( find.descendant( of: find.byKey(const Key('player-rail-comments')), matching: find.byIcon(Icons.chat_bubble_outline_rounded), ), ) .width, 24, ); await tester.tap(find.byKey(const Key('player-synopsis-toggle'))); await tester.pumpAndSettle(); final expandedBottom = tester.getRect( find.byKey(const Key('player-controls-bottom')), ); expect(expandedBottom.height, lessThanOrEqualTo(200)); expect(expandedBottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); }); testWidgets('should_keep_series_clear_state_when_swiping_next_episode', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); await _clearFromMore(tester); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-controls-top')), findsNothing); expect(find.byKey(const Key('player-controls-bottom')), findsNothing); expect(find.byKey(const Key('player-exit-clear-screen')), findsOneWidget); await tester.tap(find.byKey(const Key('player-exit-clear-screen'))); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsOneWidget); expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); expect(find.byKey(const Key('player-exit-clear-screen')), findsNothing); }); testWidgets('should_open_speed_sheet_and_apply_selected_speed', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); await tester.tap(find.byKey(const Key('player-speed-entry'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-speed-sheet')), findsOneWidget); for (final value in ['0.75', '1.0', '1.25', '1.5', '2.0', '3.0']) { expect(find.byKey(Key('player-speed-$value')), findsOneWidget); } await tester.tap(find.byKey(const Key('player-speed-1.5'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-speed-sheet')), findsNothing); await tester.tap(find.byKey(const Key('player-speed-entry'))); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('player-speed-entry')), matching: find.text('1.5x'), ), findsOneWidget, ); }); testWidgets('should_hide_speed_entry_in_clear_screen_and_keep_speed', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); await tester.tap(find.byKey(const Key('player-speed-entry'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('player-speed-1.5'))); await tester.pumpAndSettle(); await _clearFromMore(tester); await tester.pump(); expect(find.byKey(const Key('player-more')), findsNothing); await tester.tap(find.byKey(const Key('player-exit-clear-screen'))); await tester.pump(); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('player-speed-entry')), matching: find.text('1.5x'), ), findsOneWidget, ); }); testWidgets('should_enable_drag_time_preview_on_player_progress', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); final bar = tester.widget( find.byKey(const Key('player-progress')), ); expect(bar.showTimes, isTrue); }); testWidgets('should_render_k28_player_structure_and_episode_grid', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); _expectFullscreenOverlay(tester); await tester.tap(find.byKey(const Key('player-episodes'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-episodes-sheet')), findsOneWidget); expect(find.byKey(const Key('player-episodes-close')), findsOneWidget); final grid = tester.widget( find.descendant( of: find.byKey(const Key('player-episodes-sheet')), matching: find.byType(GridView), ), ); final delegate = grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; expect(delegate.crossAxisCount, 6); expect(tester.takeException(), isNull); }); testWidgets('should_show_locked_episode_in_episode_picker', (tester) async { await _pumpApp( tester, catalog: FakeCatalogRepository(dramas: const [_lockedDrama]), ); await _openSeriesPlayer( tester, cardKey: const Key('home-drama-card-locked-demo'), ); await tester.tap(find.byKey(const Key('player-episodes'))); await tester.pumpAndSettle(); expect( find.byKey(const ValueKey('episode-lock-locked-demo-1')), findsOneWidget, ); expect(find.byKey(const Key('player-episode-2')), findsOneWidget); }); testWidgets('should_open_more_actions_and_change_text_size', (tester) async { await _pumpApp(tester); await _openSeriesPlayer(tester); await tester.tap(find.byKey(const Key('player-more'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-more-sheet')), findsOneWidget); expect(find.byKey(const Key('player-more-speed')), findsOneWidget); expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget); expect(find.byKey(const Key('player-more-offline')), findsOneWidget); expect(find.byKey(const Key('player-more-clear')), findsOneWidget); expect(find.byKey(const Key('player-more-danmaku')), findsOneWidget); expect( find.byKey(const Key('player-more-danmaku-settings')), findsOneWidget, ); expect(find.byKey(const Key('player-more-cast')), findsOneWidget); expect(find.byKey(const Key('player-more-timer')), findsOneWidget); expect(find.byKey(const Key('player-more-text-size')), findsOneWidget); expect(find.byKey(const Key('player-more-report')), findsOneWidget); await tester.tap(find.byTooltip('Text size')); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-text-size-large')), findsOneWidget); await tester.tap(find.byKey(const Key('player-text-size-large'))); await tester.pumpAndSettle(); expect(find.text('Large'), findsOneWidget); }); testWidgets('should_disable_quality_not_supplied_by_ticket', (tester) async { await _pumpApp( tester, playback: FakePlaybackRepository( ticket: const PlayTicket( episodeId: 'city-lights-demo-1', accessible: true, accessType: PlayTicket.free, playUrl: '/files/city-lights-demo-1.mp4', qualities: ['AUTO'], ), ), ); await _openSeriesPlayer(tester); await tester.tap(find.byKey(const Key('player-more'))); await tester.pumpAndSettle(); expect( tester .widget(find.byKey(const Key('player-more-quality-AUTO'))) .onSelected, isNotNull, ); expect( tester .widget(find.byKey(const Key('player-more-quality-720P'))) .onSelected, isNull, ); expect(find.text('720P ยท Unavailable'), findsOneWidget); }); testWidgets('should_enter_clear_screen_from_more_panel', (tester) async { await _pumpApp(tester); await _openSeriesPlayer(tester); await tester.tap(find.byKey(const Key('player-more'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('player-more-clear'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-controls-top')), findsNothing); expect(find.byKey(const Key('player-controls-bottom')), findsNothing); expect(find.byKey(const Key('player-exit-clear-screen')), findsOneWidget); }); testWidgets('should_open_unlock_panel_with_honest_method_states', ( tester, ) async { await _pumpApp( tester, playback: FakePlaybackRepository( ticket: const PlayTicket( episodeId: 'city-lights-demo-1', accessible: false, accessType: PlayTicket.coinRequired, unlockCoin: 30, ), ), ); await _openSeriesPlayer(tester); expect(find.byKey(const Key('player-unlock-notice')), findsOneWidget); await tester.tap(find.byKey(const Key('player-open-unlock'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('player-unlock-panel')), findsOneWidget); expect(find.byKey(const Key('player-unlock-coins')), findsOneWidget); expect(find.byKey(const Key('player-unlock-ad')), findsOneWidget); expect(find.byKey(const Key('player-unlock-vip')), findsOneWidget); expect( find.text('Available unlock methods vary by title.'), findsOneWidget, ); }); } void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-back')), findsOneWidget); expect(find.byKey(const Key('player-episode-heading')), findsOneWidget); expect(find.byKey(const Key('player-speed-entry')), findsOneWidget); expect(find.byKey(const Key('player-drama-title')), findsOneWidget); expect(find.byKey(const Key('player-episode-label')), findsOneWidget); expect(find.byKey(const Key('player-synopsis')), findsOneWidget); expect(find.byKey(const Key('player-more')), findsOneWidget); for (final key in const [Key('player-back'), Key('player-more')]) { expect(tester.getSize(find.byKey(key)), const Size(36, 36)); } expect(find.byKey(const Key('player-play-toggle')), findsNothing); expect(find.byKey(const Key('player-episodes')), findsOneWidget); expect(find.byKey(const Key('player-rail-comments')), findsOneWidget); expect(find.byKey(const Key('player-fullscreen-toggle')), findsNothing); expect(find.byType(Slider), findsOneWidget); expect(find.byKey(const Key('player-title')), findsNothing); expect(find.byKey(const Key('player-subtitle')), findsNothing); expect(find.byKey(const Key('player-genre-chip')), findsNothing); expect(find.byKey(const Key('player-page-dots')), findsNothing); expect(find.byKey(const Key('player-clear-screen')), findsNothing); expect(find.byKey(const Key('player-next')), findsNothing); expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget); expect(find.byKey(const Key('player-rail-like')), findsOneWidget); expect(find.byKey(const Key('player-rail-save')), findsOneWidget); expect(find.byKey(const Key('player-rail-share')), findsOneWidget); } Future _clearFromMore(WidgetTester tester) async { await tester.tap(find.byKey(const Key('player-more'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('player-more-clear'))); await tester.pumpAndSettle(); } Future _openSeriesPlayer( WidgetTester tester, { Key cardKey = const Key('home-drama-card-city-lights-demo'), }) async { await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); final card = find.byKey(cardKey); await tester.scrollUntilVisible( card, 220, scrollable: find .descendant( of: find.byKey(const Key('discover-refresh')), matching: find.byType(Scrollable), ) .first, ); await tester.ensureVisible(card); await tester.pumpAndSettle(); await tester.tap(card); await tester.pumpAndSettle(); } Future _pumpApp( WidgetTester tester, { Size size = const Size(390, 844), FakeCatalogRepository? catalog, FakePlaybackRepository? playback, }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final repository = catalog ?? FakeCatalogRepository(); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: repository, categoryRepository: FakeCategoryRepository(catalog: repository), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: repository.dramas), playbackRepository: playback ?? FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); } const _lockedDrama = Drama( id: 'locked-demo', title: 'Locked Demo', synopsis: 'Locked demo synopsis', posterUrl: '/files/locked-demo.png', genre: 'Suspense', detailLoaded: true, episodes: [ Episode( id: 'locked-demo-1', number: 1, title: 'Locked Episode', isFree: false, locked: true, videoUrl: 'https://example.test/locked-demo-1.mp4', ), Episode( id: 'locked-demo-2', number: 2, title: 'Open Episode', isFree: true, videoUrl: 'https://example.test/locked-demo-2.mp4', ), ], );