import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:nontonki/core/data/models/drama.dart'; import 'package:nontonki/core/data/services/playback_session.dart'; import 'package:nontonki/core/data/models/player_port.dart'; void main() { final episodes = List.generate( 4, (i) => Episode( id: 'ep-${i + 1}', number: i + 1, title: 'Episode ${i + 1}', videoUrl: 'https://example.test/episode_0${i + 1}.mp4', isFree: i < 2, ), ); test('locked episode never creates or initializes a player', () async { final factory = FakeFactory(); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, ); await session.select(2); expect(session.isLocked, isTrue); expect(factory.created, isEmpty); await session.close(); }); test('rapid selection drops stale initialize and keeps one player', () async { final factory = FakeFactory(holdFirstInitialize: true); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, ); final first = session.select(0); await factory.firstCreated.future; final second = session.select(1); factory.created.first.releaseInitialize(); await Future.wait([first, second]); expect(factory.peakLive, lessThanOrEqualTo(1)); expect(factory.created.first.playCalls, 0); expect(factory.created.first.disposed, isTrue); expect(session.currentIndex, 1); expect(factory.created.last.playCalls, 1); await session.close(); expect(factory.live, 0); }); test( 'background and overlay pause without forgetting playback intent', () async { final factory = FakeFactory(); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, ); await session.select(0); final player = factory.created.single; expect(player.playCalls, 1); session.setAppResumed(false); await Future.delayed(Duration.zero); expect(player.pauseCalls, 1); session.setAppResumed(true); await Future.delayed(Duration.zero); expect(player.playCalls, 2); session.setOverlayOpen(true); await Future.delayed(Duration.zero); expect(player.pauseCalls, 2); session.setUserWantsPlay(false); session.setOverlayOpen(false); await Future.delayed(Duration.zero); expect(player.playCalls, 2); await session.close(); }, ); test('resume preparation finishes before video starts', () async { final factory = FakeFactory(); final prepared = Completer(); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, prepare: (_, _) => prepared.future, ); final selection = session.select(0); await factory.firstCreated.future; await Future.delayed(Duration.zero); expect(factory.created.single.playCalls, 0); prepared.complete(); await selection; expect(factory.created.single.playCalls, 1); await session.close(); }); test('pausing then swiping to the next episode resumes playback', () async { final factory = FakeFactory(); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, ); await session.select(0); session.setUserWantsPlay(false); await Future.delayed(Duration.zero); expect(session.userWantsPlay, isFalse); await session.select(1); expect(session.userWantsPlay, isTrue); await Future.delayed(Duration.zero); final playing = session.active; expect(playing, isA()); expect((playing! as FakePlayer).playCalls, greaterThan(0)); await session.close(); }); test('re-selecting the same episode keeps the user pause', () async { final factory = FakeFactory(); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, ); await session.select(0); session.setUserWantsPlay(false); await Future.delayed(Duration.zero); // 页面重新可见等场景会重复选中同一集,这时不能把暂停改成播放。 await session.select(0); expect(session.userWantsPlay, isFalse); expect(session.canAutoAdvance, isFalse); await session.close(); }); test('automatic advance respects visibility and playback intent', () async { final factory = FakeFactory(); final session = PlaybackSession( episodes: episodes, factory: factory.create, canPlay: (episode) => episode.isFree, ); await session.select(0); expect(session.canAutoAdvance, isTrue); session.setRouteVisible(false); expect(session.canAutoAdvance, isFalse); session.setRouteVisible(true); session.setOverlayOpen(true); expect(session.canAutoAdvance, isFalse); session.setOverlayOpen(false); session.setUserWantsPlay(false); expect(session.canAutoAdvance, isFalse); await session.close(); }); } class FakeFactory { FakeFactory({this.holdFirstInitialize = false}); final bool holdFirstInitialize; final List created = []; final Completer firstCreated = Completer(); int live = 0; int peakLive = 0; PlayerPort create(Episode episode) { live++; if (live > peakLive) peakLive = live; final player = FakePlayer( holdInitialize: holdFirstInitialize && created.isEmpty, onDispose: () => live--, ); created.add(player); if (!firstCreated.isCompleted) firstCreated.complete(); return player; } } class FakePlayer implements PlayerPort { FakePlayer({required this.holdInitialize, required this.onDispose}); final bool holdInitialize; final void Function() onDispose; final Completer _initializeGate = Completer(); bool initialized = false; bool disposed = false; int playCalls = 0; int pauseCalls = 0; void releaseInitialize() => _initializeGate.complete(); @override Future initialize() async { if (holdInitialize) await _initializeGate.future; initialized = true; } @override Future play() async => playCalls++; @override Future pause() async => pauseCalls++; @override Future dispose() async { if (disposed) return; disposed = true; onDispose(); } }