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.
This commit is contained in:
2026-08-11 01:15:34 -04:00
parent 62bb40fc4d
commit ff687f0bd9
12 changed files with 1838 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
import { test, expect, waitForEvent, resetEvents } from '../support/fixtures.js';
/**
* The home page, which is the one view whose content is a *judgement*
* rather than a listing: `backend/home` decides which shelves exist for
* this library and why, and the page is only correct if that reasoning
* survives to the screen.
*
* The fixture library has never been played, so the shelves that need
* history are legitimately absent — asserting on which ones appear is
* asserting that the page does not invent them.
*/
test.describe('home', () => {
test.beforeEach(async ({ app }) => {
await app.getByTestId('nav-home').click();
await expect(app.getByTestId('main-content')).toHaveAttribute(
'data-active-view',
'home',
);
});
test('offers shelves, each with the reason it is there', async ({ app }) => {
const shelves = app.locator('home-view .shelf');
await expect(shelves.first()).toBeVisible();
const count = await shelves.count();
expect(count).toBeGreaterThan(0);
// Every shelf says why it exists. A row of covers with no reason
// is the albums grid, which the user already has.
for (let i = 0; i < count; i += 1) {
await expect(shelves.nth(i).locator('.shelf-sub')).not.toBeEmpty();
}
});
test('never renders a shelf with nothing on it', async ({ app }) => {
// The reason shelves are built server-side: a row that promises
// "what you play the most" and then shows nothing is worse than no
// row, so a shelf with no albums must not reach the page at all.
const shelves = app.locator('home-view .shelf');
await expect(shelves.first()).toBeVisible();
const count = await shelves.count();
for (let i = 0; i < count; i += 1) {
await expect(shelves.nth(i).locator('.card').first()).toBeVisible();
}
});
test('a cover opens that album', async ({ app }) => {
const card = app.locator('home-view .card').first();
const name = await card.locator('.name').innerText();
await card.click();
await expect(app.locator('explore-album-details')).toBeVisible();
await expect(app.locator('explore-album-details .album-title')).toContainText(
name,
);
});
test('the play button plays the album instead of opening it', async ({
app,
}) => {
await resetEvents(app);
const card = app.locator('home-view .card').first();
await card.hover();
await card.locator('.play').click();
await waitForEvent(app, 'TrackChanged');
// Playing must not also navigate: the two actions live on the same
// card and the inner one has to win outright.
await expect(app.locator('home-view')).toBeVisible();
await expect(app.locator('explore-album-details')).toHaveCount(0);
});
});