import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart'; import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart'; import 'package:halkyon_learn_flutter/src/feature/jellyfin/widget/watch_screen.dart'; import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart'; import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart'; import 'package:material_ui/material_ui.dart'; import '../../support/fake_jellyfin.dart'; import '../../support/fake_studyset_gateway.dart'; import '../../support/pump_app.dart'; import '../../support/sample_episode.dart'; import '../../support/test_dependencies.dart'; void main() { late FakeJellyfinGateway jellyfin; late MemoryJellyfinStore store; setUp(() { jellyfin = sampleJellyfin(); store = MemoryJellyfinStore(); }); Future show(WidgetTester tester, String location) async { tester.view.physicalSize = const Size(1200, 1400); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final studysets = FakeStudysetGateway() ..receiveRemoteChange( 'japanese', encodeStudysetPayload( StudysetContent( title: 'Japanese', description: RichContent.empty(), frontLanguage: 'ja', backLanguage: 'en', ), ), ); await pumpApp( tester, testDependencies( studysetGateway: studysets, dictionaryGateway: sampleDictionary(), jellyfinGateway: jellyfin, jellyfinStore: store, ), location: location, ); await tester.pumpAndSettle(); } /// Finds the server, from Settings or the studyset. Future findServer(WidgetTester tester) async { await tester.enterText( find.widgetWithText(TextField, 'Server address'), 'jellyfin.local:8920', ); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); } testWidgets('Settings connects a server over HTTPS with a name and ' 'password, kept on the device, and signs out of it', (tester) async { await show(tester, RoutePaths.settings); await tester.scrollUntilVisible( find.widgetWithText(TextField, 'Server address'), 200, scrollable: find.byType(Scrollable).first, ); await tester.enterText( find.widgetWithText(TextField, 'Server address'), 'nothing.local', ); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect( find.text('No server answered at that address over HTTPS.'), findsOneWidget, ); await tester.enterText( find.widgetWithText(TextField, 'Server address'), 'http://jellyfin.local:8920', ); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); expect( find.text( "That address isn't encrypted, and your password would travel in " "the clear. Use the server's HTTPS address.", ), findsOneWidget, ); await findServer(tester); expect(find.text('Living room'), findsOneWidget); await tester.enterText(find.widgetWithText(TextField, 'User name'), 'mika'); await tester.enterText(find.widgetWithText(TextField, 'Password'), 'wrong'); await tester.tap(find.text('Sign in')); await tester.pumpAndSettle(); expect(find.text('The user name or password is wrong.'), findsOneWidget); await tester.enterText( find.widgetWithText(TextField, 'Password'), 'secret', ); await tester.tap(find.text('Sign in')); await tester.pumpAndSettle(); expect(find.text('mika on Living room'), findsOneWidget); expect(store.kept?.account?.token, 'token-1'); await tester.tap(find.byTooltip('Sign out of Jellyfin')); await tester.pumpAndSettle(); expect(find.widgetWithText(TextField, 'Server address'), findsOneWidget); expect(jellyfin.signedOut, ['token-1']); }); testWidgets('a code approved in another Jellyfin app signs in', ( tester, ) async { await show(tester, RoutePaths.jellyfin('japanese')); await findServer(tester); await tester.tap(find.text('Sign in with a code')); // It waits, spinning, until the code is approved. await tester.pump(); await tester.pump(); expect(find.text('123456'), findsOneWidget); await tester.pump(const Duration(seconds: 3)); expect(find.text('123456'), findsOneWidget); jellyfin.approved = true; await tester.pump(const Duration(seconds: 3)); await tester.pumpAndSettle(); expect(find.text('Anime'), findsOneWidget); expect(store.kept?.account?.token, 'token-2'); }); testWidgets('a show tied to the studyset comes first next time; an ' 'episode opens to be watched', (tester) async { store = signedInJellyfin(); await show(tester, RoutePaths.jellyfin('japanese')); expect(find.text('Tied to this studyset'), findsNothing); await tester.tap(find.text('Anime')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Tie to this studyset')); await tester.pumpAndSettle(); expect(find.byTooltip('Untie from this studyset'), findsOneWidget); await tester.pageBack(); await tester.pumpAndSettle(); expect(find.text('Tied to this studyset'), findsOneWidget); await tester.tap(find.text('Sample Journey')); await tester.pumpAndSettle(); await tester.tap(find.text('Season 1')); await tester.pumpAndSettle(); expect(find.text('S1 E1 ยท 24 min'), findsOneWidget); await tester.tap(find.text('The Station')); await tester.pumpAndSettle(); expect(find.byType(WatchScreen), findsOneWidget); }); testWidgets('a search finds shows and episodes by their name', ( tester, ) async { store = signedInJellyfin(); await show(tester, RoutePaths.jellyfin('japanese')); await tester.enterText(find.byType(TextField), 'station'); await tester.pump(const Duration(milliseconds: 300)); await tester.pumpAndSettle(); expect(find.text('The Station'), findsOneWidget); expect(find.text('Libraries'), findsNothing); }); testWidgets("the studyset's Add material offers watching from Jellyfin", ( tester, ) async { await show(tester, RoutePaths.studyset('japanese')); await tester.tap(find.text('Add material')); await tester.pumpAndSettle(); await tester.tap(find.text('Watch from Jellyfin')); await tester.pump(); await tester.pumpAndSettle(); expect(find.widgetWithText(TextField, 'Server address'), findsOneWidget); }); }