[verified] Publish ReelBox source under GPLv3
This commit is contained in:
commit
db2b505ca3
445 files changed
+105274
No files matched your search
@@ -0,0 +1,315 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
|
||||
const title = TitleRecord(id: 78, title: 'Film');
|
||||
const source = MediaSource(
|
||||
url: 'https://media.test/film.mkv?token=unchanged',
|
||||
type: 'mkv',
|
||||
);
|
||||
Map<String, dynamic> exitAt(
|
||||
int position, {
|
||||
bool opened = true,
|
||||
bool completed = false,
|
||||
}) => {
|
||||
'positionMs': position,
|
||||
'durationMs': 3600000,
|
||||
'opened': opened,
|
||||
'completed': completed,
|
||||
};
|
||||
Future<AppState> mount(
|
||||
WidgetTester tester, {
|
||||
EpisodeRecord? episode,
|
||||
List<EpisodeRecord> playlist = const [],
|
||||
List<MediaSource> alternatives = const [],
|
||||
String? localFile,
|
||||
}) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final state = AppState(storage: await SharedPreferences.getInstance());
|
||||
await state.initialize(discover: false);
|
||||
await state.updateSettings(state.prefs.copyWith(autoplay: true));
|
||||
await state.recordWatch(
|
||||
title,
|
||||
sourceUrl: source.url,
|
||||
positionSeconds: 600,
|
||||
durationSeconds: 3600,
|
||||
episodeId: episode?.id,
|
||||
episodeTitle: episode?.title ?? '',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
AppScope(
|
||||
state: state,
|
||||
child: MaterialApp(
|
||||
home: PlayerPage(
|
||||
title: title,
|
||||
source: source,
|
||||
episode: episode,
|
||||
playlist: playlist,
|
||||
alternatives: alternatives,
|
||||
localFile: localFile,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
return state;
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'early native EOF never advances or records a completed episode',
|
||||
(tester) async {
|
||||
const first = EpisodeRecord(
|
||||
id: 101,
|
||||
title: 'Episode 1',
|
||||
sources: [source],
|
||||
);
|
||||
const second = EpisodeRecord(
|
||||
id: 102,
|
||||
title: 'Episode 2',
|
||||
sources: [MediaSource(url: 'https://media.test/episode2.mkv')],
|
||||
);
|
||||
final calls = <MethodCall>[];
|
||||
final firstExit = Completer<Map<String, dynamic>>();
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
(call) async {
|
||||
if (call.method != 'openNativePlayer') return null;
|
||||
calls.add(call);
|
||||
return calls.length == 1
|
||||
? firstExit.future
|
||||
: exitAt(0, opened: false);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
null,
|
||||
),
|
||||
);
|
||||
final state = await mount(
|
||||
tester,
|
||||
episode: first,
|
||||
playlist: [first, second],
|
||||
);
|
||||
addTearDown(state.dispose);
|
||||
firstExit.complete(exitAt(180000, completed: true));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
calls,
|
||||
hasLength(1),
|
||||
reason: 'Three minutes is not the end of this episode',
|
||||
);
|
||||
expect(state.history, hasLength(1));
|
||||
expect(state.history.single.episodeId, first.id);
|
||||
expect(state.history.single.positionSeconds, 180);
|
||||
expect(state.history.single.durationSeconds, 3600);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'auto-next recovery keeps alternatives and history on the current episode',
|
||||
(tester) async {
|
||||
const oldAlternative = MediaSource(
|
||||
url: 'https://media.test/episode1-720.mkv',
|
||||
quality: '720p',
|
||||
);
|
||||
const first = EpisodeRecord(
|
||||
id: 101,
|
||||
title: 'Episode 1',
|
||||
sources: [source, oldAlternative],
|
||||
);
|
||||
const secondSource = MediaSource(url: 'https://media.test/episode2.mkv');
|
||||
const secondAlternative = MediaSource(
|
||||
url: 'https://media.test/episode2-720.mkv?token=unchanged',
|
||||
quality: '720p',
|
||||
);
|
||||
const trailer = MediaSource(
|
||||
url: 'https://media.test/episode2-trailer.mp4',
|
||||
quality: 'Trailer',
|
||||
);
|
||||
const denied = MediaSource(
|
||||
url: 'https://t.me/episode2',
|
||||
quality: 'Denied',
|
||||
);
|
||||
const second = EpisodeRecord(
|
||||
id: 102,
|
||||
title: 'Episode 2',
|
||||
sources: [secondSource, secondAlternative, trailer, denied],
|
||||
);
|
||||
final calls = <MethodCall>[];
|
||||
final firstExit = Completer<Map<String, dynamic>>();
|
||||
final recoveryExit = Completer<Map<String, dynamic>>();
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
(call) async {
|
||||
if (call.method != 'openNativePlayer') return null;
|
||||
calls.add(call);
|
||||
if (calls.length == 1) return firstExit.future;
|
||||
if (calls.length == 2) return exitAt(0, opened: false);
|
||||
return recoveryExit.future;
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
null,
|
||||
),
|
||||
);
|
||||
final state = await mount(
|
||||
tester,
|
||||
episode: first,
|
||||
playlist: [first, second],
|
||||
alternatives: first.sources,
|
||||
);
|
||||
addTearDown(state.dispose);
|
||||
firstExit.complete(exitAt(3600000, completed: true));
|
||||
await tester.pumpAndSettle();
|
||||
expect(calls.length, 2);
|
||||
expect(calls.last.arguments['url'], secondSource.url);
|
||||
expect(find.text('Episode 2'), findsOneWidget);
|
||||
expect(find.text('Retry'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(ListTile, '720p'));
|
||||
await tester.pump();
|
||||
expect(calls.length, 3);
|
||||
// Both episodes offer 720p: the row must dispatch episode 2, not 1.
|
||||
final recoveryUrl = calls.last.arguments['url'];
|
||||
recoveryExit.complete(exitAt(10000));
|
||||
await tester.pumpAndSettle();
|
||||
final recovered = state.history.singleWhere((e) => e.episodeId == 102);
|
||||
expect(recovered.episodeTitle, second.title);
|
||||
expect(recovered.positionSeconds, 10);
|
||||
expect(recoveryUrl, secondAlternative.url);
|
||||
expect(recovered.sourceUrl, secondAlternative.url);
|
||||
expect(state.history.map((e) => e.episodeId).toSet(), {101, 102});
|
||||
expect(find.text('Trailer'), findsNothing);
|
||||
expect(find.text('Denied'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Android default player dispatches VLC with resume and preserves source identity',
|
||||
(tester) async {
|
||||
final calls = <MethodCall>[];
|
||||
final result = Completer<Map<String, dynamic>>();
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
(call) async {
|
||||
calls.add(call);
|
||||
if (call.method == 'openNativePlayer') return result.future;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
null,
|
||||
),
|
||||
);
|
||||
final state = await mount(tester);
|
||||
addTearDown(state.dispose);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
final call = calls.singleWhere(
|
||||
(call) => call.method == 'openNativePlayer',
|
||||
);
|
||||
expect(call.arguments['url'], source.url);
|
||||
expect(call.arguments['positionMs'], 595000);
|
||||
expect(call.arguments['autoplay'], true);
|
||||
expect(state.history.single.positionSeconds, 600);
|
||||
result.complete(exitAt(700000));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.history.single.positionSeconds, 700);
|
||||
expect(state.history.single.sourceUrl, source.url);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||||
);
|
||||
|
||||
testWidgets('failed native open never overwrites a saved checkpoint', (
|
||||
tester,
|
||||
) async {
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
(call) async {
|
||||
if (call.method == 'openNativePlayer') return exitAt(0, opened: false);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
null,
|
||||
),
|
||||
);
|
||||
final state = await mount(tester);
|
||||
addTearDown(state.dispose);
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.history.single.positionSeconds, 600);
|
||||
expect(find.text('External player'), findsOneWidget);
|
||||
expect(find.text('Retry'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
||||
|
||||
testWidgets(
|
||||
'natural native episode completion opens next episode without losing history',
|
||||
(tester) async {
|
||||
const first = EpisodeRecord(
|
||||
id: 101,
|
||||
title: 'Episode 1',
|
||||
sources: [source],
|
||||
);
|
||||
const second = EpisodeRecord(
|
||||
id: 102,
|
||||
title: 'Episode 2',
|
||||
sources: [MediaSource(url: 'https://media.test/episode2.mkv')],
|
||||
);
|
||||
final calls = <MethodCall>[];
|
||||
final firstExit = Completer<Map<String, dynamic>>();
|
||||
final secondExit = Completer<Map<String, dynamic>>();
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
(call) async {
|
||||
if (call.method != 'openNativePlayer') return null;
|
||||
calls.add(call);
|
||||
return calls.length == 1 ? firstExit.future : secondExit.future;
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
MediaPlatform.channel,
|
||||
null,
|
||||
),
|
||||
);
|
||||
final state = await mount(
|
||||
tester,
|
||||
episode: first,
|
||||
playlist: [first, second],
|
||||
);
|
||||
addTearDown(state.dispose);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(calls.length, 1);
|
||||
firstExit.complete(exitAt(3600000, completed: true));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(calls.length, 2);
|
||||
expect(calls.last.arguments['url'], second.sources.single.url);
|
||||
expect(calls.last.arguments['positionMs'], 0);
|
||||
expect(state.history.single.episodeId, 101);
|
||||
secondExit.complete(exitAt(10000));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.history.map((e) => e.episodeId).toSet(), {101, 102});
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user