jellyfin_dashboard_controller_test.dart 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import 'package:cardash_launcher/data/models/jellyfin_library.dart';
  2. import 'package:cardash_launcher/data/models/jellyfin_models.dart';
  3. import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
  4. import 'package:cardash_launcher/features/jellyfin/jellyfin_dashboard_controller.dart';
  5. import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
  6. import 'package:flutter_test/flutter_test.dart';
  7. import 'jellyfin_fakes.dart';
  8. class _Fixture {
  9. _Fixture({
  10. required this.jellyfin,
  11. required this.gateway,
  12. required this.library,
  13. required this.dashboard,
  14. });
  15. final JellyfinController jellyfin;
  16. final FakeJellyfinLibraryGateway gateway;
  17. final JellyfinLibraryService library;
  18. final JellyfinDashboardController dashboard;
  19. }
  20. Future<_Fixture> _fixture({List<String> genres = const []}) async {
  21. final jellyfin = await configuredJellyfinController();
  22. await jellyfin.setGenreSelections([
  23. for (final id in genres) JellyfinGenreSelection(id: id, label: id),
  24. ]);
  25. final gateway = FakeJellyfinLibraryGateway()
  26. ..played = [
  27. track(
  28. 'a2',
  29. albumId: 'album-a',
  30. albumName: 'Album A',
  31. lastPlayed: DateTime.utc(2026, 10, 31, 12),
  32. ),
  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. ]
  47. ..random = [album('r1'), album('r2'), album('r3'), album('r4'), album('r5')]
  48. ..genres = const ['Metal', 'Melodic Metal', 'Метъл', 'Jazz', 'Рок'];
  49. final library = testLibraryService(jellyfin, gateway);
  50. final dashboard = JellyfinDashboardController(
  51. library: library,
  52. jellyfin: jellyfin,
  53. );
  54. return _Fixture(
  55. jellyfin: jellyfin,
  56. gateway: gateway,
  57. library: library,
  58. dashboard: dashboard,
  59. );
  60. }
  61. void main() {
  62. test('loads three rows and expands only the selected genres', () async {
  63. final fixture = await _fixture(genres: ['metal', 'jazz']);
  64. addTearDown(fixture.dashboard.dispose);
  65. addTearDown(fixture.jellyfin.dispose);
  66. await fixture.dashboard.load();
  67. expect(fixture.dashboard.hasLoaded, isTrue);
  68. expect(fixture.dashboard.loading, isFalse);
  69. expect(fixture.dashboard.error, isNull);
  70. expect(fixture.dashboard.latestPlayed.length, 6);
  71. expect(fixture.dashboard.latestPlayed.first.id, 'album-a');
  72. expect(fixture.dashboard.latestAdded.length, 5);
  73. expect(fixture.dashboard.random.length, 5);
  74. expect(
  75. fixture.gateway.lastRandomGenres,
  76. containsAll(['Metal', 'Melodic Metal', 'Метъл', 'Jazz']),
  77. );
  78. expect(fixture.gateway.lastRandomGenres, isNot(contains('Рок')));
  79. });
  80. test('no selected genres means no random query', () async {
  81. final fixture = await _fixture();
  82. addTearDown(fixture.dashboard.dispose);
  83. addTearDown(fixture.jellyfin.dispose);
  84. await fixture.dashboard.load();
  85. expect(fixture.dashboard.hasGenres, isFalse);
  86. expect(fixture.dashboard.random, isEmpty);
  87. expect(fixture.gateway.randomCalls, 0);
  88. });
  89. test('shuffle replaces the random row', () async {
  90. final fixture = await _fixture(genres: ['metal']);
  91. addTearDown(fixture.dashboard.dispose);
  92. addTearDown(fixture.jellyfin.dispose);
  93. await fixture.dashboard.load();
  94. expect(fixture.dashboard.random.first.id, 'r1');
  95. fixture.gateway.random = [album('r9'), album('r10')];
  96. await fixture.dashboard.shuffle();
  97. expect(fixture.dashboard.random.map((entry) => entry.id), ['r9', 'r10']);
  98. expect(fixture.gateway.randomCalls, 2);
  99. expect(fixture.dashboard.randomLoading, isFalse);
  100. });
  101. test('markPlayed moves an album to the front and trims to five', () async {
  102. final fixture = await _fixture();
  103. addTearDown(fixture.dashboard.dispose);
  104. addTearDown(fixture.jellyfin.dispose);
  105. await fixture.dashboard.load();
  106. fixture.dashboard.markPlayed(album('album-a'));
  107. expect(fixture.dashboard.latestPlayed.first.id, 'album-a');
  108. expect(fixture.dashboard.latestPlayed.length, 6);
  109. });
  110. test('a server error is surfaced with retry state', () async {
  111. final fixture = await _fixture();
  112. addTearDown(fixture.dashboard.dispose);
  113. addTearDown(fixture.jellyfin.dispose);
  114. fixture.gateway.error = JellyfinUnreachable('offline');
  115. await fixture.dashboard.load();
  116. expect(fixture.dashboard.hasLoaded, isTrue);
  117. expect(fixture.dashboard.loading, isFalse);
  118. expect(fixture.dashboard.error, contains('unreachable'));
  119. });
  120. test('a random-only failure keeps the latest rows', () async {
  121. final fixture = await _fixture(genres: ['metal']);
  122. addTearDown(fixture.dashboard.dispose);
  123. addTearDown(fixture.jellyfin.dispose);
  124. fixture.gateway.randomError = JellyfinUnreachable('offline');
  125. await fixture.dashboard.load();
  126. expect(fixture.dashboard.error, isNull);
  127. expect(fixture.dashboard.randomError, contains('unreachable'));
  128. expect(fixture.dashboard.latestPlayed, isNotEmpty);
  129. expect(fixture.dashboard.latestAdded, isNotEmpty);
  130. expect(fixture.dashboard.random, isEmpty);
  131. });
  132. test('no connection reports a pairing error', () async {
  133. final fixture = await _fixture();
  134. addTearDown(fixture.dashboard.dispose);
  135. addTearDown(fixture.jellyfin.dispose);
  136. final unconfigured = JellyfinLibraryService(
  137. connectionProvider: () => null,
  138. gateway: fixture.gateway,
  139. );
  140. final dashboard = JellyfinDashboardController(
  141. library: unconfigured,
  142. jellyfin: fixture.jellyfin,
  143. );
  144. addTearDown(dashboard.dispose);
  145. await dashboard.load();
  146. expect(dashboard.hasLoaded, isTrue);
  147. expect(dashboard.error, contains('Pair Jellyfin'));
  148. });
  149. }