From a9b812f66cdfbf491fe4a7f75e23153f5ce492c7 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:34:44 +0200 Subject: [PATCH] fix(test): restore CI unit tests --- test/screens/media_detail_screen_test.dart | 324 ++++++++++++------ .../profile/profile_switch_screen_test.dart | 5 +- .../focusable_popup_menu_button_test.dart | 2 + 3 files changed, 227 insertions(+), 104 deletions(-) diff --git a/test/screens/media_detail_screen_test.dart b/test/screens/media_detail_screen_test.dart index 0f4adc13..4e185e20 100644 --- a/test/screens/media_detail_screen_test.dart +++ b/test/screens/media_detail_screen_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:plezy/media/ids.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/i18n/strings.g.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/media_server_http_client.dart'; import 'package:plezy/utils/platform_detector.dart'; +import 'package:plezy/widgets/tv_browse_rail.dart'; import 'package:provider/provider.dart'; import '../test_helpers/prefs.dart'; @@ -229,103 +231,7 @@ void main() { expect(summaryText.style?.color, theme.colorScheme.onSurface.withValues(alpha: 0.78)); }); - testWidgets('TV detail reveals selected season before remaining episode caches load', (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>(); - 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.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 { + testWidgets('TV detail shows every season tab and prefetches adjacent first page', (tester) async { await SettingsService.getInstance(); final show = MediaItem( @@ -388,7 +294,101 @@ void main() { season1.id: [episode1], 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.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 provider = MultiServerProvider(manager, DataAggregationService(manager)); @@ -414,19 +414,122 @@ void main() { expect(find.text('Season 1'), 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>(); + 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.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(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 { final MediaItem show; final Map> childrenByParent; + final Map>> childrenPageFutures; + final Map childrenPageErrors; final Future>? pendingPlayableDescendants; - final Object? playableDescendantsError; + final childrenPageCalls = <({String parentId, int? start, int? size})>[]; _FakeMediaServerClient({ required this.show, required this.childrenByParent, + this.childrenPageFutures = const {}, + this.childrenPageErrors = const {}, this.pendingPlayableDescendants, - this.playableDescendantsError, }); @override @@ -448,6 +551,25 @@ class _FakeMediaServerClient implements MediaServerClient { return childrenByParent[parentId] ?? const []; } + @override + Future> 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 [])); + final offset = start ?? 0; + final limit = size ?? all.length; + final end = (offset + limit).clamp(0, all.length).toInt(); + final items = offset >= all.length ? const [] : all.sublist(offset, end); + return LibraryPage(items: items, totalCount: all.length, offset: offset); + } + @override Future> fetchPlayableDescendantsPage( String parentId, { @@ -455,8 +577,6 @@ class _FakeMediaServerClient implements MediaServerClient { int? size, AbortController? abort, }) async { - final error = playableDescendantsError; - if (error != null) throw error; final items = await pendingPlayableDescendants!; return LibraryPage(items: items, totalCount: items.length, offset: start ?? 0); } diff --git a/test/screens/profile/profile_switch_screen_test.dart b/test/screens/profile/profile_switch_screen_test.dart index a5f737f4..fced9f21 100644 --- a/test/screens/profile/profile_switch_screen_test.dart +++ b/test/screens/profile/profile_switch_screen_test.dart @@ -14,6 +14,7 @@ import 'package:plezy/profiles/profile_connection_registry.dart'; import 'package:plezy/profiles/profile_registry.dart'; import 'package:plezy/screens/profile/profile_switch_screen.dart'; import 'package:plezy/services/storage_service.dart'; +import 'package:plezy/theme/mono_theme.dart'; import 'package:provider/provider.dart'; import '../../test_helpers/prefs.dart'; @@ -61,7 +62,7 @@ void main() { Provider.value(value: plexHome), ChangeNotifierProvider.value(value: activeProfile), ], - child: const MaterialApp(home: ProfileSwitchScreen()), + child: MaterialApp(theme: monoTheme(dark: true), home: const ProfileSwitchScreen()), ), ), ); @@ -123,7 +124,7 @@ void main() { Provider.value(value: plexHome), ChangeNotifierProvider.value(value: activeProfile), ], - child: const MaterialApp(home: ProfileSwitchScreen()), + child: MaterialApp(theme: monoTheme(dark: true), home: const ProfileSwitchScreen()), ), ), ); diff --git a/test/widgets/focusable_popup_menu_button_test.dart b/test/widgets/focusable_popup_menu_button_test.dart index c905dd38..0224cc71 100644 --- a/test/widgets/focusable_popup_menu_button_test.dart +++ b/test/widgets/focusable_popup_menu_button_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.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/focusable_popup_menu_button.dart'; @@ -11,6 +12,7 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: monoTheme(dark: true), home: Scaffold( body: Center( child: FocusablePopupMenuButton(