fix(test): restore CI unit tests
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:plezy/media/ids.dart';
|
import 'package:plezy/media/ids.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:plezy/i18n/strings.g.dart';
|
import 'package:plezy/i18n/strings.g.dart';
|
||||||
import 'package:plezy/media/library_query.dart';
|
import 'package:plezy/media/library_query.dart';
|
||||||
@@ -19,6 +20,7 @@ import 'package:plezy/theme/mono_theme.dart';
|
|||||||
import 'package:plezy/utils/layout_constants.dart';
|
import 'package:plezy/utils/layout_constants.dart';
|
||||||
import 'package:plezy/utils/media_server_http_client.dart';
|
import 'package:plezy/utils/media_server_http_client.dart';
|
||||||
import 'package:plezy/utils/platform_detector.dart';
|
import 'package:plezy/utils/platform_detector.dart';
|
||||||
|
import 'package:plezy/widgets/tv_browse_rail.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../test_helpers/prefs.dart';
|
import '../test_helpers/prefs.dart';
|
||||||
@@ -229,103 +231,7 @@ void main() {
|
|||||||
expect(summaryText.style?.color, theme.colorScheme.onSurface.withValues(alpha: 0.78));
|
expect(summaryText.style?.color, theme.colorScheme.onSurface.withValues(alpha: 0.78));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('TV detail reveals selected season before remaining episode caches load', (tester) async {
|
testWidgets('TV detail shows every season tab and prefetches adjacent first page', (tester) async {
|
||||||
await SettingsService.getInstance();
|
|
||||||
|
|
||||||
final show = MediaItem(
|
|
||||||
id: 'show_1',
|
|
||||||
backend: MediaBackend.jellyfin,
|
|
||||||
kind: MediaKind.show,
|
|
||||||
title: 'The Show',
|
|
||||||
serverId: 'server_1',
|
|
||||||
serverName: 'Server',
|
|
||||||
);
|
|
||||||
final season1 = MediaItem(
|
|
||||||
id: 'season_1',
|
|
||||||
backend: MediaBackend.jellyfin,
|
|
||||||
kind: MediaKind.season,
|
|
||||||
title: 'Season 1',
|
|
||||||
index: 1,
|
|
||||||
parentId: show.id,
|
|
||||||
serverId: show.serverId,
|
|
||||||
serverName: show.serverName,
|
|
||||||
);
|
|
||||||
final season2 = MediaItem(
|
|
||||||
id: 'season_2',
|
|
||||||
backend: MediaBackend.jellyfin,
|
|
||||||
kind: MediaKind.season,
|
|
||||||
title: 'Season 2',
|
|
||||||
index: 2,
|
|
||||||
parentId: show.id,
|
|
||||||
serverId: show.serverId,
|
|
||||||
serverName: show.serverName,
|
|
||||||
);
|
|
||||||
final episode1 = MediaItem(
|
|
||||||
id: 'episode_1',
|
|
||||||
backend: MediaBackend.jellyfin,
|
|
||||||
kind: MediaKind.episode,
|
|
||||||
title: 'Episode 1',
|
|
||||||
index: 1,
|
|
||||||
parentId: season1.id,
|
|
||||||
parentIndex: season1.index,
|
|
||||||
grandparentId: show.id,
|
|
||||||
serverId: show.serverId,
|
|
||||||
serverName: show.serverName,
|
|
||||||
);
|
|
||||||
final episode2 = MediaItem(
|
|
||||||
id: 'episode_2',
|
|
||||||
backend: MediaBackend.jellyfin,
|
|
||||||
kind: MediaKind.episode,
|
|
||||||
title: 'Episode 2',
|
|
||||||
index: 1,
|
|
||||||
parentId: season2.id,
|
|
||||||
parentIndex: season2.index,
|
|
||||||
grandparentId: show.id,
|
|
||||||
serverId: show.serverId,
|
|
||||||
serverName: show.serverName,
|
|
||||||
);
|
|
||||||
|
|
||||||
final descendantsCompleter = Completer<List<MediaItem>>();
|
|
||||||
final client = _FakeMediaServerClient(
|
|
||||||
show: show,
|
|
||||||
childrenByParent: {
|
|
||||||
show.id: [season1, season2],
|
|
||||||
},
|
|
||||||
pendingPlayableDescendants: descendantsCompleter.future,
|
|
||||||
);
|
|
||||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
|
||||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
|
||||||
addTearDown(provider.dispose);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
TranslationProvider(
|
|
||||||
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
|
||||||
value: provider,
|
|
||||||
child: MaterialApp(
|
|
||||||
theme: monoTheme(dark: true),
|
|
||||||
home: SizedBox(width: 1280, height: 720, child: MediaDetailScreen(metadata: show)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
|
|
||||||
expect(find.text('Season 1'), findsOneWidget);
|
|
||||||
expect(find.text('Season 2'), findsNothing);
|
|
||||||
|
|
||||||
descendantsCompleter.complete([episode1, episode2]);
|
|
||||||
await tester.pump();
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
|
|
||||||
expect(find.text('Season 1'), findsOneWidget);
|
|
||||||
expect(find.text('Season 2'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('TV detail falls back to per-season episodes when descendant cache fails', (tester) async {
|
|
||||||
await SettingsService.getInstance();
|
await SettingsService.getInstance();
|
||||||
|
|
||||||
final show = MediaItem(
|
final show = MediaItem(
|
||||||
@@ -388,7 +294,101 @@ void main() {
|
|||||||
season1.id: [episode1],
|
season1.id: [episode1],
|
||||||
season2.id: [episode2],
|
season2.id: [episode2],
|
||||||
},
|
},
|
||||||
playableDescendantsError: Exception('descendant cache failed'),
|
);
|
||||||
|
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||||
|
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||||
|
addTearDown(provider.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
TranslationProvider(
|
||||||
|
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
||||||
|
value: provider,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: SizedBox(width: 1280, height: 720, child: MediaDetailScreen(metadata: show)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
// Every season tab is derived from the season list, so both appear
|
||||||
|
// immediately. TV warms only the selected first page plus the adjacent first
|
||||||
|
// page; it still does not walk the whole show or load page 2+.
|
||||||
|
expect(find.text('Season 1'), findsOneWidget);
|
||||||
|
expect(find.text('Season 2'), findsOneWidget);
|
||||||
|
expect(client.childrenPageCalls.map((call) => call.parentId), containsAll([season1.id, season2.id]));
|
||||||
|
expect(client.childrenPageCalls.every((call) => call.start == 0 && call.size == 200), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TV detail keeps every season tab when a season episode load fails', (tester) async {
|
||||||
|
await SettingsService.getInstance();
|
||||||
|
|
||||||
|
final show = MediaItem(
|
||||||
|
id: 'show_1',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.show,
|
||||||
|
title: 'The Show',
|
||||||
|
serverId: 'server_1',
|
||||||
|
serverName: 'Server',
|
||||||
|
);
|
||||||
|
final season1 = MediaItem(
|
||||||
|
id: 'season_1',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.season,
|
||||||
|
title: 'Season 1',
|
||||||
|
index: 1,
|
||||||
|
parentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final season2 = MediaItem(
|
||||||
|
id: 'season_2',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.season,
|
||||||
|
title: 'Season 2',
|
||||||
|
index: 2,
|
||||||
|
parentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final episode1 = MediaItem(
|
||||||
|
id: 'episode_1',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.episode,
|
||||||
|
title: 'Episode 1',
|
||||||
|
index: 1,
|
||||||
|
parentId: season1.id,
|
||||||
|
parentIndex: season1.index,
|
||||||
|
grandparentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final episode2 = MediaItem(
|
||||||
|
id: 'episode_2',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.episode,
|
||||||
|
title: 'Episode 2',
|
||||||
|
index: 1,
|
||||||
|
parentId: season2.id,
|
||||||
|
parentIndex: season2.index,
|
||||||
|
grandparentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final client = _FakeMediaServerClient(
|
||||||
|
show: show,
|
||||||
|
childrenByParent: {
|
||||||
|
show.id: [season1, season2],
|
||||||
|
season1.id: [episode1],
|
||||||
|
season2.id: [episode2],
|
||||||
|
},
|
||||||
|
childrenPageErrors: {season1.id: Exception('season cache failed')},
|
||||||
);
|
);
|
||||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||||
@@ -414,19 +414,122 @@ void main() {
|
|||||||
expect(find.text('Season 1'), findsOneWidget);
|
expect(find.text('Season 1'), findsOneWidget);
|
||||||
expect(find.text('Season 2'), findsOneWidget);
|
expect(find.text('Season 2'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('TV detail completes adjacent prefetch after focus moves to that season', (tester) async {
|
||||||
|
await SettingsService.getInstance();
|
||||||
|
|
||||||
|
final show = MediaItem(
|
||||||
|
id: 'show_1',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.show,
|
||||||
|
title: 'The Show',
|
||||||
|
serverId: 'server_1',
|
||||||
|
serverName: 'Server',
|
||||||
|
);
|
||||||
|
final season1 = MediaItem(
|
||||||
|
id: 'season_1',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.season,
|
||||||
|
title: 'Season 1',
|
||||||
|
index: 1,
|
||||||
|
parentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final season2 = MediaItem(
|
||||||
|
id: 'season_2',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.season,
|
||||||
|
title: 'Season 2',
|
||||||
|
index: 2,
|
||||||
|
parentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final episode1 = MediaItem(
|
||||||
|
id: 'episode_1',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.episode,
|
||||||
|
title: 'Episode 1',
|
||||||
|
index: 1,
|
||||||
|
parentId: season1.id,
|
||||||
|
parentIndex: season1.index,
|
||||||
|
grandparentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final episode2 = MediaItem(
|
||||||
|
id: 'episode_2',
|
||||||
|
backend: MediaBackend.jellyfin,
|
||||||
|
kind: MediaKind.episode,
|
||||||
|
title: 'Episode 2',
|
||||||
|
index: 1,
|
||||||
|
parentId: season2.id,
|
||||||
|
parentIndex: season2.index,
|
||||||
|
grandparentId: show.id,
|
||||||
|
serverId: show.serverId,
|
||||||
|
serverName: show.serverName,
|
||||||
|
);
|
||||||
|
final season2Completer = Completer<List<MediaItem>>();
|
||||||
|
final client = _FakeMediaServerClient(
|
||||||
|
show: show,
|
||||||
|
childrenByParent: {
|
||||||
|
show.id: [season1, season2],
|
||||||
|
season1.id: [episode1],
|
||||||
|
},
|
||||||
|
childrenPageFutures: {season2.id: season2Completer.future},
|
||||||
|
);
|
||||||
|
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||||
|
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||||
|
addTearDown(provider.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
TranslationProvider(
|
||||||
|
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
||||||
|
value: provider,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: SizedBox(width: 1280, height: 720, child: MediaDetailScreen(metadata: show)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
tester.state<TvBrowseRailState>(find.byType(TvBrowseRail)).requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Episode 2'), findsNothing);
|
||||||
|
|
||||||
|
season2Completer.complete([episode2]);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
expect(find.text('Episode 2'), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FakeMediaServerClient implements MediaServerClient {
|
class _FakeMediaServerClient implements MediaServerClient {
|
||||||
final MediaItem show;
|
final MediaItem show;
|
||||||
final Map<String, List<MediaItem>> childrenByParent;
|
final Map<String, List<MediaItem>> childrenByParent;
|
||||||
|
final Map<String, Future<List<MediaItem>>> childrenPageFutures;
|
||||||
|
final Map<String, Object> childrenPageErrors;
|
||||||
final Future<List<MediaItem>>? pendingPlayableDescendants;
|
final Future<List<MediaItem>>? pendingPlayableDescendants;
|
||||||
final Object? playableDescendantsError;
|
final childrenPageCalls = <({String parentId, int? start, int? size})>[];
|
||||||
|
|
||||||
_FakeMediaServerClient({
|
_FakeMediaServerClient({
|
||||||
required this.show,
|
required this.show,
|
||||||
required this.childrenByParent,
|
required this.childrenByParent,
|
||||||
|
this.childrenPageFutures = const {},
|
||||||
|
this.childrenPageErrors = const {},
|
||||||
this.pendingPlayableDescendants,
|
this.pendingPlayableDescendants,
|
||||||
this.playableDescendantsError,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -448,6 +551,25 @@ class _FakeMediaServerClient implements MediaServerClient {
|
|||||||
return childrenByParent[parentId] ?? const [];
|
return childrenByParent[parentId] ?? const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LibraryPage<MediaItem>> fetchChildrenPage(
|
||||||
|
String parentId, {
|
||||||
|
int? start,
|
||||||
|
int? size,
|
||||||
|
AbortController? abort,
|
||||||
|
}) async {
|
||||||
|
childrenPageCalls.add((parentId: parentId, start: start, size: size));
|
||||||
|
final error = childrenPageErrors[parentId];
|
||||||
|
if (error != null) throw error;
|
||||||
|
final all =
|
||||||
|
await (childrenPageFutures[parentId] ?? Future.value(childrenByParent[parentId] ?? const <MediaItem>[]));
|
||||||
|
final offset = start ?? 0;
|
||||||
|
final limit = size ?? all.length;
|
||||||
|
final end = (offset + limit).clamp(0, all.length).toInt();
|
||||||
|
final items = offset >= all.length ? const <MediaItem>[] : all.sublist(offset, end);
|
||||||
|
return LibraryPage(items: items, totalCount: all.length, offset: offset);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LibraryPage<MediaItem>> fetchPlayableDescendantsPage(
|
Future<LibraryPage<MediaItem>> fetchPlayableDescendantsPage(
|
||||||
String parentId, {
|
String parentId, {
|
||||||
@@ -455,8 +577,6 @@ class _FakeMediaServerClient implements MediaServerClient {
|
|||||||
int? size,
|
int? size,
|
||||||
AbortController? abort,
|
AbortController? abort,
|
||||||
}) async {
|
}) async {
|
||||||
final error = playableDescendantsError;
|
|
||||||
if (error != null) throw error;
|
|
||||||
final items = await pendingPlayableDescendants!;
|
final items = await pendingPlayableDescendants!;
|
||||||
return LibraryPage(items: items, totalCount: items.length, offset: start ?? 0);
|
return LibraryPage(items: items, totalCount: items.length, offset: start ?? 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:plezy/profiles/profile_connection_registry.dart';
|
|||||||
import 'package:plezy/profiles/profile_registry.dart';
|
import 'package:plezy/profiles/profile_registry.dart';
|
||||||
import 'package:plezy/screens/profile/profile_switch_screen.dart';
|
import 'package:plezy/screens/profile/profile_switch_screen.dart';
|
||||||
import 'package:plezy/services/storage_service.dart';
|
import 'package:plezy/services/storage_service.dart';
|
||||||
|
import 'package:plezy/theme/mono_theme.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../test_helpers/prefs.dart';
|
import '../../test_helpers/prefs.dart';
|
||||||
@@ -61,7 +62,7 @@ void main() {
|
|||||||
Provider<PlexHomeService>.value(value: plexHome),
|
Provider<PlexHomeService>.value(value: plexHome),
|
||||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfile),
|
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfile),
|
||||||
],
|
],
|
||||||
child: const MaterialApp(home: ProfileSwitchScreen()),
|
child: MaterialApp(theme: monoTheme(dark: true), home: const ProfileSwitchScreen()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -123,7 +124,7 @@ void main() {
|
|||||||
Provider<PlexHomeService>.value(value: plexHome),
|
Provider<PlexHomeService>.value(value: plexHome),
|
||||||
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfile),
|
ChangeNotifierProvider<ActiveProfileProvider>.value(value: activeProfile),
|
||||||
],
|
],
|
||||||
child: const MaterialApp(home: ProfileSwitchScreen()),
|
child: MaterialApp(theme: monoTheme(dark: true), home: const ProfileSwitchScreen()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:plezy/theme/mono_theme.dart';
|
||||||
import 'package:plezy/widgets/app_menu.dart';
|
import 'package:plezy/widgets/app_menu.dart';
|
||||||
import 'package:plezy/widgets/focusable_popup_menu_button.dart';
|
import 'package:plezy/widgets/focusable_popup_menu_button.dart';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: FocusablePopupMenuButton<String>(
|
child: FocusablePopupMenuButton<String>(
|
||||||
|
|||||||
Reference in New Issue
Block a user