Files
reelbox/test/native_player_offline_lifecycle_test.dart
T

141 lines
4.4 KiB
Dart

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<Map<String, dynamic>>();
final opened = Completer<Uri>();
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<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 20)),
);
await tester.runAsync(
() => HttpOverrides.runWithHttpOverrides(() async {
await expectLater(fetch(uri), throwsA(isA<SocketException>()));
}, _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),
);
}