import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:reelbox/data/models.dart'; import 'package:reelbox/media/media_platform.dart'; import 'package:reelbox/media/player_page.dart'; import 'package:reelbox/state/app_state.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'cast_media_share_test.dart' show syntheticPackage, fetch; class _LocalNetwork extends HttpOverrides {} void main() { testWidgets( 'disposing an offline route keeps its real loopback lease until native exit', (tester) async { final root = await tester.runAsync( () => Directory.systemTemp.createTemp('native-offline-route-'), ); final package = await tester.runAsync(() => syntheticPackage(root!)); SharedPreferences.setMockInitialValues({}); final app = AppState(storage: await SharedPreferences.getInstance()); await app.initialize(discover: false); const title = TitleRecord(id: 98, title: 'Offline fixture'); const source = MediaSource( url: 'https://media.test/source.m3u8', type: 'hls', ); await app.recordWatch( title, sourceUrl: source.url, positionSeconds: 600, durationSeconds: 3600, ); final exit = Completer>(); final opened = Completer(); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( MediaPlatform.channel, (call) async { if (call.method != 'openNativePlayer') return null; opened.complete(Uri.parse((call.arguments as Map)['url'] as String)); return exit.future; }, ); addTearDown(() async { if (!exit.isCompleted) { exit.complete({ 'positionMs': 0, 'durationMs': 0, 'opened': false, 'completed': false, }); } await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 20)), ); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( MediaPlatform.channel, null, ); app.dispose(); await tester.runAsync(() => root!.delete(recursive: true)); }); await tester.runAsync( () => tester.pumpWidget( AppScope( state: app, child: MaterialApp( home: PlayerPage( title: title, source: source, localFile: package!.path, ), ), ), ), ); for (var attempt = 0; attempt < 100 && !opened.isCompleted; attempt++) { await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 10)), ); await tester.pump(); } expect( opened.isCompleted, true, reason: 'Offline preparation must dispatch a native open', ); final uri = await tester.runAsync( () => opened.future.timeout(const Duration(seconds: 2)), ); expect(uri!.host, '127.0.0.1'); final before = await tester.runAsync( () => HttpOverrides.runWithHttpOverrides( () => fetch(uri), _LocalNetwork(), ), ); expect(before!.status, 200); await tester.pumpWidget(const SizedBox()); final after = await tester.runAsync( () => HttpOverrides.runWithHttpOverrides( () => fetch(uri), _LocalNetwork(), ), ); expect( after!.status, 200, reason: 'The native activity is still reading this offline package', ); exit.complete({ 'positionMs': 1200000, 'durationMs': 3600000, 'opened': true, 'completed': false, }); await tester.pump(); await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 20)), ); await tester.runAsync( () => HttpOverrides.runWithHttpOverrides(() async { await expectLater(fetch(uri), throwsA(isA())); }, _LocalNetwork()), ); expect( app.history.single.positionSeconds, 600, reason: 'Disposed routes must not save stale native results', ); expect(tester.takeException(), isNull); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); }