// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/player_port.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/android_picture_in_picture.dart'; import 'package:fruitshorts/core/data/services/catalog_controller.dart'; import 'package:fruitshorts/core/data/services/favorite_controller.dart'; import 'package:fruitshorts/core/data/services/playback_position_cache.dart'; import 'package:fruitshorts/core/data/services/playback_session.dart'; import 'package:fruitshorts/core/data/services/video_player_port.dart'; import 'package:fruitshorts/core/platform/haptics.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart'; import 'package:fruitshorts/pages/player/player_logic.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; import '../support/fake_catalog.dart'; import '../support/fake_haptics.dart'; import '../support/fake_watch_history.dart'; void main() { testWidgets( 'should_keep_active_player_and_progress_when_detail_route_is_open', (tester) async { final drama = _drama(); final history = FakeWatchHistory(); final playback = FakePlaybackRepository(); final catalog = CatalogController(FakeCatalogRepository(dramas: [drama])); final positionCache = PlaybackPositionCache(); final favorites = FavoriteController( InMemoryFavoriteRepository(saved: {drama.id}, cards: [drama]), ); final logic = PlayerLogic( history: history, playback: playback, positionCache: positionCache, pictureInPicture: AndroidPictureInPicture( channel: const MethodChannel('test/player_open_detail/pip'), ), ); final factory = _VideoRecordingFactory(); logic.attach( PlayerPage(drama: drama, initialIndex: 0), isMounted: () => true, ); logic.start(factory: factory.create, coordinator: PlaybackCoordinator()); await _pumpSession(tester); final active = logic.session.active; final index = logic.session.currentIndex; final player = factory.created.first; final createdBeforeDetail = factory.created.length; logic ..trackedPlayer = player ..trackedEpisode = drama.episodes.first ..trackedDrama = drama; late BuildContext pageContext; await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: history), Provider.value(value: playback), Provider.value(value: positionCache), ChangeNotifierProvider.value(value: catalog), ChangeNotifierProvider.value(value: favorites), Provider.value(value: FakeHaptics()), ], child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Builder( builder: (context) { pageContext = context; return const SizedBox.shrink(); }, ), ), ), ); logic.beginScrub(player); await logic.seekTo( pageContext, player, const Duration(minutes: 1, seconds: 22), ); unawaited(logic.openDetail(pageContext)); await tester.pump(); await tester.pump(const Duration(milliseconds: 400)); await _pumpSession(tester); await tester.runAsync(() => logic.session.playbackSettled); expect(find.byType(DramaDetailPage), findsOneWidget); expect(logic.session.active, same(active)); expect(logic.session.currentIndex, index); expect(logic.session.userWantsPlay, isTrue); expect(factory.created, hasLength(createdBeforeDetail)); expect( player.controller.value.position, const Duration(minutes: 1, seconds: 22), ); expect(history.writes, ['signal-1|82000|false', 'signal-1|82000|false']); expect( positionCache.read('signal-1', dramaId: 'signal')?.position, const Duration(minutes: 1, seconds: 22), ); await tester.pageBack(); await tester.pump(); await tester.pump(const Duration(milliseconds: 400)); await _pumpSession(tester); await logic.pendingWrite; expect(logic.session.active, same(active)); expect(logic.session.currentIndex, index); expect(logic.session.userWantsPlay, isTrue); expect(factory.created, hasLength(createdBeforeDetail)); expect( player.controller.value.position, const Duration(minutes: 1, seconds: 22), ); expect(history.writes, [ 'signal-1|82000|false', 'signal-1|82000|false', 'signal-1|82000|false', ]); logic ..trackedPlayer = null ..trackedEpisode = null ..trackedDrama = null ..dispose(); await _pumpSession(tester); }, ); } Drama _drama() => Drama( id: 'signal', title: 'Signal', synopsis: 'synopsis', posterUrl: '/files/signal.png', episodes: const [ Episode( id: 'signal-1', number: 1, title: 'Episode 1', videoUrl: 'https://example.test/signal-1.mp4', isFree: true, ), ], ); Future _pumpSession(WidgetTester tester) async { for (var attempt = 0; attempt < 5; attempt++) { await tester.pump(); await tester.pump(const Duration(milliseconds: 1)); } } class _VideoRecordingFactory { final List<_VideoRecordingPlayer> created = []; PlayerPort create(Episode episode) { final player = _VideoRecordingPlayer(episode); created.add(player); return player; } } class _VideoRecordingPlayer extends VideoPlayerPort { _VideoRecordingPlayer(super.episode); @override Future initialize() async { controller.value = const VideoPlayerValue( duration: Duration(minutes: 5), size: Size(1280, 720), position: Duration(seconds: 1), isInitialized: true, ); } @override Future play() async { _setPlaying(true); } @override Future pause() async { _setPlaying(false); } @override Future seekTo(Duration position) async { final current = controller.value; controller.value = VideoPlayerValue( duration: current.duration, size: current.size, position: position, isInitialized: current.isInitialized, isPlaying: current.isPlaying, ); } void _setPlaying(bool playing) { final current = controller.value; controller.value = VideoPlayerValue( duration: current.duration, size: current.size, position: current.position, isInitialized: current.isInitialized, isPlaying: playing, ); } @override Future dispose() async { await controller.dispose(); } }