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:
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* A card grid repaints when the selection changes — and the thing that
|
||||
* makes it repaint is not obvious.
|
||||
*
|
||||
* `perf.m1` says `artists-view` and `genres-view` should hoist their
|
||||
* `.renderItem` / `.keyFunction` arrow functions to stable class fields
|
||||
* the way `cover-grid` does, because `LitVirtualizer` declares both as
|
||||
* plain properties and a fresh function marks them dirty on every host
|
||||
* update. That is true, and the fix is a regression: a parent
|
||||
* re-render only reaches the virtualizer's children *because* one of
|
||||
* those properties changed. Hoist them, and `LitVirtualizer` sees no
|
||||
* changed property, never re-renders, and the directive's `update()`
|
||||
* never runs — so the cards keep the classes they had.
|
||||
*
|
||||
* Measured in the running app on the fixture library: 1 highlighted
|
||||
* card before the change, 0 after. There was no compensating win to
|
||||
* pay for it, so the closures stay.
|
||||
*
|
||||
* This test is the reason they stay. It fails if someone applies m1
|
||||
* without also pushing an explicit `virtualizer.requestUpdate()` on
|
||||
* every piece of host state a card reads — which is nearly every reason
|
||||
* these views re-render, i.e. the same work under a longer name.
|
||||
*/
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import type { LitElement } from 'lit';
|
||||
|
||||
import '@components/artists-view/artists-view';
|
||||
import '@components/genres-view/genres-view';
|
||||
import { emit, stub, flush, resetHarness } from '@test/support/harness';
|
||||
import { Events } from '../../src/events';
|
||||
import { fixture, shadowAll } from '@test/support/render';
|
||||
|
||||
const ARTISTS = [
|
||||
{ ID: 1, Name: 'Alpha', AlbumCount: 2, TrackCount: 9 },
|
||||
{ ID: 2, Name: 'Beta', AlbumCount: 1, TrackCount: 4 },
|
||||
];
|
||||
|
||||
const GENRES = [
|
||||
{ name: 'Ambient', count: 12 },
|
||||
{ name: 'Doom', count: 3 },
|
||||
];
|
||||
|
||||
/** Give the virtualizer a viewport; a zero-height host renders nothing. */
|
||||
function sized(el: HTMLElement): void {
|
||||
el.style.display = 'block';
|
||||
el.style.height = '600px';
|
||||
el.style.width = '900px';
|
||||
}
|
||||
|
||||
async function settle(el: LitElement): Promise<void> {
|
||||
await flush();
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 80));
|
||||
}
|
||||
|
||||
describe('a card grid shows its selection', () => {
|
||||
beforeEach(() => {
|
||||
resetHarness();
|
||||
stub('library.Library.GetAllArtists', ARTISTS);
|
||||
stub('library.Library.GetAllGenresWithCounts', GENRES);
|
||||
stub('library.Library.GetAllTracks', []);
|
||||
stub('library.Library.GetAllAlbums', []);
|
||||
// The views read through LibraryController, whose cache is only
|
||||
// primed by a scan-complete; without it they render nothing and the
|
||||
// assertion below fails for the wrong reason.
|
||||
emit(Events.LibraryScanComplete);
|
||||
});
|
||||
|
||||
it('highlights an artist card that was ctrl-clicked', async () => {
|
||||
const el = await fixture<LitElement>('artists-view');
|
||||
|
||||
sized(el);
|
||||
await settle(el);
|
||||
|
||||
const card = shadowAll(el, '.artist-card')[0];
|
||||
|
||||
expect(card, 'no artist cards rendered').toBeTruthy();
|
||||
|
||||
card!.dispatchEvent(
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
);
|
||||
await settle(el);
|
||||
|
||||
// The clicked card specifically, not a count: a view is free to
|
||||
// arrive with something already selected, and the question here is
|
||||
// only whether the click reached the DOM.
|
||||
expect(shadowAll(el, '.artist-card')[0]?.classList.contains('selected')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('highlights a genre card that was ctrl-clicked', async () => {
|
||||
const el = await fixture<LitElement>('genres-view');
|
||||
|
||||
sized(el);
|
||||
await settle(el);
|
||||
|
||||
const card = shadowAll(el, '.genre-card')[0];
|
||||
|
||||
expect(card, 'no genre cards rendered').toBeTruthy();
|
||||
|
||||
card!.dispatchEvent(
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
);
|
||||
await settle(el);
|
||||
|
||||
// The clicked card specifically, not a count: a view is free to
|
||||
// arrive with something already selected, and the question here is
|
||||
// only whether the click reached the DOM.
|
||||
expect(shadowAll(el, '.genre-card')[0]?.classList.contains('selected')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user