test(frontend): cover the lifecycle, the voice and the repaints

Component and store cases for everything in this series, several of
which exist because the thing they pin is invisible everywhere else:

- `view-lifecycle` and `keyboard-reach` — a document listener count
  that does not grow across a simulated navigate cycle, and a tab
  sequence that reaches the sidebar and plays a row without a mouse.
- `notifications`, `notification-store`, `confirm-dialog`,
  `empty-states` — the four levels, the (level, region, key)
  coalescing window, and loading/failed/empty as three states.
- `card-grid-repaint` — fails if `artists-view`'s or `genres-view`'s
  per-render arrow functions are hoisted to stable fields, which is
  the audit's own recommendation and takes the cards from 1 highlighted
  to 0. It exists for no other reason.
- `lazy-track-details` — reads the five sources and fails on a
  returning static import, the same shape as `TestNoDirectRuntimeEmits`
  and for the same reason: the invariant is about what the code does
  *not* say.
- `now-playing` — a position report that changes nothing must not
  touch the DOM again, and a track change must. The first fails
  against the old unconditional `updated()`.
- `playlist-virtualization`, `list-render-cost`, `selection`, `icons`,
  and the store cases for the library-filter race, the never-settling
  waiter and the per-playlist patch.
This commit is contained in:
2026-08-12 01:20:03 -04:00
parent 2518385330
commit 5830b1ba17
25 changed files with 2498 additions and 37 deletions
+28 -8
View File
@@ -7,7 +7,7 @@ import { describe, expect, it, beforeEach } from 'vitest';
import { searchStore } from '@store/search-store';
import { trackListStore } from '@store/tracklist-store';
import { exploreCache } from '@store/explore-cache';
import { exploreCache, ARTIST_IMAGE_CACHE_LIMIT } from '@store/explore-cache';
import { Events } from '../../src/events';
import { emit, lastCall, flush } from '@test/support/harness';
@@ -141,14 +141,34 @@ describe('explore cache', () => {
expect(exploreCache.getArtist('mbid-2')?.imageURL).toBe('http://x/eno.jpg');
});
it('caches an artists release groups and top tracks separately', () => {
exploreCache.setArtistAlbums('mbid-3', [{ mbid: 'rg-1' }] as never);
exploreCache.setArtistTopTracks('mbid-3', [{ mbid: 'rec-1' }] as never);
// `perf.M8`. An artist entry holds the artist photo's base64 data URL
// — ~128 kB measured — so an unbounded map on a view that never
// unmounts grows for the life of the process.
it('evicts the least recently used artist past its cap', () => {
const over = ARTIST_IMAGE_CACHE_LIMIT + 10;
expect([
exploreCache.getArtistAlbums('mbid-3')?.length,
exploreCache.getArtistTopTracks('mbid-3')?.length,
]).toEqual([1, 1]);
for (let i = 0; i < over; i++) {
exploreCache.setArtist(`cap-${i}`, { mbid: `cap-${i}`, name: `A${i}` });
}
expect(exploreCache.stats().artists.entries).toBe(ARTIST_IMAGE_CACHE_LIMIT);
// The first inserted is gone; the last is not.
expect(exploreCache.getArtist('cap-0')).toBeUndefined();
expect(exploreCache.getArtist(`cap-${over - 1}`)?.name).toBe(`A${over - 1}`);
});
it('keeps an artist alive by reading it', () => {
// Recency is what makes the cap safe: the entry being rendered must
// not be the one evicted, or the render refetches it immediately.
for (let i = 0; i < ARTIST_IMAGE_CACHE_LIMIT; i++) {
exploreCache.setArtist(`lru-${i}`, { mbid: `lru-${i}`, name: `A${i}` });
}
exploreCache.getArtist('lru-0');
exploreCache.setArtist('lru-new', { mbid: 'lru-new', name: 'New' });
expect(exploreCache.getArtist('lru-0')?.name).toBe('A0');
expect(exploreCache.getArtist('lru-1')).toBeUndefined();
});
it('populates artists and albums from one search result', () => {