Files
yellowjacket/frontend/test/utils/track-index.test.ts
T
logan 5830b1ba17 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.
2026-08-12 01:20:03 -04:00

88 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* `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([]);
});
});