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/play_ticket.dart'; import 'package:fruitshorts/core/data/models/player_port.dart'; import 'package:fruitshorts/core/data/models/wallet.dart'; import 'package:fruitshorts/core/data/repositories/commerce_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/services/android_picture_in_picture.dart'; import 'package:fruitshorts/core/data/services/playback_session.dart'; import 'package:fruitshorts/core/data/services/wallet_controller.dart'; import 'package:fruitshorts/pages/player/player_logic.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import '../support/fake_watch_history.dart'; const _lockedDrama = Drama( id: 'locked', title: 'Locked', synopsis: 'synopsis', posterUrl: '/files/locked.png', episodes: [ Episode( id: 'locked-1', number: 1, title: 'Locked episode', isFree: false, locked: true, ), ], ); void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('should_not_request_ticket_for_guest_on_locked_episode', () async { final playback = FakePlaybackRepository(); final logic = _logic(playback, signedIn: false); logic.attach( const PlayerPage(drama: _lockedDrama, initialIndex: 0), isMounted: () => true, ); logic.start( factory: (_) => _NoopPlayer(), coordinator: PlaybackCoordinator(), ); await _drain(logic); expect(playback.requests, isEmpty); expect(logic.session.isLocked, isTrue); logic.dispose(); await _drain(logic); }); test('should_request_ticket_after_user_is_signed_in', () async { final playback = FakePlaybackRepository( ticket: const PlayTicket( episodeId: 'locked-1', accessible: false, accessType: PlayTicket.coinRequired, unlockCoin: 30, ), ); final logic = _logic(playback, signedIn: true); logic.attach( const PlayerPage(drama: _lockedDrama, initialIndex: 0), isMounted: () => true, ); logic.start( factory: (_) => _NoopPlayer(), coordinator: PlaybackCoordinator(), ); await _drain(logic); expect(playback.requests, ['locked-1']); expect(logic.blockedEpisodeId, 'locked-1'); logic.dispose(); await _drain(logic); }); test('should_resume_unlock_and_play_for_same_episode_after_login', () async { var signedIn = false; final playback = FakePlaybackRepository( ticket: const PlayTicket( episodeId: 'locked-1', accessible: true, accessType: 'ENTITLED', playUrl: '/files/locked-1.mp4', ), ); final logic = _logic( playback, signedIn: false, isSignedIn: () => signedIn, wallet: WalletController(_UnlockCommerceRepository()), ); logic.attach( const PlayerPage(drama: _lockedDrama, initialIndex: 0), isMounted: () => true, ); logic.start( factory: (_) => _NoopPlayer(), coordinator: PlaybackCoordinator(), ); await _drain(logic); expect(playback.requests, isEmpty); expect(logic.blockedEpisodeId, 'locked-1'); signedIn = true; final unlocked = await logic.unlockEpisode(0); await _drain(logic); expect(unlocked, isTrue); expect(playback.requests, ['locked-1']); expect(logic.session.active, isNotNull); logic.dispose(); await _drain(logic); }); } PlayerLogic _logic( PlaybackRepository playback, { bool signedIn = true, bool Function()? isSignedIn, WalletController? wallet, }) { return PlayerLogic( history: FakeWatchHistory(), playback: playback, wallet: wallet, pictureInPicture: AndroidPictureInPicture( channel: const MethodChannel('test/player_locked_auth_flow/pip'), ), waitForFrame: () async {}, isSignedIn: isSignedIn ?? () => signedIn, ); } Future _drain(PlayerLogic logic) async { for (var attempt = 0; attempt < 4; attempt++) { await pumpEventQueue(); await logic.session.playbackSettled; await logic.session.preloadSettled; } } class _NoopPlayer implements PlayerPort { @override Future initialize() async {} @override Future play() async {} @override Future pause() async {} @override Future dispose() async {} } class _UnlockCommerceRepository implements CommerceRepository { @override Future fetchWallet() async => const WalletSnapshot( wallet: CoinWallet( coinBalance: 100, totalRechargedCoin: 0, totalConsumedCoin: 0, totalRefundedCoin: 0, ), ); @override Future unlock(String episodeId) async => const UnlockResult( unlocked: true, accessType: 'ENTITLED', coinCost: 30, coinBalance: 70, ); }