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
+87
View File
@@ -0,0 +1,87 @@
/**
* `tracksByFilePath` is a cache keyed on an array's identity, which is
* only safe because the stores replace the array whenever its contents
* change. These tests pin both halves of that: the cache is reused for
* the same array, and a new array gets a new map.
*
* The reason it exists is `perf.m6`: five components resolved selected
* file paths back to tracks with `filePaths.map(fp => tracks.find(…))`,
* O(selection × total). Measured through the real opener at 50 000
* tracks, "Select all → Edit tags" blocked the main thread for
* 3.06.3 s; with the map, 68 ms.
*/
import { describe, expect, it } from 'vitest';
import { tracksByFilePath, tracksForPaths } from '@utils/track-index';
type Track = { FilePath: string; Title: string };
const track = (n: number): Track => ({
FilePath: `/music/${n}.mp3`,
Title: `Track ${n}`,
});
// The util is typed against the generated `library.Track`; these
// fixtures carry only the fields it reads.
const asTracks = (t: Track[]) => t as unknown as Parameters<
typeof tracksByFilePath
>[0];
describe('tracksByFilePath', () => {
it('indexes by file path', () => {
const tracks = asTracks([track(1), track(2), track(3)]);
const map = tracksByFilePath(tracks);
expect(map.size).toBe(3);
expect(map.get('/music/2.mp3')).toBe(tracks[1]);
expect(map.get('/music/nope.mp3')).toBeUndefined();
});
it('reuses the map for the same array', () => {
const tracks = asTracks([track(1), track(2)]);
expect(tracksByFilePath(tracks)).toBe(tracksByFilePath(tracks));
});
it('builds a fresh map for a replaced array', () => {
// The store shares unchanged members and replaces the array,
// so identity is the invalidation signal.
const first = asTracks([track(1)]);
const second = asTracks([track(1), track(2)]);
expect(tracksByFilePath(second)).not.toBe(tracksByFilePath(first));
expect(tracksByFilePath(second).size).toBe(2);
});
it('keeps the first of a duplicated path, as find() did', () => {
const a = { FilePath: '/music/1.mp3', Title: 'first' };
const b = { FilePath: '/music/1.mp3', Title: 'second' };
expect(tracksByFilePath(asTracks([a, b])).get('/music/1.mp3'))
.toBe(a);
});
});
describe('tracksForPaths', () => {
it('resolves in the order asked for, not list order', () => {
const tracks = asTracks([track(1), track(2), track(3)]);
const got = tracksForPaths(
tracks,
['/music/3.mp3', '/music/1.mp3'],
);
expect(got.map((t) => t.FilePath))
.toEqual(['/music/3.mp3', '/music/1.mp3']);
});
it('drops paths that are not in the list', () => {
const tracks = asTracks([track(1)]);
expect(tracksForPaths(tracks, ['/music/1.mp3', '/gone.mp3']))
.toHaveLength(1);
});
it('is empty for an empty selection', () => {
expect(tracksForPaths(asTracks([track(1)]), [])).toEqual([]);
});
});