2a18e91c39
Four-part change to push more surfaces onto the drift cache and eliminate cold-tab-visit latency on the Library screen. * **Library Artists tab** — _libraryArtistsProvider migrates from REST-paginated AsyncNotifier with infinite-scroll loadMore to a drift-first StreamProvider over cached_artists ordered by sortName. Sync already populates the full set; cacheFirst's fetchAndPopulate covers the fresh-install + sync-not-yet-done cold case via /api/artists?limit=1000. SWR refresh on every visit. GridView.builder lazily realizes only visible cells so loading the full list up front is fine for typical libraries. loadMore + NotificationListener gone. * **Library Albums tab** — same migration, drift-first over cached_albums joined with cached_artists for the artistName field. * **systemPlaylistsStatusProvider** — new CachedSystemPlaylistsStatus single-row table (schema 6 → 7, JSON blob like CachedHomeSnapshot) for the home Playlists row's "building / pending / failed" placeholder logic. Drift-first means the row paints with the prior status instantly instead of flickering through SystemPlaylistsStatus.empty() while the REST call resolves. * **Library screen tab pre-warm** — ref.listen on all 5 tab providers in _LibraryScreenState.build subscribes them upfront so swiping between tabs feels instant rather than each tab paying its own cold-cache cost on first visit. cacheFirst handles dedupe of concurrent fetchAndPopulate triggers. Test mock updated for the StreamProvider type change on systemPlaylistsStatusProvider.
110 lines
4.2 KiB
Dart
110 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:minstrel/api/endpoints/playlists.dart';
|
|
import 'package:minstrel/library/home_screen.dart';
|
|
import 'package:minstrel/library/library_providers.dart';
|
|
import 'package:minstrel/models/home_index.dart';
|
|
import 'package:minstrel/models/playlist.dart';
|
|
import 'package:minstrel/models/system_playlists_status.dart';
|
|
import 'package:minstrel/playlists/playlists_provider.dart';
|
|
import 'package:minstrel/theme/theme_data.dart';
|
|
|
|
// All tests pump an empty HomeIndex unless they care about populated
|
|
// section IDs — per-tile hydration is intentionally not exercised here
|
|
// (that requires drift, and the @Tags(['drift']) tier covers it).
|
|
// What this suite verifies is the screen's section-level shape:
|
|
// placeholders / empty-state copy / section presence given the index.
|
|
const _emptyIndex = HomeIndex.empty;
|
|
|
|
void main() {
|
|
testWidgets('renders 4 placeholder cards in Playlists row when no system playlists exist',
|
|
(tester) async {
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
homeIndexProvider.overrideWith((ref) => Stream.value(_emptyIndex)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(PlaylistsList.empty()),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) => Stream.value(SystemPlaylistsStatus.empty()),
|
|
),
|
|
],
|
|
child: MaterialApp(theme: buildThemeData(), home: const HomeScreen()),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
// For-You placeholder + Discover placeholder + 3 Songs-like placeholders.
|
|
expect(find.text('For You'), findsOneWidget);
|
|
expect(find.text('Discover'), findsOneWidget);
|
|
expect(find.text('Songs like…'), findsNWidgets(3));
|
|
});
|
|
|
|
testWidgets('renders empty-state copy for each section when the index is empty',
|
|
(tester) async {
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
homeIndexProvider.overrideWith((ref) => Stream.value(_emptyIndex)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(PlaylistsList.empty()),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) => Stream.value(SystemPlaylistsStatus.empty()),
|
|
),
|
|
],
|
|
child: MaterialApp(theme: buildThemeData(), home: const HomeScreen()),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.text("Nothing added yet. Scan a folder via the server's config."),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.text('No forgotten favourites yet. Like some albums or artists to fill this in.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('No plays to draw from. Listen to something.'),
|
|
findsOneWidget);
|
|
expect(find.text('No recent plays.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('renders For-You card when system playlist exists',
|
|
(tester) async {
|
|
const forYou = Playlist(
|
|
id: 'fy',
|
|
userId: 'u1',
|
|
name: 'For You',
|
|
description: '',
|
|
isPublic: false,
|
|
systemVariant: 'for_you',
|
|
trackCount: 75,
|
|
coverUrl: '',
|
|
ownerUsername: 'alice',
|
|
createdAt: '2026-05-01T00:00:00Z',
|
|
updatedAt: '2026-05-01T00:00:00Z',
|
|
);
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
homeIndexProvider.overrideWith((ref) => Stream.value(_emptyIndex)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(
|
|
const PlaylistsList(owned: [forYou], public: [])),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) => Stream.value(SystemPlaylistsStatus.empty()),
|
|
),
|
|
],
|
|
child: MaterialApp(theme: buildThemeData(), home: const HomeScreen()),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
// The real card carries the playlist name + the "for you" badge.
|
|
expect(find.text('For You'), findsOneWidget);
|
|
expect(find.text('for you'), findsOneWidget);
|
|
});
|
|
|
|
// Section-with-populated-IDs coverage lives in the drift-tagged
|
|
// integration suite (Slice F follow-up) because per-tile providers
|
|
// require a real (in-memory) drift DB. The flutter-ci runner skips
|
|
// drift tests pending libsqlite3.
|
|
}
|