// 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_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 favorites = FavoriteController( InMemoryFavoriteRepository(saved: {drama.id}, cards: [drama]), ); final logic = PlayerLogic( history: history, playback: playback, pictureInPicture: AndroidPictureInPicture( channel: const MethodChannel('test/player_open_detail/pip'), ), ); final factory = _RecordingFactory(); 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 playsBefore = player.plays; final tracked = VideoPlayerPort(drama.episodes.first); tracked.controller.value = const VideoPlayerValue( duration: Duration(minutes: 5), position: Duration(seconds: 37), isInitialized: true, ); logic ..trackedPlayer = tracked ..trackedEpisode = drama.episodes.first ..trackedDrama = drama; late BuildContext pageContext; await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: history), Provider.value(value: playback), 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(); }, ), ), ), ); unawaited(logic.openDetail(pageContext)); await tester.pump(); await tester.pump(const Duration(milliseconds: 400)); await _pumpSession(tester); expect(find.byType(DramaDetailPage), findsOneWidget); expect(logic.session.active, same(active)); expect(logic.session.currentIndex, index); expect(logic.session.userWantsPlay, isTrue); expect(player.pauses, 1); expect(player.plays, playsBefore); expect(tracked.controller.value.position, const Duration(seconds: 37)); expect(history.writes, isEmpty); 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(player.plays, playsBefore + 1); expect(tracked.controller.value.position, const Duration(seconds: 37)); expect(history.writes, ['signal-1|37000|false']); logic ..trackedPlayer = null ..trackedEpisode = null ..trackedDrama = null ..dispose(); await _pumpSession(tester); await tracked.dispose(); }, ); } 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 _RecordingFactory { final List<_RecordingPlayer> created = []; PlayerPort create(Episode episode) { final player = _RecordingPlayer(); created.add(player); return player; } } class _RecordingPlayer implements PlayerPort { int plays = 0; int pauses = 0; @override Future initialize() async {} @override Future play() async => plays++; @override Future pause() async => pauses++; @override Future dispose() async {} }