Files
yellowjacket/frontend/test/components/home-view.test.ts
T
logan ff687f0bd9 feat(home): populate the home page with start-listening shelves
The sidebar had a Home item that fell through to "Coming soon". What
was missing was not another view of the library — four of those exist,
sorted and complete — but the opposite: a complete, sorted library is
exactly what gives you nothing to play, because every entry point into
it is alphabetical and identical every time you open the app.

So a shelf is a *reason*, not a filter. Each one answers a different
question you might be asking when you do not know what you want (what
was I listening to, what is new, what do I keep coming back to, what
have I forgotten, what fits, what would I never pick myself) and each
says which question it answered — a row of covers with no explanation
is just another grid.

Two consequences run through it. Shelves are built from what the user
actually did — play counts, last played, import order — with random
sampling only where there is no signal to use, so randomness is the
fallback rather than the design. And a shelf with nothing behind it is
omitted instead of rendered empty: a fresh library legitimately gets
three, and an empty row labelled "on repeat" would be a lie.

The queries return album ids and nothing else, joined back to
GetAllAlbumsWithDetails in Go, so the album projection keeps having one
definition rather than one per shelf.
2026-08-11 01:15:34 -04:00

152 lines
4.2 KiB
TypeScript

/**
* The home page renders whatever `backend/home` decided, and nothing
* else: the shelves, their stated reasons, and two things a cover can
* do. So these tests are about the contract between the two — that a
* shelf's reason is displayed rather than swallowed, that a card opens
* the album it names, and that a play button plays it instead of
* opening it.
*/
import { describe, expect, it, beforeEach } from 'vitest';
import '@components/home-view/home-view';
import { stub, calls, lastArgs, stubFailure } from '@test/support/harness';
import { fixture, shadow, shadowAll, texts, text } from '@test/support/render';
function album(id: number, name: string, artist = 'Artist') {
return {
ID: id,
Name: name,
ArtistName: artist,
MBID: '',
Year: 2000,
ReleaseYear: 2000,
CoverArtPath: '',
CoverArtSmall: '',
CoverArtMedium: '',
CoverArtLarge: '',
ArtistMBID: '',
};
}
const SHELVES = [
{
id: 'recently-played',
kind: 'recently-played',
title: 'Pick up where you left off',
subtitle: 'The last albums you played',
albums: [album(1, 'Kid A'), album(2, 'Amnesiac')],
},
{
id: 'genre-Doom Jazz',
kind: 'genre',
title: 'Doom Jazz',
subtitle: 'Because your library is full of it',
albums: [album(3, 'Black Ships')],
},
];
describe('home view', () => {
beforeEach(() => {
stub('home.Service.GetShelves', SHELVES);
stub('library.Library.GetAlbumTracks', [
{ FilePath: '/music/1.mp3' },
{ FilePath: '/music/2.mp3' },
]);
});
it('renders a row per shelf, each with the reason it exists', async () => {
const el = await fixture('home-view');
await el.updateComplete;
expect(texts(el, '.shelf-title')).toEqual([
'Pick up where you left off',
'Doom Jazz',
]);
// The subtitle is the whole difference between a shelf and a grid.
expect(texts(el, '.shelf-sub')).toEqual([
'The last albums you played',
'Because your library is full of it',
]);
});
it('keys each row by the kind the backend assigned', async () => {
const el = await fixture('home-view');
await el.updateComplete;
expect(
shadowAll(el, '.shelf').map((s) => s.getAttribute('data-kind')),
).toEqual(['recently-played', 'genre']);
});
it('opens the album a card names, by local id', async () => {
const el = await fixture('home-view');
await el.updateComplete;
const seen: unknown[] = [];
el.addEventListener('navigate', (e) => seen.push((e as CustomEvent).detail));
shadow<HTMLElement>(el, '.card')!.click();
expect(seen).toEqual([
{
view: 'explore-album-details',
releaseGroupMBID: '',
albumName: 'Kid A',
artistName: 'Artist',
localAlbumId: 1,
},
]);
});
it('plays the album from the play button without navigating', async () => {
const el = await fixture('home-view');
await el.updateComplete;
const seen: unknown[] = [];
el.addEventListener('navigate', (e) => seen.push(e));
shadow<HTMLElement>(el, '.play')!.click();
await new Promise((r) => setTimeout(r, 0));
expect(seen).toEqual([]);
expect(lastArgs('library.Library.GetAlbumTracks')).toEqual([1]);
expect(lastArgs('queue.Queue.SetQueue')).toEqual([
['/music/1.mp3', '/music/2.mp3'],
0,
true,
]);
});
it('says so rather than rendering an empty page when there is nothing', async () => {
stub('home.Service.GetShelves', []);
const el = await fixture('home-view');
await el.updateComplete;
expect(text(el, '.empty')).toContain('Nothing to suggest yet');
});
it('reports a backend failure instead of pretending the library is empty', async () => {
stubFailure('home.Service.GetShelves');
const el = await fixture('home-view');
await el.updateComplete;
await el.updateComplete;
expect(text(el, '.empty')).toContain('Could not read your library');
});
it('rebuilds on demand', async () => {
const el = await fixture('home-view');
await el.updateComplete;
const before = calls('home.Service.GetShelves').length;
shadow<HTMLElement>(el, 'wa-button')!.click();
await el.updateComplete;
expect(calls('home.Service.GetShelves').length).toBe(before + 1);
});
});