import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:reelbox/media/native_player.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); const channel = MethodChannel('org.reelbox/media'); setUp(() => debugDefaultTargetPlatformOverride = TargetPlatform.android); tearDown(() { debugDefaultTargetPlatformOverride = null; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, null); }); test('awaits exit and sends exact URL, title and checkpoint', () async { final exit = Completer>(); MethodCall? received; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (call) { received = call; return exit.future; }); var returned = false; const url = 'https://media.example.org/a%2Fb.m3u8?sig=a%2Bz&x=1'; final pending = NativePlayerBridge.open( url: url, title: 'عنوان — episode', position: const Duration(milliseconds: 12345), autoplay: false, externalPackage: 'vlc', orientation: 'landscape', videoFit: 'cover', speed: 1.25, subtitleEnabled: false, ).then((result) { returned = true; return result; }); await Future.delayed(Duration.zero); expect(returned, isFalse); expect(received!.method, 'openNativePlayer'); expect(received!.arguments, containsPair('url', url)); expect(received!.arguments, containsPair('positionMs', 12345)); expect(received!.arguments, containsPair('autoplay', false)); expect(received!.arguments, containsPair('externalPackage', 'vlc')); expect(received!.arguments, containsPair('subtitleEnabled', false)); expect(received!.arguments, containsPair('orientation', 'landscape')); expect(received!.arguments, containsPair('videoFit', 'cover')); expect(received!.arguments, containsPair('speed', 1.25)); exit.complete({ 'positionMs': 42000, 'durationMs': 90000, 'opened': true, 'completed': false, }); final result = await pending; expect(result.position.inMilliseconds, 42000); expect(result.duration.inMilliseconds, 90000); expect(result.opened, isTrue); expect(result.completed, isFalse); }); test( 'local and loopback sources and optional subtitles are unchanged', () async { final calls = []; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (call) async { calls.add(call); return { 'positionMs': 0, 'durationMs': 0, 'opened': false, 'completed': false, }; }); for (final url in [ '/data/user/0/org.reelbox.app/files/فیلم.mkv', 'http://127.0.0.1:49123/media/lease/index.m3u8?token=a%2Bz', ]) { final result = await NativePlayerBridge.open( url: url, title: 'Film', subtitleUrl: 'https://media.example.org/sub.vtt?token=a%2Fz', hardwareAcceleration: false, pipEnabled: false, ); expect(calls.last.arguments, containsPair('url', url)); expect( calls.last.arguments, containsPair( 'subtitleUrl', 'https://media.example.org/sub.vtt?token=a%2Fz', ), ); expect( calls.last.arguments, containsPair('hardwareAcceleration', false), ); expect(calls.last.arguments, containsPair('pipEnabled', false)); expect(result.opened, isFalse); } expect(calls, hasLength(2)); }, ); test( 'rejects malformed exit maps instead of manufacturing progress', () async { for (final response in [ null, { 'positionMs': -1, 'durationMs': 10, 'opened': true, 'completed': false, }, { 'positionMs': double.nan, 'durationMs': 10, 'opened': true, 'completed': false, }, { 'positionMs': 0x7fffffffffffffff, 'durationMs': 0, 'opened': true, 'completed': false, }, { 'positionMs': 1.5, 'durationMs': 10, 'opened': true, 'completed': false, }, { 'positionMs': 10, 'durationMs': -1, 'opened': true, 'completed': false, }, { 'positionMs': 10, 'durationMs': 20, 'opened': 'true', 'completed': true, }, ]) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (_) async => response); await expectLater( NativePlayerBridge.open( url: 'https://media.example.org/video.mp4', title: 'Film', ), throwsFormatException, ); } final bounded = NativePlaybackResult.fromMap({ 'positionMs': 1000, 'durationMs': 100, 'opened': false, 'completed': true, }); expect(bounded.position.inMilliseconds, 100); expect(bounded.completed, isFalse); }, ); test('completion requires a credible near-end checkpoint, not just EOF', () { for (final sample in >[ {'positionMs': 180000, 'durationMs': 3600000}, {'positionMs': 180000, 'durationMs': 0}, {'positionMs': 0, 'durationMs': 3600000}, {'positionMs': 3600001, 'durationMs': 3600000}, {'positionMs': 3597000, 'durationMs': 3600000}, {'positionMs': 3600000, 'durationMs': 3600000, 'opened': false}, {'positionMs': 3600000, 'durationMs': 3600000, 'completed': false}, ]) { final result = NativePlaybackResult.fromMap({ 'opened': true, 'completed': true, ...sample, }); expect(result.completed, isFalse, reason: sample.toString()); } for (final position in [3599500, 3600000]) { final result = NativePlaybackResult.fromMap({ 'positionMs': position, 'durationMs': 3600000, 'opened': true, 'completed': true, }); expect(result.completed, isTrue); } }); test('platform failures and busy results propagate without retry', () async { var calls = 0; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (_) async { calls++; throw PlatformException(code: 'NATIVE_PLAYER_BUSY'); }); await expectLater( NativePlayerBridge.open( url: 'https://media.example.org/video.mp4', title: 'Film', ), throwsA(isA()), ); expect(calls, 1); }); test('unsupported platforms do not call Android', () async { debugDefaultTargetPlatformOverride = TargetPlatform.linux; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( channel, (_) async => fail('Android was invoked'), ); await expectLater( NativePlayerBridge.open( url: 'https://media.example.org/video.mp4', title: 'Film', ), throwsUnsupportedError, ); }); test('invalid launch preferences fail before starting an activity', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (_) async { fail('Invalid preferences reached Android'); }); await expectLater( NativePlayerBridge.open( url: 'https://media.example.org/video.mp4', title: 'Film', speed: double.nan, ), throwsArgumentError, ); await expectLater( NativePlayerBridge.open( url: 'https://media.example.org/video.mp4', title: 'Film', orientation: 'invalid', ), throwsArgumentError, ); await expectLater( NativePlayerBridge.open( url: 'https://media.example.org/video.mp4', title: 'Film', videoFit: 'invalid', ), throwsArgumentError, ); await expectLater( NativePlayerBridge.open(url: '', title: 'Film'), throwsArgumentError, ); }); }