Files
reelbox/test/next_library_ui_test.dart

324 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:reelbox/data/api_service.dart';
import 'package:reelbox/data/models.dart';
import 'package:reelbox/app.dart';
import 'package:reelbox/state/app_state.dart';
import 'package:reelbox/ui/library.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _LocalApi extends ApiService {
@override
Future<void> initialize({bool discover = true}) async {}
@override
Future<Map<String, dynamic>> home() async => {
'genres': [],
'actors': [],
'countries': [],
};
@override
Future<List<Map<String, dynamic>>> actors({
int page = 1,
String search = '',
}) async => [];
@override
Future<List<Tag>> countries() async => [];
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
Future<AppState> state() async {
final s = AppState(api: _LocalApi());
await s.initialize(discover: false);
addTearDown(s.dispose);
return s;
}
Widget host(AppState s, Widget child) => AppScope(
state: s,
child: MaterialApp(home: child),
);
testWidgets(
'fresh app routes through introduction before exposing navigation',
(tester) async {
final s = await state();
expect(s.prefs.introCompleted, false);
await tester.pumpWidget(MyApp(state: s));
await tester.pumpAndSettle();
expect(find.byType(FirstRunIntroPage), findsOneWidget);
expect(find.byType(NavigationBar), findsNothing);
await tester.tap(find.byKey(const Key('intro-skip')));
await tester.pumpAndSettle();
expect(s.prefs.introCompleted, true);
expect(find.byType(FirstRunIntroPage), findsNothing);
expect(find.byType(NavigationBar), findsOneWidget);
},
);
testWidgets(
'counted library tabs remain readable and reachable with large text',
(tester) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final s = await state();
await tester.pumpWidget(
host(
s,
MediaQuery(
data: const MediaQueryData(textScaler: TextScaler.linear(2)),
child: const LibraryPage(),
),
),
);
await tester.pumpAndSettle();
final label = find.text('Collections (0)');
await tester.ensureVisible(label);
await tester.pumpAndSettle();
final render = tester.renderObject<RenderBox>(label);
expect(
render.size.width,
greaterThanOrEqualTo(
render.getMaxIntrinsicWidth(double.infinity) - 0.1,
),
);
await tester.tap(label);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
},
);
for (final locale in ['en', 'fa']) {
testWidgets(
'More shares independent app information in $locale without an invented install link',
(tester) async {
const channel = MethodChannel('dev.fluttercommunity.plus/share');
final payloads = <Map>[];
final methods = <String>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(call) async {
methods.add(call.method);
payloads.add(call.arguments as Map);
return 'fixture.receiver';
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
final s = await state();
await s.updateSettings(s.prefs.copyWith(locale: locale));
await tester.pumpWidget(host(s, const MorePage()));
expect(payloads, isEmpty);
final action = find.byKey(const Key('share-reelbox'));
await tester.scrollUntilVisible(action, 300);
await tester.ensureVisible(action);
await tester.pumpAndSettle();
await tester.tap(action);
await tester.pumpAndSettle();
expect(payloads, hasLength(1));
expect(methods, ['share']);
expect(
payloads.single['text'],
locale == 'en'
? 'Reelbox — an account-free film and series client with a private, device-local library'
: 'Reelbox — فیلم و سریال، با کتابخانه خصوصی روی دستگاه و بدون نیاز به حساب کاربری',
);
expect(payloads.single['title'], 'Reelbox');
expect(
payloads.single['text'],
isNot(
matches(
RegExp(
r'https?://|telegram|t\.me/|ملودی|نورتون',
caseSensitive: false,
),
),
),
);
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
}
testWidgets(
'unavailable app sharing shows a safe error rather than native details',
(tester) async {
const channel = MethodChannel('dev.fluttercommunity.plus/share');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
_,
) async {
throw PlatformException(
code: 'fixture',
message: 'private native fixture detail',
);
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
final s = await state();
await tester.pumpWidget(host(s, const MorePage()));
final action = find.byKey(const Key('share-reelbox'));
await tester.scrollUntilVisible(action, 300);
await tester.ensureVisible(action);
await tester.pumpAndSettle();
await tester.tap(action);
await tester.pumpAndSettle();
expect(
find.text('Sharing is unavailable on this device'),
findsOneWidget,
);
expect(
find.textContaining('private native fixture detail'),
findsNothing,
);
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
testWidgets('first-run skip persists completion without touching library', (
tester,
) async {
final s = await state();
await tester.pumpWidget(host(s, const FirstRunIntroPage()));
await tester.tap(find.byKey(const Key('intro-skip')));
await tester.pumpAndSettle();
expect(s.prefs.introCompleted, true);
});
testWidgets('first-run finish persists completion', (tester) async {
final s = await state();
await tester.pumpWidget(host(s, const FirstRunIntroPage()));
await tester.tap(find.byKey(const Key('intro-complete')));
await tester.pumpAndSettle();
expect(s.prefs.introCompleted, true);
});
testWidgets(
'introduction keeps a short description and local-data disclosure',
(tester) async {
final s = await state();
await tester.pumpWidget(host(s, const FirstRunIntroPage()));
expect(
find.text(
'Save films and series, resume watching and download for offline viewing.',
),
findsOneWidget,
);
expect(find.textContaining('No account needed'), findsOneWidget);
expect(
find.textContaining('external catalog and media providers'),
findsOneWidget,
);
expect(find.text('English'), findsOneWidget);
expect(find.text('فارسی'), findsOneWidget);
},
);
testWidgets(
'more exposes no removed service links and about uses runtime package info',
(tester) async {
final s = await state();
PackageInfo.setMockInitialValues(
appName: 'Reelbox',
packageName: 'org.reelbox.app',
version: '8.9.10',
buildNumber: '456',
buildSignature: '',
);
await tester.pumpWidget(host(s, const MorePage()));
expect(find.text('Live channels'), findsNothing);
expect(find.text('Public playlists'), findsNothing);
expect(find.text('Original service support'), findsNothing);
await tester.scrollUntilVisible(find.text('About Reelbox'), 300);
await tester.tap(find.text('About Reelbox'));
await tester.pumpAndSettle();
expect(find.textContaining('8.9.10'), findsOneWidget);
expect(find.textContaining('456'), findsOneWidget);
expect(find.textContaining('org.reelbox.app'), findsOneWidget);
for (final label in [
'Dart runtime',
'Playback engine',
'Display',
'Device platform',
'Flutter framework',
'Flutter engine revision',
]) {
expect(find.text(label), findsNothing);
}
expect(find.text('Open-source licenses'), findsOneWidget);
await tester.ensureVisible(find.text('Open-source licenses'));
await tester.tap(find.text('Open-source licenses'));
await tester.pumpAndSettle();
expect(find.byType(LicensePage), findsOneWidget);
},
);
testWidgets(
'custom ARGB color and opacity save exact RGB without changing background',
(tester) async {
final s = await state();
await tester.pumpWidget(host(s, const SettingsPage()));
await tester.scrollUntilVisible(find.text('Custom subtitle color'), 450);
await tester.tap(find.text('Custom subtitle color'));
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const Key('custom-color-hex')),
'12345678',
);
await tester.pump();
await tester.tap(find.byKey(const Key('custom-color-opacity')));
await tester.pump();
await tester.tap(
find.descendant(
of: find.byType(AlertDialog),
matching: find.text('Save'),
),
);
await tester.pumpAndSettle();
expect(s.prefs.subtitleColor & 0xFFFFFF, 0x345678);
expect(s.prefs.subtitleColor >> 24, inInclusiveRange(1, 254));
expect(s.prefs.subtitleBackground, 0x80000000);
expect(tester.takeException(), isNull);
},
);
testWidgets(
'settings safely displays imported custom values and resets subtitle style only',
(tester) async {
final s = await state();
await s.updateSettings(
s.prefs.copyWith(
subtitleFontFamily: 'My Custom Font',
subtitleColor: 0x12123456,
subtitleFontSize: 72,
playbackSpeed: 1.7,
parallelDownloads: 7,
playerVolume: 0.33,
),
);
await tester.pumpWidget(host(s, const SettingsPage()));
await tester.scrollUntilVisible(
find.byKey(const Key('reset-subtitle-style')),
450,
);
expect(tester.takeException(), isNull);
await tester.tap(find.byKey(const Key('reset-subtitle-style')));
await tester.pumpAndSettle();
expect(s.prefs.subtitleColor, 0xFFFFFFFF);
expect(s.prefs.parallelDownloads, 7);
expect(s.prefs.playbackSpeed, 1.7);
},
);
}