jellyfin_playback_controller_test.dart 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import 'dart:typed_data';
  2. import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
  3. import 'package:cardash_launcher/features/jellyfin/jellyfin_playback_controller.dart';
  4. import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
  5. import 'package:flutter_test/flutter_test.dart';
  6. import 'jellyfin_fakes.dart';
  7. class _Fixture {
  8. _Fixture({
  9. required this.jellyfin,
  10. required this.gateway,
  11. required this.library,
  12. required this.platform,
  13. required this.playback,
  14. });
  15. final JellyfinController jellyfin;
  16. final FakeJellyfinLibraryGateway gateway;
  17. final JellyfinLibraryService library;
  18. final FakeJellyfinPlaybackPlatform platform;
  19. final JellyfinPlaybackController playback;
  20. }
  21. Future<_Fixture> _fixture() async {
  22. final jellyfin = await configuredJellyfinController();
  23. final gateway = FakeJellyfinLibraryGateway()
  24. ..tracks = {
  25. 'album-1': [
  26. track('t1', albumId: 'album-1', albumName: 'Album 1', index: 1),
  27. track('t2', albumId: 'album-1', albumName: 'Album 1', index: 2),
  28. track('t3', albumId: 'album-1', albumName: 'Album 1', index: 3),
  29. ],
  30. 'album-2': [
  31. track('u1', albumId: 'album-2', albumName: 'Album 2', index: 1),
  32. track('u2', albumId: 'album-2', albumName: 'Album 2', index: 2),
  33. ],
  34. }
  35. ..artwork = Uint8List.fromList([1, 2, 3]);
  36. final library = testLibraryService(jellyfin, gateway);
  37. final platform = FakeJellyfinPlaybackPlatform();
  38. final playback = JellyfinPlaybackController(
  39. platform,
  40. library,
  41. jellyfin,
  42. pollInterval: const Duration(hours: 1),
  43. );
  44. return _Fixture(
  45. jellyfin: jellyfin,
  46. gateway: gateway,
  47. library: library,
  48. platform: platform,
  49. playback: playback,
  50. );
  51. }
  52. void main() {
  53. test('starts an album and exposes the current track', () async {
  54. final fixture = await _fixture();
  55. addTearDown(fixture.playback.dispose);
  56. addTearDown(fixture.jellyfin.dispose);
  57. final error = await fixture.playback.playAlbum(album('album-1'));
  58. expect(error, isNull);
  59. expect(fixture.platform.setQueueCalls, 1);
  60. expect(fixture.playback.isActive, isTrue);
  61. expect(fixture.playback.playing, isTrue);
  62. expect(fixture.playback.currentItem?.trackId, 't1');
  63. expect(fixture.playback.mediaPlayback.title, 'Track t1');
  64. expect(fixture.playback.queueItems.length, 3);
  65. expect(fixture.platform.queue?.artwork, [1, 2, 3]);
  66. expect(
  67. fixture.platform.queue?.authorization,
  68. contains('Token="test-token"'),
  69. );
  70. });
  71. test('the same album is not rebuilt but a different album is', () async {
  72. final fixture = await _fixture();
  73. addTearDown(fixture.playback.dispose);
  74. addTearDown(fixture.jellyfin.dispose);
  75. await fixture.playback.playAlbum(album('album-1'));
  76. await fixture.playback.playAlbum(album('album-1'));
  77. expect(fixture.platform.setQueueCalls, 1);
  78. await fixture.playback.playAlbum(album('album-2'));
  79. expect(fixture.platform.setQueueCalls, 2);
  80. expect(fixture.playback.currentItem?.trackId, 'u1');
  81. });
  82. test('toggle pauses and resumes without losing the position', () async {
  83. final fixture = await _fixture();
  84. addTearDown(fixture.playback.dispose);
  85. addTearDown(fixture.jellyfin.dispose);
  86. await fixture.playback.playAlbum(album('album-1'));
  87. await fixture.playback.toggle();
  88. expect(fixture.playback.playing, isFalse);
  89. expect(fixture.platform.commands, contains('pause'));
  90. await fixture.playback.toggle();
  91. expect(fixture.playback.playing, isTrue);
  92. expect(fixture.platform.commands, contains('play'));
  93. });
  94. test('next and previous move through the queue', () async {
  95. final fixture = await _fixture();
  96. addTearDown(fixture.playback.dispose);
  97. addTearDown(fixture.jellyfin.dispose);
  98. await fixture.playback.playAlbum(album('album-1'));
  99. await fixture.playback.next();
  100. expect(fixture.playback.currentItem?.trackId, 't2');
  101. expect(fixture.playback.canGoPrevious, isTrue);
  102. await fixture.playback.previous();
  103. expect(fixture.playback.currentItem?.trackId, 't1');
  104. });
  105. test('selecting a track seeks to its queue index', () async {
  106. final fixture = await _fixture();
  107. addTearDown(fixture.playback.dispose);
  108. addTearDown(fixture.jellyfin.dispose);
  109. await fixture.playback.playAlbum(album('album-1'));
  110. await fixture.playback.playTrackAt(2);
  111. expect(fixture.platform.commands, contains('seekToIndex:2'));
  112. expect(fixture.playback.currentItem?.trackId, 't3');
  113. expect(fixture.playback.playing, isTrue);
  114. });
  115. test('tapping play after the end restarts at track one', () async {
  116. final fixture = await _fixture();
  117. addTearDown(fixture.playback.dispose);
  118. addTearDown(fixture.jellyfin.dispose);
  119. await fixture.playback.playAlbum(album('album-1'));
  120. fixture.platform.state = fixture.platform.state.copyWith(
  121. playing: false,
  122. positionMs: fixture.platform.state.durationMs,
  123. );
  124. await fixture.playback.restore();
  125. await fixture.playback.toggle();
  126. expect(fixture.platform.commands, contains('seekToIndex:0'));
  127. expect(fixture.playback.playing, isTrue);
  128. expect(fixture.playback.snapshot.positionMs, 0);
  129. });
  130. test('stop clears the queue and snapshot', () async {
  131. final fixture = await _fixture();
  132. addTearDown(fixture.playback.dispose);
  133. addTearDown(fixture.jellyfin.dispose);
  134. await fixture.playback.playAlbum(album('album-1'));
  135. await fixture.playback.stop();
  136. expect(fixture.playback.isActive, isFalse);
  137. expect(fixture.playback.queueItems, isEmpty);
  138. expect(fixture.playback.snapshot.available, isFalse);
  139. });
  140. test('notification permission delegates to the platform', () async {
  141. final fixture = await _fixture();
  142. addTearDown(fixture.playback.dispose);
  143. addTearDown(fixture.jellyfin.dispose);
  144. expect(fixture.platform.permissionGranted, isFalse);
  145. expect(await fixture.playback.notificationPermissionGranted(), isFalse);
  146. expect(await fixture.playback.requestNotificationPermission(), isTrue);
  147. expect(fixture.platform.permissionGranted, isTrue);
  148. });
  149. test('media progress falls back to the queued track duration', () async {
  150. final fixture = await _fixture();
  151. addTearDown(fixture.playback.dispose);
  152. addTearDown(fixture.jellyfin.dispose);
  153. await fixture.playback.playAlbum(album('album-1'));
  154. fixture.platform.zeroDuration = true;
  155. fixture.platform.state = fixture.platform.snapshotAt(
  156. 0,
  157. playing: true,
  158. positionMs: 30000,
  159. );
  160. await fixture.playback.toggle();
  161. expect(fixture.playback.snapshot.durationMs, 0);
  162. final media = fixture.playback.mediaPlayback;
  163. expect(media.durationMs, fixture.playback.currentItem!.durationMs);
  164. expect(media.positionMs, 30000);
  165. expect(media.progress, greaterThan(0));
  166. });
  167. test('a failed queue start surfaces an error', () async {
  168. final fixture = await _fixture();
  169. addTearDown(fixture.playback.dispose);
  170. addTearDown(fixture.jellyfin.dispose);
  171. fixture.platform.setQueueResult = false;
  172. final error = await fixture.playback.playAlbum(album('album-1'));
  173. expect(error, isNotNull);
  174. expect(fixture.playback.error, isNotNull);
  175. expect(fixture.playback.isActive, isFalse);
  176. });
  177. test('an album with no playable tracks reports an error', () async {
  178. final fixture = await _fixture();
  179. addTearDown(fixture.playback.dispose);
  180. addTearDown(fixture.jellyfin.dispose);
  181. final error = await fixture.playback.playAlbum(album('missing'));
  182. expect(error, contains('No playable tracks'));
  183. expect(fixture.playback.error, contains('No playable tracks'));
  184. });
  185. }