Jellyfin v10.11 and later no longer say which accounts have a password (HasPassword is always true, jellyfin/jellyfin#14950), so the demo's account was listed as "With a password" and only named in the form. Every listed account is now named in the form with the password field focused, which says to leave it empty for an account without one, as Jellyfin's own apps do; the line under each account goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
305 lines
10 KiB
Dart
305 lines
10 KiB
Dart
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/data/jellyfin_suggestion.dart';
|
||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.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<void> show(
|
||
WidgetTester tester,
|
||
String location, {
|
||
String? suggested,
|
||
}) 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,
|
||
jellyfinSuggestion: JellyfinSuggestion(() async => suggested),
|
||
),
|
||
location: location,
|
||
);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
/// Finds the server, from Settings or the studyset.
|
||
Future<void> 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 music library lists its albums and songs; a song without '
|
||
'lyrics is greyed and opens nothing, one with lyrics plays', (
|
||
tester,
|
||
) async {
|
||
jellyfin = sampleMusic();
|
||
store = signedInJellyfin();
|
||
await show(
|
||
tester,
|
||
RoutePaths.jellyfin(
|
||
'japanese',
|
||
parentId: 'album-1',
|
||
name: 'Sample Album',
|
||
),
|
||
);
|
||
|
||
expect(find.text('Sample Song'), findsOneWidget);
|
||
expect(find.textContaining('No lyrics'), findsOneWidget);
|
||
await tester.tap(find.text('Sample Song (Instrumental)'));
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Sample Song (Instrumental)'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Sample Song'));
|
||
for (var frame = 0; frame < 6; frame++) {
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
}
|
||
expect(find.text('Words in this song'), findsOneWidget);
|
||
});
|
||
|
||
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 Jellyfin only once this "
|
||
'device is signed in to a server (in Settings), as the assistant '
|
||
'shows only once switched on', (tester) async {
|
||
await show(tester, RoutePaths.studyset('japanese'));
|
||
await tester.tap(find.text('Add material'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Create a path'), findsOneWidget);
|
||
expect(find.text('From Jellyfin'), findsNothing);
|
||
});
|
||
|
||
testWidgets('signed in, Add material › From Jellyfin opens its '
|
||
'libraries', (tester) async {
|
||
store = signedInJellyfin();
|
||
await show(tester, RoutePaths.studyset('japanese'));
|
||
await tester.tap(find.text('Add material'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('From Jellyfin'));
|
||
await tester.pump();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Libraries'), findsOneWidget);
|
||
});
|
||
|
||
group('a Jellyfin server the instance suggests (decided 2026-10-13)', () {
|
||
setUp(() {
|
||
jellyfin
|
||
..shownUsers = const [
|
||
JellyfinPublicUser(id: 'user-2', name: 'Demo'),
|
||
JellyfinPublicUser(id: 'user-1', name: 'mika'),
|
||
]
|
||
..withoutPassword = const {'Demo'};
|
||
});
|
||
|
||
testWidgets('Add material offers it before signing in; the sign-in '
|
||
"finds it by itself and lists the server's public people, one "
|
||
'without a password signed in with the password left empty', (
|
||
tester,
|
||
) async {
|
||
await show(
|
||
tester,
|
||
RoutePaths.studyset('japanese'),
|
||
suggested: 'https://jellyfin.local:8920',
|
||
);
|
||
await tester.tap(find.text('Add material'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('From Jellyfin'));
|
||
await tester.pump();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Living room'), findsOneWidget);
|
||
expect(find.text('Demo'), findsOneWidget);
|
||
await tester.tap(find.text('Demo'));
|
||
await tester.pumpAndSettle();
|
||
expect(store.kept?.account, isNull);
|
||
await tester.tap(find.text('Sign in'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Libraries'), findsOneWidget);
|
||
expect(store.kept?.account?.userName, 'Demo');
|
||
});
|
||
|
||
testWidgets('a tap names one in the form, which asks for the '
|
||
'password', (tester) async {
|
||
await show(
|
||
tester,
|
||
RoutePaths.jellyfin('japanese'),
|
||
suggested: 'https://jellyfin.local:8920',
|
||
);
|
||
|
||
await tester.tap(find.text('mika'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(
|
||
tester
|
||
.widget<TextField>(find.widgetWithText(TextField, 'User name'))
|
||
.controller!
|
||
.text,
|
||
'mika',
|
||
);
|
||
expect(
|
||
tester
|
||
.widget<TextField>(find.widgetWithText(TextField, 'Password'))
|
||
.focusNode!
|
||
.hasFocus,
|
||
isTrue,
|
||
);
|
||
expect(store.kept?.account, isNull);
|
||
});
|
||
});
|
||
}
|