[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,325 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:reelbox/data/api_service.dart';
|
||||
import 'package:reelbox/data/models.dart';
|
||||
import 'package:reelbox/state/app_state.dart';
|
||||
import 'package:reelbox/ui/common.dart';
|
||||
import 'package:reelbox/ui/library.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const movie = TitleRecord(id: 42, title: 'Movie');
|
||||
const channel = TitleRecord(id: 42, title: 'Channel', type: 'channel');
|
||||
|
||||
class SettingsLocalApi extends ApiService {
|
||||
int discoveryChecks = 0;
|
||||
@override
|
||||
Future<void> initialize({bool discover = true}) async {
|
||||
if (discover) discoveryChecks++;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mount(WidgetTester tester, AppState state, Widget page) async {
|
||||
await tester.pumpWidget(
|
||||
AppScope(
|
||||
state: state,
|
||||
child: MaterialApp(home: page),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
|
||||
testWidgets('imported non-menu settings remain selected and editable', (
|
||||
tester,
|
||||
) async {
|
||||
final state = AppState(api: SettingsLocalApi());
|
||||
addTearDown(state.dispose);
|
||||
final backup = jsonDecode(state.exportLibrary()) as Map<String, dynamic>;
|
||||
backup['settings'] = {
|
||||
...state.prefs.toJson(),
|
||||
'defaultQuality': '2160p',
|
||||
'playbackSpeed': 1.75,
|
||||
'skipSeconds': 12,
|
||||
'subtitleColor': 0xFF12AB34,
|
||||
'subtitleBackground': 0x40123456,
|
||||
};
|
||||
await state.importLibrary(jsonEncode(backup));
|
||||
final original = state.prefs.toJson();
|
||||
await mount(tester, state, const SettingsPage());
|
||||
for (final setting in <String, Object>{
|
||||
'Default quality': '2160p',
|
||||
'Playback speed': 1.75,
|
||||
'Seek step': 12,
|
||||
'Subtitle color': 0xFF12AB34,
|
||||
'Subtitle background': 0x40123456,
|
||||
}.entries) {
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(setting.key),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
final tile = find.ancestor(
|
||||
of: find.text(setting.key),
|
||||
matching: find.byType(ListTile),
|
||||
);
|
||||
final dropdown = tester.widget<DropdownButton<dynamic>>(
|
||||
find.descendant(
|
||||
of: tile,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is DropdownButton,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(dropdown.value, setting.value);
|
||||
expect(
|
||||
dropdown.items!.where((item) => item.value == setting.value).length,
|
||||
1,
|
||||
);
|
||||
}
|
||||
expect(state.prefs.toJson(), original);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Playback speed'),
|
||||
-250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
final speedTile = find.ancestor(
|
||||
of: find.text('Playback speed'),
|
||||
matching: find.byType(ListTile),
|
||||
);
|
||||
await tester.ensureVisible(speedTile);
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: speedTile,
|
||||
matching: find.byWidgetPredicate((widget) => widget is DropdownButton),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('1×').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.prefs.playbackSpeed, 1.0);
|
||||
expect(state.prefs.defaultQuality, '2160p');
|
||||
});
|
||||
|
||||
for (final locale in ['en', 'fa']) {
|
||||
testWidgets(
|
||||
'settings keep useful controls without endpoint or technical clutter in $locale',
|
||||
(tester) async {
|
||||
final api = SettingsLocalApi();
|
||||
final state = AppState(api: api);
|
||||
addTearDown(state.dispose);
|
||||
await state.initialize(discover: false);
|
||||
await state.updateSettings(state.prefs.copyWith(locale: locale));
|
||||
await mount(tester, state, const SettingsPage());
|
||||
final labels = <String>{};
|
||||
final scrollable = find.byType(Scrollable).first;
|
||||
for (var step = 0; step < 40; step++) {
|
||||
labels.addAll(
|
||||
tester.widgetList<Text>(find.byType(Text)).map((w) => w.data ?? ''),
|
||||
);
|
||||
expect(find.byKey(const Key('catalog-api-endpoint')), findsNothing);
|
||||
final position = tester.state<ScrollableState>(scrollable).position;
|
||||
if (position.pixels >= position.maxScrollExtent) break;
|
||||
await tester.drag(scrollable, const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
final copy = labels.join('\n');
|
||||
expect(copy, isNot(contains(ApiService.iranBaseUrl)));
|
||||
expect(
|
||||
copy,
|
||||
isNot(
|
||||
matches(
|
||||
RegExp(
|
||||
r'API|Iran Catalogue|فهرست ایران|Parallel download segments|بخشهای همزمان دانلود|Player volume|بلندی صدای پخش',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final label
|
||||
in locale == 'en'
|
||||
? [
|
||||
'Language',
|
||||
'Theme',
|
||||
'Resume from last position',
|
||||
'Enable subtitles',
|
||||
'Download on Wi-Fi only',
|
||||
'Export local library',
|
||||
'Import local library',
|
||||
'Clear disposable cache',
|
||||
]
|
||||
: [
|
||||
'زبان',
|
||||
'پوسته',
|
||||
'ادامه از آخرین موقعیت',
|
||||
'نمایش زیرنویس',
|
||||
'دانلود فقط با وایفای',
|
||||
'خروجی کتابخانه محلی',
|
||||
'ورود کتابخانه محلی',
|
||||
'پاک کردن حافظه موقت',
|
||||
]) {
|
||||
expect(labels, contains(label));
|
||||
}
|
||||
for (final label
|
||||
in locale == 'en'
|
||||
? ['Export local library', 'Import local library']
|
||||
: ['خروجی کتابخانه محلی', 'ورود کتابخانه محلی']) {
|
||||
final tile = find.ancestor(
|
||||
of: find.text(label),
|
||||
matching: find.byType(ListTile),
|
||||
);
|
||||
expect(tester.widget<ListTile>(tile).onTap, isNotNull);
|
||||
}
|
||||
expect(api.discoveryChecks, 0);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('More opens a factual privacy and local-data explanation', (
|
||||
tester,
|
||||
) async {
|
||||
final state = AppState(api: SettingsLocalApi());
|
||||
addTearDown(state.dispose);
|
||||
await mount(tester, state, const MorePage());
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Privacy & your data'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.tap(find.text('Privacy & your data'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('External services'), findsOneWidget);
|
||||
final disclosures = <String>{};
|
||||
for (final topic in [
|
||||
'On this device',
|
||||
'External services',
|
||||
'Sharing & backups',
|
||||
'Deleting local data',
|
||||
'Unencrypted media',
|
||||
]) {
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(topic),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
disclosures.addAll(
|
||||
tester.widgetList<Text>(find.byType(Text)).map((w) => w.data ?? ''),
|
||||
);
|
||||
expect(find.text(topic), findsOneWidget);
|
||||
}
|
||||
final copy = disclosures.join('\n');
|
||||
for (final disclosure in [
|
||||
'No account needed',
|
||||
'recent searches',
|
||||
'IP address',
|
||||
'access tokens',
|
||||
'outside Reelbox’s control',
|
||||
'completed downloads',
|
||||
'unencrypted HTTP',
|
||||
'observe or modify',
|
||||
]) {
|
||||
expect(copy, contains(disclosure));
|
||||
}
|
||||
expect(copy, isNot(contains('API')));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('notifications explain new-task scope and Android requirements', (
|
||||
tester,
|
||||
) async {
|
||||
final state = AppState(api: SettingsLocalApi());
|
||||
addTearDown(state.dispose);
|
||||
await mount(tester, state, const SettingsPage());
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Notifications for new downloads'),
|
||||
400,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('Notifications for new downloads'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('Running downloads keep their notification settings'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.textContaining(
|
||||
'Android may still require a foreground notification',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'legacy channel history is pruned while same-ID movie remains removable',
|
||||
(tester) async {
|
||||
final state = AppState(api: SettingsLocalApi());
|
||||
addTearDown(state.dispose);
|
||||
final backup = jsonDecode(state.exportLibrary()) as Map<String, dynamic>;
|
||||
backup['history'] = [
|
||||
WatchEntry(title: channel, positionSeconds: 30).toJson(),
|
||||
WatchEntry(title: movie, positionSeconds: 15).toJson(),
|
||||
];
|
||||
await state.importLibrary(jsonEncode(backup));
|
||||
await mount(tester, state, const LibraryPage());
|
||||
expect(find.text('Watchlist (0)'), findsOneWidget);
|
||||
expect(find.text('Continue (1)'), findsOneWidget);
|
||||
await tester.tap(find.text('Continue (1)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Channel'), findsNothing);
|
||||
expect(find.text('Movie'), findsOneWidget);
|
||||
expect(state.history.single.title.toJson(), movie.toJson());
|
||||
expect(state.resumeSeconds(42), 15);
|
||||
expect(state.resumeSeconds(42, kind: 'channel'), 0);
|
||||
await tester.tap(find.byTooltip('Remove history'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.history, isEmpty);
|
||||
expect(find.text('Continue (0)'), findsOneWidget);
|
||||
expect(find.text('Pick up where you left off'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'removed channel card has no bookmark action and preserves the same-ID movie',
|
||||
(tester) async {
|
||||
final state = AppState(api: SettingsLocalApi());
|
||||
addTearDown(state.dispose);
|
||||
await state.toggleSaved(movie);
|
||||
await mount(
|
||||
tester,
|
||||
state,
|
||||
const Scaffold(
|
||||
body: SizedBox(height: 244, child: PosterCard(title: channel)),
|
||||
),
|
||||
);
|
||||
expect(find.text('Channel'), findsNothing);
|
||||
expect(find.byTooltip('Watchlist'), findsNothing);
|
||||
expect(find.byIcon(Icons.bookmark_border_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.bookmark_rounded), findsNothing);
|
||||
expect(state.saved.single.toJson(), movie.toJson());
|
||||
expect(state.hasSaved(42, kind: 'channel'), isFalse);
|
||||
|
||||
// Allowed cards still expose the real bookmark action for this ID.
|
||||
await mount(
|
||||
tester,
|
||||
state,
|
||||
const Scaffold(
|
||||
body: SizedBox(height: 244, child: PosterCard(title: movie)),
|
||||
),
|
||||
);
|
||||
expect(find.text('Movie'), findsWidgets);
|
||||
expect(find.byIcon(Icons.bookmark_rounded), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Watchlist'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.saved, isEmpty);
|
||||
expect(find.byIcon(Icons.bookmark_border_rounded), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Watchlist'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.saved.single.toJson(), movie.toJson());
|
||||
expect(find.byIcon(Icons.bookmark_rounded), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user