([], 100)],
hasNextPage: true,
fetchNextPage
})
);
render(TracksOverflow);
await fireEvent.click(screen.getByRole('button', { name: /load more/i }));
expect(fetchNextPage).toHaveBeenCalledTimes(1);
});
test('empty q shows a prompt', () => {
state.pageUrl = new URL('http://localhost/search/tracks');
render(TracksOverflow);
expect(screen.getByText(/no query/i)).toBeInTheDocument();
});
});
```
- [ ] **Step 10: Run, confirm fail**
Run: `cd web && npm test -- 'src/routes/search/tracks/tracks.test.ts'`
Expected: FAIL.
- [ ] **Step 11: Implement `web/src/routes/search/tracks/+page.svelte`**
```svelte
{#if !q}
No query — return to search to start typing.
{:else if query?.isError}
{:else if showSkeleton.value && tracks.length === 0}
{:else if tracks.length === 0}
No track matches.
{:else}
{#each tracks as t, i (t.id)}
{/each}
{#if query?.hasNextPage}
query.fetchNextPage()}
>
{query.isFetchingNextPage ? 'Loading…' : 'Load more'}
{/if}
{/if}
```
- [ ] **Step 12: Run, confirm pass**
Run: `cd web && npm test -- 'src/routes/search/tracks/tracks.test.ts'`
Expected: PASS — 4 tests.
- [ ] **Step 13: Type-check**
Run: `cd web && npm run check`
Expected: `0 ERRORS 0 WARNINGS`.
- [ ] **Step 14: Commit**
```bash
git add web/src/routes/search/artists web/src/routes/search/albums web/src/routes/search/tracks
git commit -m "feat(web): add three search overflow pages
/search/artists, /search/albums, /search/tracks each read ?q= and run
their own createSearchInfiniteQuery (limit=50). Load more pulls the
next offset; back link returns to /search?q=. Empty q shows a 'no
query' prompt and fires no request. Tracks overflow uses the onPlay
prop on TrackRow to call playRadio(track.id)."
```
---
## Task 12: Final verification + branch finish
**Files:** none (verification only).
- [ ] **Step 1: Full Go suite**
Run: `go test -short -race ./...`
Expected: PASS.
- [ ] **Step 2: Go linter**
Run: `golangci-lint run ./...`
Expected: clean.
- [ ] **Step 3: Web check + full tests + build**
Run: `cd web && npm run check && npm test && npm run build`
Expected: check 0/0; all vitest files pass; build emits `web/build/`.
Run: `git checkout -- web/build/index.html`
Expected: committed placeholder restored.
- [ ] **Step 4: Docker build smoke**
Run: `docker build -t minstrel:search-smoke .`
Expected: image builds.
Run: `docker run --rm --entrypoint /bin/sh minstrel:search-smoke -c 'test -x /usr/local/bin/minstrel && echo ok'`
Expected: `ok`.
- [ ] **Step 5: Local end-to-end manual check**
Bring up the stack:
```bash
docker compose up --build -d
```
In a browser at `localhost:4533`:
1. Sign in. Header now has a search box between brand and the user-menu.
2. From the home page, type `mil` (or any string with multiple library matches). After ~250ms the URL becomes `/search?q=mil` and three sections appear (or fewer if some facets match nothing).
3. Empty matches: type `zzzqqq` — page shows "No matches for 'zzzqqq'".
4. Backspace through the input until empty: URL drops `?q=`; main `/search` page shows "Start typing".
5. Click the `+` on a track result → bottom-bar player updates queue length but does NOT auto-play (or, if a track was already playing, current playback continues).
6. Click a track result (the row, not the `+`) → radio plays (one-track queue, since it's the M6 stub).
7. Click the `+` on an album card → all of that album's tracks are appended; current playback continues.
8. Click "See all 47 artists →" → overflow page loads; "Load more" pulls the next page; back link returns to `/search?q=…`.
9. Click an artist on `/search?q=…` → navigates to `/artists/:id`. The header search box still shows "mil".
10. Browser back → returns to `/search?q=mil` with cached results.
11. Press Escape with the input focused → input clears and URL clears.
Tear down:
```bash
docker compose down
```
- [ ] **Step 6: Finish the branch**
Follow `superpowers:finishing-a-development-branch`. Expected path: Option 2 — push `dev`, open a PR to `main`, wait for Forgejo CI, merge once green.
---
## Self-Review Notes
**Spec coverage:**
- Goal / non-goals → Task structure honors the non-goals (no autocomplete, no Cmd-K, no recent searches).
- Header search bar → Task 7 (component) + Task 9 (Shell wiring).
- Live debounced URL sync → Task 7 with debounce/replaceState/Escape behavior covered by tests.
- `/search` page with three sections + see-all links → Task 10.
- Three overflow pages → Task 11.
- `/api/radio` stub → Task 1 (Go handler + 5 tests covering 200/404/missing/blank/bad-uuid).
- `enqueueTrack` / `enqueueTracks` / `playRadio` → Task 3.
- TrackRow `` refactor + `onPlay` + `+ queue` → Task 5.
- AlbumCard `+ queue` → Task 6.
- States (empty/loading/error/no-results) → covered in Tasks 10 and 11.
- Component tests → each task has its own. Manual end-to-end → Task 12 Step 5.
- Risks → addressed in spec; the TrackRow refactor risk is mitigated by re-running the album-page tests in Task 5 Step 5.
**Type consistency:**
- `SearchResponse`, `RadioResponse`, `Page`, `ArtistRef`, `AlbumRef`, `TrackRef`, `AlbumDetail` — defined once in `types.ts`; consumed identically in queries, store, and pages.
- `playRadio(seedTrackId: string)` — same signature in spec, store, and call sites.
- `enqueueTrack(t: TrackRef)`, `enqueueTracks(ts: TrackRef[])` — consistent.
- `qk.search`, `qk.searchArtists`, `qk.searchAlbums`, `qk.searchTracks` — consistent across queries.ts and tests.
- `onPlay?: (tracks: TrackRef[], index: number) => void` — same shape in `TrackRow.svelte`, search page, and tracks overflow page.
- `SEARCH_SUMMARY_LIMIT = 10` and `SEARCH_FACET_PAGE_SIZE = 50` — used only inside `queries.ts`; not surfaced to UI tests.
**Filename hazards:**
- No `+`-prefixed test files under route dirs. Search route tests use plain names: `search.test.ts`, `artists.test.ts`, `albums.test.ts`, `tracks.test.ts`.
- `SearchInput.test.ts` is plain `.test.ts` because the test body itself uses no rune syntax (only the imported component does); `.svelte.test.ts` is reserved for tests like `useDelayed` where the test body declares `$state`/`$effect.root` directly.
**Placeholder scan:** no TBD/TODO/later markers. Every code block is complete; every command has expected output.
**TrackRow refactor risk:** acknowledged; Task 5 re-runs the album page tests as Step 5 to catch any regression before commit.