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.
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
/**
|
||
* `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.0–6.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([]);
|
||
});
|
||
});
|