jellyfin_dashboard_screen_test.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import 'package:cardash_launcher/core/theme/app_theme.dart';
  2. import 'package:cardash_launcher/data/models/jellyfin_library.dart';
  3. import 'package:cardash_launcher/data/models/jellyfin_models.dart';
  4. import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
  5. import 'package:cardash_launcher/features/home/system_controller.dart';
  6. import 'package:cardash_launcher/features/jellyfin/jellyfin_dashboard_screen.dart';
  7. import 'package:cardash_launcher/features/jellyfin/jellyfin_playback_controller.dart';
  8. import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
  9. import 'package:flutter/material.dart';
  10. import 'package:flutter_test/flutter_test.dart';
  11. import 'package:provider/provider.dart';
  12. import 'jellyfin_fakes.dart';
  13. class _Fixture {
  14. _Fixture({
  15. required this.jellyfin,
  16. required this.gateway,
  17. required this.library,
  18. required this.playback,
  19. });
  20. final JellyfinController jellyfin;
  21. final FakeJellyfinLibraryGateway gateway;
  22. final JellyfinLibraryService library;
  23. final JellyfinPlaybackController playback;
  24. }
  25. Future<_Fixture> _fixture({List<String> genres = const ['metal']}) async {
  26. final jellyfin = await configuredJellyfinController();
  27. await jellyfin.setGenreSelections([
  28. for (final id in genres) JellyfinGenreSelection(id: id, label: id),
  29. ]);
  30. final gateway = FakeJellyfinLibraryGateway()
  31. ..played = [
  32. track('a2', albumId: 'album-a', albumName: 'Album A'),
  33. track('a1', albumId: 'album-a', albumName: 'Album A'),
  34. track('b1', albumId: 'album-b', albumName: 'Album B'),
  35. track('c1', albumId: 'album-c', albumName: 'Album C'),
  36. track('d1', albumId: 'album-d', albumName: 'Album D'),
  37. track('e1', albumId: 'album-e', albumName: 'Album E'),
  38. track('f1', albumId: 'album-f', albumName: 'Album F'),
  39. ]
  40. ..added = [
  41. album('new-1'),
  42. album('new-2'),
  43. album('new-3'),
  44. album('new-4'),
  45. album('new-5'),
  46. album('new-6'),
  47. ]
  48. ..random = [
  49. album('r1'),
  50. album('r2'),
  51. album('r3'),
  52. album('r4'),
  53. album('r5'),
  54. album('r6'),
  55. ]
  56. ..genres = const ['Metal', 'Jazz'];
  57. final library = testLibraryService(jellyfin, gateway);
  58. final playback = JellyfinPlaybackController(
  59. FakeJellyfinPlaybackPlatform(),
  60. library,
  61. jellyfin,
  62. pollInterval: const Duration(hours: 1),
  63. );
  64. return _Fixture(
  65. jellyfin: jellyfin,
  66. gateway: gateway,
  67. library: library,
  68. playback: playback,
  69. );
  70. }
  71. Future<void> _pump(
  72. WidgetTester tester,
  73. _Fixture fixture, {
  74. JellyfinAlbum? openAlbum,
  75. }) async {
  76. await tester.binding.setSurfaceSize(const Size(1280, 800));
  77. addTearDown(() => tester.binding.setSurfaceSize(null));
  78. addTearDown(fixture.playback.dispose);
  79. addTearDown(fixture.jellyfin.dispose);
  80. final system = SystemController(FakeSystemPlatform());
  81. addTearDown(system.dispose);
  82. await tester.pumpWidget(
  83. MultiProvider(
  84. providers: [
  85. Provider<JellyfinLibraryService>.value(value: fixture.library),
  86. ChangeNotifierProvider<JellyfinController>.value(
  87. value: fixture.jellyfin,
  88. ),
  89. ChangeNotifierProvider<JellyfinPlaybackController>.value(
  90. value: fixture.playback,
  91. ),
  92. ChangeNotifierProvider<SystemController>.value(value: system),
  93. ],
  94. child: MaterialApp(
  95. theme: AppTheme.light(),
  96. home: JellyfinDashboardScreen(openAlbum: openAlbum),
  97. ),
  98. ),
  99. );
  100. await tester.pumpAndSettle();
  101. }
  102. void main() {
  103. testWidgets('renders six cards per row on a wide screen', (tester) async {
  104. final fixture = await _fixture();
  105. await _pump(tester, fixture);
  106. expect(find.text('Recently played'), findsOneWidget);
  107. expect(find.text('Recently added'), findsOneWidget);
  108. expect(find.text('Random'), findsOneWidget);
  109. expect(find.text('Artists'), findsOneWidget);
  110. expect(find.text('Album A'), findsWidgets);
  111. expect(find.text('Album F'), findsOneWidget);
  112. expect(find.text('Album new-1'), findsOneWidget);
  113. expect(find.text('Album new-6'), findsOneWidget);
  114. expect(find.text('Album r1'), findsOneWidget);
  115. expect(find.text('Album r6'), findsOneWidget);
  116. expect(find.bySemanticsLabel('Shuffle random albums'), findsOneWidget);
  117. expect(tester.takeException(), isNull);
  118. });
  119. testWidgets('shows the genre prompt when no genres are selected', (
  120. tester,
  121. ) async {
  122. final fixture = await _fixture(genres: const []);
  123. await _pump(tester, fixture);
  124. expect(find.text('Choose up to 5 genres in Settings.'), findsOneWidget);
  125. expect(find.bySemanticsLabel('Shuffle random albums'), findsNothing);
  126. expect(fixture.gateway.randomCalls, 0);
  127. });
  128. testWidgets('shows an inline error with retry when the server fails', (
  129. tester,
  130. ) async {
  131. final fixture = await _fixture();
  132. fixture.gateway.error = JellyfinUnreachable('offline');
  133. await _pump(tester, fixture);
  134. expect(
  135. find.text('Jellyfin is unreachable. Check the server and try again.'),
  136. findsOneWidget,
  137. );
  138. expect(find.text('Retry'), findsOneWidget);
  139. expect(find.bySemanticsLabel('Shuffle random albums'), findsNothing);
  140. });
  141. testWidgets('favorites tab shows a scrollable album grid', (tester) async {
  142. final fixture = await _fixture();
  143. fixture.gateway.favorites = [
  144. album('fav-1'),
  145. album('fav-2'),
  146. album('fav-3'),
  147. ];
  148. await _pump(tester, fixture);
  149. await tester.tap(find.text('Favorites'));
  150. await tester.pumpAndSettle();
  151. expect(find.text('Album fav-1'), findsOneWidget);
  152. expect(find.text('Album fav-2'), findsOneWidget);
  153. expect(find.text('Album fav-3'), findsOneWidget);
  154. expect(find.text('Recently played'), findsNothing);
  155. expect(fixture.gateway.favoriteCalls, 1);
  156. expect(tester.takeException(), isNull);
  157. });
  158. testWidgets('favorites tab shows No favorites when empty', (tester) async {
  159. final fixture = await _fixture();
  160. await _pump(tester, fixture);
  161. await tester.tap(find.text('Favorites'));
  162. await tester.pumpAndSettle();
  163. expect(find.text('No favorites'), findsOneWidget);
  164. expect(find.text('Recently played'), findsNothing);
  165. expect(tester.takeException(), isNull);
  166. });
  167. testWidgets('openAlbum opens the album page and Back returns to Albums', (
  168. tester,
  169. ) async {
  170. final fixture = await _fixture();
  171. fixture.gateway.tracks = {
  172. 'album-a': [track('a1', albumId: 'album-a', albumName: 'Album A')],
  173. };
  174. await _pump(tester, fixture, openAlbum: album('album-a'));
  175. expect(find.text('Album album-a'), findsWidgets);
  176. expect(find.text('Tracks'), findsOneWidget);
  177. await tester.tap(find.bySemanticsLabel('Back').last);
  178. await tester.pumpAndSettle();
  179. expect(find.text('Recently played'), findsOneWidget);
  180. expect(find.text('Albums'), findsOneWidget);
  181. expect(find.text('Favorites'), findsOneWidget);
  182. expect(tester.takeException(), isNull);
  183. });
  184. }