test(e2e): freeze the four reproductions this work started from
Each was written first and watched fail first: - `view-lifecycle` — pressing `s` on Settings must not skip an album out of the Autotag queue, and on Autotag the same key must not also toggle shuffle. - `player-truth` — the elapsed clock tracks the backend through steady playback and four keyboard seeks (the measurement that failed by 30 s), a finished queue keeps the track on the bar at 0:00, and auto-advance skips a missing file and reaches the next track. - `offline-icons` — blocks every non-local request and asserts on the <svg> *inside* each icon's shadow root, since asserting the element exists would have passed before the fix too. Verified red: 24 empty icons. - `failure-voice` — induces a real binding failure through /__test/sql and asserts a sentence appears. Its first version renamed the decoy library to its own name, which the backend accepts: it failed at the right assertion while never inducing the failure, so it now picks the row by the seeded library's name from /__test/health. - `play-count` — a finished track does not refetch the library. The queue spec now opens the panel before asserting on its rows and waits for it to close again: the panel's width is animated and the transport slides with it, so a click issued during the close lands on whichever button moved under the pointer.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
import { test, expect } from '../support/fixtures.js';
|
||||
|
||||
/**
|
||||
* The app must work with no network.
|
||||
*
|
||||
* Every `<wa-icon>` used to resolve to ka-f.fontawesome.com at runtime
|
||||
* (audit H-4 / perf.M9), so a desktop music player offline, on a
|
||||
* captive portal or behind a firewall rendered no icons at all — while
|
||||
* playing files sitting on the local disk. `src/icons/` bundles them
|
||||
* and overrides Web Awesome's `default` library.
|
||||
*
|
||||
* The reproduction is the point. Asserting that a `<wa-icon>` *exists*
|
||||
* would have passed before the fix too: the element is always in the
|
||||
* DOM and only its contents came from the network. So this asserts on
|
||||
* the `<svg>` inside each icon's shadow root, with everything that is
|
||||
* not the app's own origin blocked — which is what a closed network
|
||||
* actually looks like to a local server.
|
||||
*/
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const INIT_SCRIPT = resolve(here, '../../.playwright/init-events.js');
|
||||
|
||||
/** Icons on the first screen. Fewer than this means nothing rendered. */
|
||||
const EXPECTED_MIN_ICONS = 5;
|
||||
|
||||
const VIEWS = [
|
||||
'home', 'tracks', 'albums', 'artists', 'genres', 'playlists',
|
||||
'explore', 'downloads', 'jobs', 'settings',
|
||||
];
|
||||
|
||||
type IconState = { name: string; hasSvg: boolean };
|
||||
|
||||
const collectIcons = (): IconState[] => {
|
||||
const icons: IconState[] = [];
|
||||
|
||||
const walk = (root: Document | ShadowRoot): void => {
|
||||
for (const el of Array.from(root.querySelectorAll('*'))) {
|
||||
if (el.tagName === 'WA-ICON') {
|
||||
icons.push({
|
||||
name: el.getAttribute('name') ?? '(unnamed)',
|
||||
hasSvg: !!el.shadowRoot?.querySelector('svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (el.shadowRoot) walk(el.shadowRoot);
|
||||
}
|
||||
};
|
||||
|
||||
walk(document);
|
||||
|
||||
return icons;
|
||||
};
|
||||
|
||||
test.describe('offline', () => {
|
||||
test('icons render with every external request blocked', async ({
|
||||
page,
|
||||
baseURL,
|
||||
}) => {
|
||||
const blocked: string[] = [];
|
||||
|
||||
// Deliberately not `context.setOffline(true)`: the app *is* a local
|
||||
// server, so taking the whole stack down would break the bindings
|
||||
// rather than the icons. A closed network is precisely "the app's
|
||||
// own origin still answers, nothing else does".
|
||||
await page.route('**/*', (route) => {
|
||||
const url = route.request().url();
|
||||
|
||||
if (url.startsWith(baseURL!) || url.startsWith('data:')) {
|
||||
return route.continue();
|
||||
}
|
||||
|
||||
blocked.push(url);
|
||||
|
||||
return route.abort();
|
||||
});
|
||||
|
||||
await page.addInitScript({ path: INIT_SCRIPT });
|
||||
await page.goto(baseURL!);
|
||||
await page.evaluate(() => window.__yjEvents.ready(20_000));
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const drawn = await page.evaluate(collectIcons);
|
||||
|
||||
expect(
|
||||
drawn.length,
|
||||
'no <wa-icon> on screen at all — the app did not render',
|
||||
).toBeGreaterThanOrEqual(EXPECTED_MIN_ICONS);
|
||||
|
||||
expect(
|
||||
drawn.filter((i) => !i.hasSvg).map((i) => i.name),
|
||||
'icons that did not draw with the network closed',
|
||||
).toEqual([]);
|
||||
|
||||
// The complementary half: nothing should have *wanted* the network.
|
||||
// An icon drawing from a warm module cache would satisfy the
|
||||
// assertion above on a machine that happens to be online.
|
||||
expect(
|
||||
blocked.filter((u) => u.includes('fontawesome')),
|
||||
'still reaching for the icon CDN',
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('every icon the app asks for is bundled', async ({ app }) => {
|
||||
for (const view of VIEWS) {
|
||||
await app.evaluate((v) => document.dispatchEvent(
|
||||
new CustomEvent('navigate', { detail: { view: v } }),
|
||||
), view);
|
||||
await app.waitForTimeout(600);
|
||||
}
|
||||
|
||||
const drawn = await app.evaluate(collectIcons);
|
||||
|
||||
expect(drawn.length).toBeGreaterThan(EXPECTED_MIN_ICONS);
|
||||
|
||||
// Covers the icons that only exist on views past the first screen,
|
||||
// which the offline test above never reaches.
|
||||
expect(
|
||||
drawn.filter((i) => !i.hasSvg).map((i) => i.name),
|
||||
'icons that did not draw on some view',
|
||||
).toEqual([]);
|
||||
|
||||
// The resolver records what it could not find rather than failing
|
||||
// silently, because twenty call sites compute their icon name from
|
||||
// state and no static check can enumerate them.
|
||||
const misses = await app.evaluate(
|
||||
() => (window as unknown as { __yjIconMisses?: string[] })
|
||||
.__yjIconMisses ?? [],
|
||||
);
|
||||
|
||||
expect(
|
||||
misses,
|
||||
'add these to frontend/src/icons/names.txt, then run: ' +
|
||||
'node frontend/scripts/fetch-icons.mjs',
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user