22bd06a578
Home screen is the first surface on app open; without a local cache
the cold-start blocks on /api/home, which dominates felt latency on
slow or remote connections. This commit caches the last successful
HomeData as a single-row JSON blob in drift, so subsequent app opens
yield content immediately and revalidate in the background.
Schema:
- New CachedHomeSnapshot table (single row: id=1, json TEXT,
updated_at). schemaVersion bumped 2 → 3 with a forward migration
that calls m.createTable(cachedHomeSnapshot). Codegen regenerated
via build_runner; the *.g.dart files are gitignored and rebuilt
by the CI Codegen step.
Provider rewrite:
- homeProvider: FutureProvider<HomeData> → StreamProvider<HomeData>
using the existing cacheFirst<CachedHomeSnapshotData, HomeData>
pattern (alwaysRefresh: true for SWR). On cold cache the first
/api/home fetch populates the row. On warm cache the cached
HomeData is yielded immediately and a background REST fetch
overwrites the row, which drift's watch() picks up.
- Encoder helpers (_albumToJson / _artistToJson / _trackToJson) so
HomeData survives the JSON round-trip into and out of drift.
Field names match the server's /api/home wire shape exactly so
HomeData.fromJson handles both fresh server responses and cached
drift rows.
Callers untouched: home_screen.dart's ref.watch + ref.refresh +
metadata_prefetcher's ref.listen all keep working with the
StreamProvider shape (AsyncValue<HomeData> stays the surface type).
Test fix: 4 homeProvider.overrideWith sites in home_screen_test.dart
switched from `(ref) async => _emptyHome` (FutureProvider form) to
`(ref) => Stream.value(_emptyHome)` (StreamProvider form).
For #357. Completes the user-visible deferred follow-up. Remaining
deferred items: library_changes server-side retention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
4.8 KiB
Dart
138 lines
4.8 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/album.dart';
|
|
import 'package:minstrel/models/artist.dart';
|
|
import 'package:minstrel/models/home_data.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';
|
|
|
|
const _emptyHome = HomeData(
|
|
recentlyAddedAlbums: [],
|
|
rediscoverAlbums: [],
|
|
rediscoverArtists: [],
|
|
mostPlayedTracks: [],
|
|
lastPlayedArtists: [],
|
|
);
|
|
|
|
void main() {
|
|
testWidgets('renders 4 placeholder cards in Playlists row when no system playlists exist',
|
|
(tester) async {
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
homeProvider.overrideWith((ref) => Stream.value(_emptyHome)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(PlaylistsList.empty()),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) async => SystemPlaylistsStatus.empty(),
|
|
),
|
|
],
|
|
child: MaterialApp(theme: buildThemeData(), home: const HomeScreen()),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
// For-You placeholder + 3 Songs-like placeholders.
|
|
expect(find.text('For You'), findsOneWidget);
|
|
expect(find.text('Songs like…'), findsNWidgets(3));
|
|
});
|
|
|
|
testWidgets('renders empty-state copy for each section when home payload is empty',
|
|
(tester) async {
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
homeProvider.overrideWith((ref) => Stream.value(_emptyHome)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(PlaylistsList.empty()),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) async => 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: [
|
|
homeProvider.overrideWith((ref) => Stream.value(_emptyHome)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(
|
|
const PlaylistsList(owned: [forYou], public: [])),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) async => 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);
|
|
});
|
|
|
|
testWidgets('renders home rollups when payload is non-empty', (tester) async {
|
|
const home = HomeData(
|
|
recentlyAddedAlbums: [
|
|
AlbumRef(id: 'a', title: 'Geogaddi', artistId: 'x', artistName: 'BoC'),
|
|
],
|
|
rediscoverAlbums: [],
|
|
rediscoverArtists: [
|
|
ArtistRef(id: 'r', name: 'Aphex Twin'),
|
|
],
|
|
mostPlayedTracks: [],
|
|
lastPlayedArtists: [],
|
|
);
|
|
await tester.pumpWidget(ProviderScope(
|
|
overrides: [
|
|
homeProvider.overrideWith((ref) => Stream.value(home)),
|
|
playlistsListProvider('all').overrideWith(
|
|
(ref) => Stream.value(PlaylistsList.empty()),
|
|
),
|
|
systemPlaylistsStatusProvider.overrideWith(
|
|
(ref) async => SystemPlaylistsStatus.empty(),
|
|
),
|
|
],
|
|
child: MaterialApp(theme: buildThemeData(), home: const HomeScreen()),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Recently added'), findsOneWidget);
|
|
expect(find.text('Geogaddi'), findsOneWidget);
|
|
expect(find.text('Rediscover'), findsOneWidget);
|
|
expect(find.text('Aphex Twin'), findsOneWidget);
|
|
});
|
|
}
|