A library that a server push had marked stale stayed stale for good if the reload triggered on the next activation failed: the tab wrote the push's epoch down as consumed before starting the reload, so every later activation, tab switch, and visibility replay saw nothing to do until another push arrived. The same shared load-start snapshot was also read by whichever load committed last rather than the one that had taken it, and in folder grouping the epoch was credited from a paged fetch nobody renders while the displayed folder tree was never reloaded. Each load now owns its epoch snapshot and commits it only when its data actually landed and it is still the current load; a failed or dropped load releases the snapshot without credit. An in-flight snapshot still dedupes an activation that lands mid-fetch. Folder grouping refreshes the displayed tree and credits the push only when that refresh succeeded, and switching libraries resets the previous library's credit.
148 lines
5.2 KiB
Dart
148 lines
5.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:plezy/focus/input_mode_tracker.dart';
|
|
import 'package:plezy/media/media_kind.dart';
|
|
import 'package:plezy/providers/multi_server_provider.dart';
|
|
import 'package:plezy/screens/libraries/folder_tree_view.dart';
|
|
import 'package:plezy/services/jellyfin_client.dart';
|
|
import 'package:plezy/services/settings_service.dart';
|
|
import 'package:plezy/theme/mono_theme.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../test_helpers/backend_client_fixtures.dart';
|
|
import '../../test_helpers/multi_server_fixtures.dart';
|
|
import '../../test_helpers/prefs.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() async {
|
|
resetSharedPreferencesForTest();
|
|
SettingsService.resetForTesting();
|
|
await SettingsService.getInstance();
|
|
});
|
|
|
|
testWidgets('Jellyfin music artists and albums expand through direct children', (tester) async {
|
|
final client = JellyfinClient.forTesting(
|
|
connection: testJellyfinConnection(machineId: 'music-server'),
|
|
httpClient: MockClient((request) async {
|
|
final parentId = request.url.queryParameters['ParentId'];
|
|
final foldersOnly = request.url.queryParameters['IncludeItemTypes'] == 'Folder,CollectionFolder';
|
|
final items = foldersOnly
|
|
? const <Map<String, Object?>>[]
|
|
: switch (parentId) {
|
|
'music-library' => const [
|
|
{'Id': 'artist-1', 'Name': 'Artist One', 'Type': 'MusicArtist', 'IsFolder': true},
|
|
],
|
|
'artist-1' => const [
|
|
{
|
|
'Id': 'album-1',
|
|
'Name': 'Album One',
|
|
'Type': 'MusicAlbum',
|
|
'IsFolder': true,
|
|
'AlbumArtists': [
|
|
{'Id': 'artist-1', 'Name': 'Artist One'},
|
|
],
|
|
},
|
|
],
|
|
'album-1' => const [
|
|
{'Id': 'track-1', 'Name': 'Track One', 'Type': 'Audio', 'AlbumId': 'album-1', 'Album': 'Album One'},
|
|
],
|
|
_ => const <Map<String, Object?>>[],
|
|
};
|
|
return http.Response(
|
|
jsonEncode({'Items': items, 'TotalRecordCount': items.length}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}),
|
|
);
|
|
final provider = testMultiServer(clients: [client]).provider;
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<MultiServerProvider>.value(
|
|
value: provider,
|
|
child: InputModeTracker(
|
|
child: MaterialApp(
|
|
theme: monoTheme(dark: true),
|
|
home: Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: const [
|
|
FolderTreeView(libraryKey: 'music-library', serverId: 'music-server', libraryKind: MediaKind.artist),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Artist One'), findsOneWidget);
|
|
expect(find.text('Album One'), findsNothing);
|
|
|
|
await tester.tap(find.text('Artist One'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Album One'), findsOneWidget);
|
|
expect(find.text('Track One'), findsNothing);
|
|
|
|
await tester.tap(find.text('Album One'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Track One'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('refresh reports whether the root listing landed', (tester) async {
|
|
var failNext = false;
|
|
final client = JellyfinClient.forTesting(
|
|
connection: testJellyfinConnection(machineId: 'srv'),
|
|
httpClient: MockClient((request) async {
|
|
if (failNext) return http.Response('down', 500);
|
|
final foldersOnly = request.url.queryParameters['IncludeItemTypes'] == 'Folder,CollectionFolder';
|
|
final items = foldersOnly
|
|
? const <Map<String, Object?>>[]
|
|
: const [
|
|
{'Id': 'f1', 'Name': 'Folder One', 'Type': 'Folder', 'IsFolder': true},
|
|
];
|
|
return http.Response(
|
|
jsonEncode({'Items': items, 'TotalRecordCount': items.length}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}),
|
|
);
|
|
final provider = testMultiServer(clients: [client]).provider;
|
|
final key = GlobalKey<FolderTreeViewState>();
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<MultiServerProvider>.value(
|
|
value: provider,
|
|
child: InputModeTracker(
|
|
child: MaterialApp(
|
|
theme: monoTheme(dark: true),
|
|
home: Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: [FolderTreeView(key: key, libraryKey: 'lib', serverId: 'srv', libraryKind: MediaKind.movie)],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Folder One'), findsOneWidget);
|
|
|
|
final refreshed = key.currentState!.refresh();
|
|
await tester.pumpAndSettle();
|
|
expect(await refreshed, isTrue);
|
|
|
|
failNext = true;
|
|
final failed = key.currentState!.refresh();
|
|
await tester.pumpAndSettle();
|
|
expect(await failed, isFalse);
|
|
});
|
|
}
|