feat(web): smart empty-states with action affordances (Tier B6)
test-web / test (push) Successful in 32s

Replace dead-end empty copy with EmptyState cards that include a
clear next step.

- EmptyState.svelte: reusable card with `block` (whole-tab) and
  `inline` (sub-section) variants, an actions snippet for buttons.
- Artists / Albums: when the whole library is empty, link to
  /settings (the operator can scan a folder there).
- Liked: when all three sub-sections are empty, show a single
  whole-tab card ("No likes yet" with Explore Home + Browse albums).
  When only one sub-section is empty, an inline hint with a link
  to the corresponding library tab.
- History: "Listen to something" button → Home.
- Playlists: "Create a playlist" button calls the existing create
  flow; renamed from "New playlist" to avoid colliding with the
  header's button-by-name lookup in tests.

Liked tab tests updated to match: the previous "three 'no liked X
yet'" assertion is now a single onboarding card test + a sibling
test for the mixed populated/empty case.
This commit is contained in:
2026-06-01 13:53:38 -04:00
parent be7be6f617
commit 859aff8d30
7 changed files with 167 additions and 12 deletions
+23 -2
View File
@@ -64,7 +64,7 @@ describe('liked library page', () => {
expect(screen.getByRole('heading', { name: /tracks/i })).toBeInTheDocument();
});
test('empty section renders "No liked X yet" hint', () => {
test('all three sections empty renders the whole-tab onboarding card', () => {
(createLikedArtistsInfiniteQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockInfiniteQuery({ pages: [page<ArtistRef>([], 0)] })
);
@@ -75,7 +75,28 @@ describe('liked library page', () => {
mockInfiniteQuery({ pages: [page<TrackRef>([], 0)] })
);
render(LikedPage);
expect(screen.getAllByText(/no liked/i)).toHaveLength(3);
// The whole-tab EmptyState replaces the three per-section hints
// when every section is empty — single heading + action affordances.
expect(screen.getByRole('heading', { name: /no likes yet/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /explore home/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /browse albums/i })).toBeInTheDocument();
});
test('one populated section + two empty subsections renders inline hints', () => {
(createLikedArtistsInfiniteQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockInfiniteQuery({ pages: [page<ArtistRef>([{ id: 'a1', name: 'Miles' } as ArtistRef], 1)] })
);
(createLikedAlbumsInfiniteQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockInfiniteQuery({ pages: [page<AlbumRef>([], 0)] })
);
(createLikedTracksInfiniteQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockInfiniteQuery({ pages: [page<TrackRef>([], 0)] })
);
render(LikedPage);
expect(screen.getByText(/no liked albums yet/i)).toBeInTheDocument();
expect(screen.getByText(/no liked tracks yet/i)).toBeInTheDocument();
// Inline variant uses link affordances, not the big card heading.
expect(screen.queryByRole('heading', { name: /no likes yet/i })).not.toBeInTheDocument();
});
test('Load more calls fetchNextPage on each section that has more', async () => {