- PaginatedCardGridTabState: the collections and playlists tabs were 95% identical; they now supply only pageSize/fetchPage/idOf instead of each duplicating the grid, memo, inflation budget and focus wiring. - EtagCachedRemoteStore: the anime-lists and fribb mapping stores now share one download/cache/isolate-parse/conditional-GET lifecycle. - FocusableTileStateMixin manages its own initState/didUpdateWidget/dispose instead of requiring every caller to forward three lifecycle hooks. Also drops unused ServerCapabilities entries and dead code in focusable_list_tile and music/track_row.
176 lines
6.1 KiB
Dart
176 lines
6.1 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:plezy/focus/focusable_button.dart';
|
|
import 'package:plezy/media/ids.dart';
|
|
import 'package:plezy/media/media_backend.dart';
|
|
import 'package:plezy/media/media_kind.dart';
|
|
import 'package:plezy/media/media_library.dart';
|
|
import 'package:plezy/providers/multi_server_provider.dart';
|
|
import 'package:plezy/screens/libraries/state_messages.dart';
|
|
import 'package:plezy/screens/libraries/tabs/library_browse_tab.dart';
|
|
import 'package:plezy/services/jellyfin_client.dart';
|
|
import 'package:plezy/services/storage_service.dart';
|
|
import 'package:plezy/services/multi_server_manager.dart';
|
|
import 'package:plezy/services/settings_service.dart';
|
|
import 'package:plezy/utils/platform_detector.dart';
|
|
import 'package:plezy/widgets/focusable_filter_chip.dart';
|
|
import 'package:plezy/widgets/media_card.dart';
|
|
|
|
import '../../test_helpers/backend_client_fixtures.dart';
|
|
import '../../test_helpers/library_tab_scaffold.dart';
|
|
import '../../test_helpers/multi_server_fixtures.dart';
|
|
import '../../test_helpers/prefs.dart';
|
|
|
|
final _musicLibrary = MediaLibrary(
|
|
id: 'music-library',
|
|
backend: MediaBackend.jellyfin,
|
|
title: 'Music',
|
|
kind: MediaKind.artist,
|
|
serverId: ServerId('music-server'),
|
|
);
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() async {
|
|
resetSharedPreferencesForTest();
|
|
SettingsService.resetForTesting();
|
|
await SettingsService.getInstance();
|
|
await StorageService.getInstance();
|
|
});
|
|
|
|
tearDown(() {
|
|
TvDetectionService.debugSetAppleTVOverride(null);
|
|
});
|
|
|
|
testWidgets('TV full-card preference keeps music browse captions visible', (tester) async {
|
|
TvDetectionService.debugSetAppleTVOverride(true);
|
|
await SettingsService.instance.write(SettingsService.tvFullCardLayout, true);
|
|
final harness = _MusicBrowseHarness();
|
|
addTearDown(harness.dispose);
|
|
|
|
await _pumpBrowseTab(tester, harness);
|
|
|
|
expect(harness.browseRequestCount, 1);
|
|
final card = tester.widget<MediaCard>(find.byType(MediaCard).first);
|
|
expect(card.fullBleedImage, isFalse);
|
|
expect(find.text('Artist One'), findsOneWidget);
|
|
expect(find.descendant(of: find.byType(MediaCard), matching: find.byType(ClipOval)), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('D-pad down focuses Retry and select reloads music browse', (tester) async {
|
|
TvDetectionService.debugSetAppleTVOverride(true);
|
|
final harness = _MusicBrowseHarness(failFirstBrowse: true);
|
|
addTearDown(harness.dispose);
|
|
|
|
await _pumpBrowseTab(tester, harness);
|
|
|
|
expect(find.byType(ErrorStateWidget), findsOneWidget);
|
|
expect(harness.browseRequestCount, 1);
|
|
|
|
final groupingChip = tester.widget<FocusableFilterChip>(find.byType(FocusableFilterChip).first);
|
|
groupingChip.focusNode!.requestFocus();
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
|
|
final retryFinder = find.descendant(of: find.byType(ErrorStateWidget), matching: find.byType(FocusableButton));
|
|
final retry = tester.widget<FocusableButton>(retryFinder);
|
|
expect(retry.focusNode, isNotNull);
|
|
expect(retry.focusNode!.hasFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await pumpRequestFrames(tester);
|
|
|
|
expect(harness.browseRequestCount, 2);
|
|
expect(find.byType(ErrorStateWidget), findsNothing);
|
|
expect(find.text('Artist One'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('missing library owner shows an error without querying another online server', (tester) async {
|
|
final harness = _MusicBrowseHarness();
|
|
addTearDown(harness.dispose);
|
|
final missingOwnerLibrary = MediaLibrary(
|
|
id: _musicLibrary.id,
|
|
backend: _musicLibrary.backend,
|
|
title: _musicLibrary.title,
|
|
kind: _musicLibrary.kind,
|
|
serverId: 'missing-server',
|
|
);
|
|
|
|
await _pumpBrowseTab(tester, harness, library: missingOwnerLibrary);
|
|
|
|
expect(find.byType(ErrorStateWidget), findsOneWidget);
|
|
expect(harness.requestCount, 0);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
|
|
Future<void> _pumpBrowseTab(WidgetTester tester, _MusicBrowseHarness harness, {MediaLibrary? library}) async {
|
|
await pumpLibraryTab(
|
|
tester,
|
|
provider: harness.provider,
|
|
tab: LibraryBrowseTab(
|
|
library: library ?? _musicLibrary,
|
|
canGroupByFolders: true,
|
|
suppressAutoFocus: true,
|
|
onBack: () {},
|
|
),
|
|
);
|
|
await pumpRequestFrames(tester);
|
|
}
|
|
|
|
class _MusicBrowseHarness {
|
|
final bool failFirstBrowse;
|
|
var browseRequestCount = 0;
|
|
var requestCount = 0;
|
|
late final JellyfinClient client;
|
|
late final MultiServerManager manager;
|
|
late final MultiServerProvider provider;
|
|
|
|
_MusicBrowseHarness({this.failFirstBrowse = false}) {
|
|
client = JellyfinClient.forTesting(
|
|
connection: testJellyfinConnection(machineId: 'music-server'),
|
|
httpClient: MockClient((request) async {
|
|
requestCount++;
|
|
if (request.url.path == '/Items/Filters') {
|
|
return http.Response(
|
|
jsonEncode({'Genres': const [], 'OfficialRatings': const [], 'Tags': const [], 'Years': const []}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
if (request.url.path == '/Artists/AlbumArtists') {
|
|
browseRequestCount++;
|
|
if (failFirstBrowse && browseRequestCount == 1) {
|
|
return http.Response('gateway timeout', 504);
|
|
}
|
|
return http.Response(
|
|
jsonEncode({
|
|
'Items': const [
|
|
{'Id': 'artist-1', 'Name': 'Artist One', 'Type': 'MusicArtist'},
|
|
],
|
|
'TotalRecordCount': 1,
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
}),
|
|
);
|
|
manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
|
provider = testMultiServerProvider(manager);
|
|
}
|
|
|
|
void dispose() {
|
|
provider.dispose();
|
|
manager.dispose();
|
|
}
|
|
}
|