jellyfin_dashboard_screen_test.dart 8.2 KB

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