Files
yellowjacket/frontend/test/components/artist-images.test.ts
T
yonluandClaude Opus 5 edb13a6f39 perf(explore): ask the disk once, prefetch once, and menu the releases
Three things on the Explore surfaces, all about not asking twice.

A portrait already on disk costs no network call. explore-view seeded
only from the library store — owned artists, which on a catalog search
is nearly none of the results — and sent everything else to
GetArtistImageURL, the resolving entry point, one await at a time.
GetArtistImagesCachedPaths asks the disk about every unresolved artist
in one call, and only what it does not answer reaches the resolver,
in parallel.

The artist page's two sections both wanted PrefetchReleases and each
called it, so the most expensive call the app makes was issued twice
for an overlapping set on a 1 req/s limiter. They are collected and
sent once on a microtask, and prefetchRequested stops the cold-artist
refetch re-asking for what it already asked for.

The release cards — most of the artist page — had no context menu at
all. They have one now on both release shapes, normalised to a
ReleaseMenuTarget when the menu opens so the union does not reach the
action handlers. It is a discriminated union rather than one nullable
field per kind because the panel is shared with the track menu: that is
what keeps aria-label moving with the target, which is the fault
cover-grid shipped. Which items appear is three different questions —
playback is gated on a local album id, not on "owned", and the request
needs a catalog MBID, so it is absent for a library-only release.

Note on the docs: the CLAUDE.md and NOTES.md prose here was
reconstructed after a mishandled `git stash --keep-index` destroyed the
uncommitted originals. One NOTES.md section is marked as incomplete
where its text could not be recovered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
2026-08-14 13:34:15 -04:00

90 lines
2.7 KiB
TypeScript

/**
* Which call an artist portrait comes from.
*
* `GetArtistImageURL` is the *resolving* entry point: on a miss it does
* MusicBrainz artist-rels → Wikidata → Wikipedia → a Wikimedia
* download. `GetArtistImagesCachedPaths` is a disk existence check.
* Explore used to seed only from the library store — owned artists,
* which on a catalog search is nearly none of the results — and send
* everything else to the resolver, one `await` at a time.
*
* So the rule under test is that a portrait already on disk costs no
* network call at all, and that the ones that do need resolving are not
* serialised behind each other.
*/
import { describe, expect, it, beforeEach } from 'vitest';
import type { LitElement } from 'lit';
import '@components/explore-view/explore-view';
import { stub, flush, resetHarness, calls } from '@test/support/harness';
import { fixture } from '@test/support/render';
const CACHED = 'artist-cached';
const UNCACHED = 'artist-uncached';
function searchResult() {
return {
artists: [
{ mbid: CACHED, name: 'Tideline', popularity: 10 },
{ mbid: UNCACHED, name: 'Shorebreak', popularity: 5 },
],
releaseGroups: [],
recordings: [],
topResults: [],
};
}
beforeEach(() => {
resetHarness();
stub('explore.Service.SearchLocal', searchResult());
stub('explore.Service.GetThumbnails', {});
stub('explore.Service.GetExploreShelves', { state: 'ready', shelves: [] });
stub('explore.Service.GetArtistImagesCachedPaths', {
[CACHED]: '/artist-images/ar/artist-cached/primary_md.jpg',
});
stub('explore.Service.GetArtistImageURL', '');
});
async function search(): Promise<LitElement> {
const el = await fixture<LitElement>('explore-view', {});
await (
el as unknown as {
executeIndexSearch: (v: number, q: string) => Promise<void>;
}
).executeIndexSearch(0, 'tide');
await flush();
await el.updateComplete;
return el;
}
describe('where Explore gets its artist portraits', () => {
it('asks the disk about every unresolved artist in one call', async () => {
await search();
const cachedCalls = calls('explore.Service.GetArtistImagesCachedPaths');
// Exactly one: the point is that N artists cost one disk lookup,
// and a zero here would mean the search never ran.
expect(cachedCalls.length).toBe(1);
const asked = cachedCalls[0]?.args[0];
expect(asked).toContain(CACHED);
expect(asked).toContain(UNCACHED);
});
it('never sends a disk-cached artist to the resolver', async () => {
await search();
const resolved = calls('explore.Service.GetArtistImageURL').map(
(c) => c.args[0],
);
expect(resolved).not.toContain(CACHED);
});
});