import 'package:cardash_launcher/data/models/jellyfin_library.dart'; import 'package:cardash_launcher/data/models/jellyfin_models.dart'; import 'package:cardash_launcher/data/services/jellyfin_library_service.dart'; import 'package:cardash_launcher/features/jellyfin/jellyfin_dashboard_controller.dart'; import 'package:cardash_launcher/features/settings/jellyfin_controller.dart'; import 'package:flutter_test/flutter_test.dart'; import 'jellyfin_fakes.dart'; class _Fixture { _Fixture({ required this.jellyfin, required this.gateway, required this.library, required this.dashboard, }); final JellyfinController jellyfin; final FakeJellyfinLibraryGateway gateway; final JellyfinLibraryService library; final JellyfinDashboardController dashboard; } Future<_Fixture> _fixture({List genres = const []}) async { final jellyfin = await configuredJellyfinController(); await jellyfin.setGenreSelections([ for (final id in genres) JellyfinGenreSelection(id: id, label: id), ]); final gateway = FakeJellyfinLibraryGateway() ..played = [ track( 'a2', albumId: 'album-a', albumName: 'Album A', lastPlayed: DateTime.utc(2026, 10, 31, 12), ), track('a1', albumId: 'album-a', albumName: 'Album A'), track('b1', albumId: 'album-b', albumName: 'Album B'), track('c1', albumId: 'album-c', albumName: 'Album C'), track('d1', albumId: 'album-d', albumName: 'Album D'), track('e1', albumId: 'album-e', albumName: 'Album E'), track('f1', albumId: 'album-f', albumName: 'Album F'), ] ..added = [ album('new-1'), album('new-2'), album('new-3'), album('new-4'), album('new-5'), ] ..random = [album('r1'), album('r2'), album('r3'), album('r4'), album('r5')] ..genres = const ['Metal', 'Melodic Metal', 'Метъл', 'Jazz', 'Рок']; final library = testLibraryService(jellyfin, gateway); final dashboard = JellyfinDashboardController( library: library, jellyfin: jellyfin, ); return _Fixture( jellyfin: jellyfin, gateway: gateway, library: library, dashboard: dashboard, ); } void main() { test('loads three rows and expands only the selected genres', () async { final fixture = await _fixture(genres: ['metal', 'jazz']); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); await fixture.dashboard.load(); expect(fixture.dashboard.hasLoaded, isTrue); expect(fixture.dashboard.loading, isFalse); expect(fixture.dashboard.error, isNull); expect(fixture.dashboard.latestPlayed.length, 6); expect(fixture.dashboard.latestPlayed.first.id, 'album-a'); expect(fixture.dashboard.latestAdded.length, 5); expect(fixture.dashboard.random.length, 5); expect( fixture.gateway.lastRandomGenres, containsAll(['Metal', 'Melodic Metal', 'Метъл', 'Jazz']), ); expect(fixture.gateway.lastRandomGenres, isNot(contains('Рок'))); }); test('no selected genres means no random query', () async { final fixture = await _fixture(); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); await fixture.dashboard.load(); expect(fixture.dashboard.hasGenres, isFalse); expect(fixture.dashboard.random, isEmpty); expect(fixture.gateway.randomCalls, 0); }); test('shuffle replaces the random row', () async { final fixture = await _fixture(genres: ['metal']); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); await fixture.dashboard.load(); expect(fixture.dashboard.random.first.id, 'r1'); fixture.gateway.random = [album('r9'), album('r10')]; await fixture.dashboard.shuffle(); expect(fixture.dashboard.random.map((entry) => entry.id), ['r9', 'r10']); expect(fixture.gateway.randomCalls, 2); expect(fixture.dashboard.randomLoading, isFalse); }); test('markPlayed moves an album to the front and trims to five', () async { final fixture = await _fixture(); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); await fixture.dashboard.load(); fixture.dashboard.markPlayed(album('album-a')); expect(fixture.dashboard.latestPlayed.first.id, 'album-a'); expect(fixture.dashboard.latestPlayed.length, 6); }); test('a server error is surfaced with retry state', () async { final fixture = await _fixture(); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); fixture.gateway.error = JellyfinUnreachable('offline'); await fixture.dashboard.load(); expect(fixture.dashboard.hasLoaded, isTrue); expect(fixture.dashboard.loading, isFalse); expect(fixture.dashboard.error, contains('unreachable')); }); test('a random-only failure keeps the latest rows', () async { final fixture = await _fixture(genres: ['metal']); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); fixture.gateway.randomError = JellyfinUnreachable('offline'); await fixture.dashboard.load(); expect(fixture.dashboard.error, isNull); expect(fixture.dashboard.randomError, contains('unreachable')); expect(fixture.dashboard.latestPlayed, isNotEmpty); expect(fixture.dashboard.latestAdded, isNotEmpty); expect(fixture.dashboard.random, isEmpty); }); test('no connection reports a pairing error', () async { final fixture = await _fixture(); addTearDown(fixture.dashboard.dispose); addTearDown(fixture.jellyfin.dispose); final unconfigured = JellyfinLibraryService( connectionProvider: () => null, gateway: fixture.gateway, ); final dashboard = JellyfinDashboardController( library: unconfigured, jellyfin: fixture.jellyfin, ); addTearDown(dashboard.dispose); await dashboard.load(); expect(dashboard.hasLoaded, isTrue); expect(dashboard.error, contains('Pair Jellyfin')); }); }