- 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.
214 lines
6.7 KiB
Dart
214 lines
6.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/i18n/strings.g.dart';
|
|
import 'package:plezy/media/ids.dart';
|
|
import 'package:plezy/media/library_query.dart';
|
|
import 'package:plezy/media/media_backend.dart';
|
|
import 'package:plezy/media/media_hub.dart';
|
|
import 'package:plezy/media/media_item.dart';
|
|
import 'package:plezy/media/media_kind.dart';
|
|
import 'package:plezy/media/media_server_client.dart';
|
|
import 'package:plezy/media/server_capabilities.dart';
|
|
import 'package:plezy/providers/multi_server_provider.dart';
|
|
import 'package:plezy/screens/hub_detail_screen.dart';
|
|
import 'package:plezy/services/multi_server_manager.dart';
|
|
import 'package:plezy/services/settings_service.dart';
|
|
import 'package:plezy/theme/mono_theme.dart';
|
|
import 'package:plezy/utils/media_server_http_client.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../test_helpers/paged_fakes.dart';
|
|
import '../test_helpers/prefs.dart';
|
|
import '../test_helpers/media_items.dart';
|
|
import '../test_helpers/multi_server_fixtures.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
resetSharedPreferencesForTest();
|
|
SettingsService.resetForTesting();
|
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
|
});
|
|
|
|
testWidgets('Jellyfin hub advances by raw page size after screen filtering', (tester) async {
|
|
final items = List.generate(
|
|
205,
|
|
(index) => _item(index, libraryId: index.isEven ? '7' : '8', backend: MediaBackend.jellyfin),
|
|
);
|
|
final harness = await _createHarness(items, backend: MediaBackend.jellyfin);
|
|
|
|
await tester.pumpWidget(
|
|
harness.wrap(
|
|
HubDetailScreen(
|
|
hub: MediaHub(
|
|
id: 'home.continue',
|
|
title: 'Recent',
|
|
type: 'movie',
|
|
items: items.take(5).toList(),
|
|
size: items.length,
|
|
more: true,
|
|
libraryId: '7',
|
|
serverId: 'server_1',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(harness.client.requestedStarts, [0, 200]);
|
|
expect(harness.client.fullHubRequests, 0);
|
|
expect(find.text(t.common.retry), findsNothing);
|
|
|
|
await tester.drag(find.byType(CustomScrollView), const Offset(0, -30000));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Item 204'), findsOneWidget);
|
|
expect(find.text('Item 203'), findsNothing);
|
|
});
|
|
|
|
testWidgets('Jellyfin Recently Added fetches every page only as the user reaches the end', (tester) async {
|
|
final items = List.generate(450, (index) => _item(index, backend: MediaBackend.jellyfin));
|
|
final harness = await _createHarness(items, backend: MediaBackend.jellyfin);
|
|
|
|
await tester.pumpWidget(
|
|
harness.wrap(
|
|
HubDetailScreen(
|
|
hub: MediaHub(
|
|
id: 'home.recent',
|
|
title: 'Recently Added',
|
|
type: 'mixed',
|
|
items: items.take(20).toList(),
|
|
size: items.length,
|
|
more: true,
|
|
serverId: 'server_1',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(harness.client.requestedStarts, [0]);
|
|
expect(harness.client.requestedSizes, [200]);
|
|
|
|
await tester.drag(find.byType(CustomScrollView), const Offset(0, -50000));
|
|
await tester.pumpAndSettle();
|
|
expect(harness.client.requestedStarts, [0, 200, 400]);
|
|
expect(harness.client.requestedSizes, [200, 200, 50]);
|
|
await tester.drag(find.byType(CustomScrollView), const Offset(0, -50000));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Item 449'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Plex hub replaces its preview with the full-hub response', (tester) async {
|
|
final items = List.generate(205, (index) => _item(index, backend: MediaBackend.plex));
|
|
final harness = await _createHarness(items, backend: MediaBackend.plex);
|
|
|
|
await tester.pumpWidget(
|
|
harness.wrap(
|
|
HubDetailScreen(
|
|
hub: MediaHub(
|
|
id: '/hubs/home/recentlyAdded',
|
|
title: 'Recently Added',
|
|
type: 'movie',
|
|
items: items.take(5).toList(),
|
|
size: items.length,
|
|
more: true,
|
|
serverId: 'server_1',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(harness.client.requestedStarts, [0]);
|
|
expect(harness.client.fullHubRequests, 1);
|
|
expect(find.text(t.common.retry), findsNothing);
|
|
|
|
await tester.drag(find.byType(CustomScrollView), const Offset(0, -30000));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Item 204'), findsOneWidget);
|
|
});
|
|
}
|
|
|
|
MediaItem _item(int index, {required MediaBackend backend, String? libraryId}) => testMediaItem(
|
|
id: 'item_$index',
|
|
backend: backend,
|
|
kind: MediaKind.movie,
|
|
title: 'Item $index',
|
|
libraryId: libraryId,
|
|
serverId: 'server_1',
|
|
serverName: 'Server',
|
|
);
|
|
|
|
Future<_HubHarness> _createHarness(List<MediaItem> items, {required MediaBackend backend}) async {
|
|
await SettingsService.getInstance();
|
|
final client = _PagedHubClient(items, backend: backend);
|
|
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
|
final provider = testMultiServerProvider(manager);
|
|
addTearDown(provider.dispose);
|
|
return _HubHarness(client: client, provider: provider);
|
|
}
|
|
|
|
class _HubHarness {
|
|
const _HubHarness({required this.client, required this.provider});
|
|
|
|
final _PagedHubClient client;
|
|
final MultiServerProvider provider;
|
|
|
|
Widget wrap(Widget child) => TranslationProvider(
|
|
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
|
value: provider,
|
|
child: MaterialApp(
|
|
theme: monoTheme(dark: true),
|
|
home: SizedBox(width: 1280, height: 720, child: child),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _PagedHubClient implements MediaServerClient {
|
|
_PagedHubClient(this.items, {required this.backend});
|
|
|
|
final List<MediaItem> items;
|
|
final List<int?> requestedStarts = [];
|
|
final List<int?> requestedSizes = [];
|
|
int fullHubRequests = 0;
|
|
|
|
@override
|
|
final MediaBackend backend;
|
|
|
|
@override
|
|
ServerId get serverId => ServerId('server_1');
|
|
|
|
@override
|
|
String? get serverName => 'Server';
|
|
|
|
@override
|
|
ServerCapabilities get capabilities =>
|
|
backend == MediaBackend.plex ? ServerCapabilities.plex : ServerCapabilities.jellyfin;
|
|
|
|
@override
|
|
Future<LibraryPage<MediaItem>> fetchMoreHubItemsPage(
|
|
String hubId, {
|
|
int? start,
|
|
int? size,
|
|
AbortController? abort,
|
|
}) async {
|
|
requestedStarts.add(start);
|
|
requestedSizes.add(size);
|
|
return fakeLibraryPage(items, start: start, size: size);
|
|
}
|
|
|
|
@override
|
|
Future<List<MediaItem>> fetchMoreHubItems(String hubId, {int? limit}) async {
|
|
fullHubRequests++;
|
|
return List.unmodifiable(items);
|
|
}
|
|
|
|
@override
|
|
void close() {}
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|