Four specs still called `library.Library.GetTracks` after #281 replaced it, and the suite reported twenty failures across transport, bottom-bar and reduced-motion specs — none of which mention the library list. A binding call carries only a method id, so a stale name fails at runtime in whichever spec happens to call it. `binding-names.spec.ts` reads every spec and harness file, extracts the names they pass as strings, and checks them against the map derived from the generated bindings tree. It asserts first that it read something: a sweep over an empty glob passes and proves nothing. It immediately found a second thing: `play-count.spec.ts` asserted that a play refetched no collection by matching a `GetAll` prefix, which matches exactly one real name (`GetAllLibrariesWithTrackCounts`) — so the assertion held whatever the app refetched. It names the four collection bindings now.
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import {
|
|
test,
|
|
expect,
|
|
callBinding,
|
|
libraryTracks,
|
|
waitForEvent,
|
|
NO_QUEUE_SOURCE,
|
|
} from '../support/fixtures.js';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* `a11y.15` — WCAG 2.2.2. The bottom bar's title and artist scroll
|
|
* continuously while a track plays, re-armed in a loop by
|
|
* `transitionend`, with no pause mechanism and no reduced-motion guard.
|
|
*
|
|
* The component test for this fakes `window.matchMedia`, which is a
|
|
* stub of the thing being tested. This spec sets the real context
|
|
* option, so the real media query answers.
|
|
*
|
|
* Both directions are here on purpose. A guard that suppressed
|
|
* everything would pass the reduce case for free, and so would a bar
|
|
* whose text simply does not overflow at this viewport — which is what
|
|
* the component test failed on first.
|
|
*/
|
|
|
|
/** The fixture track whose title is long enough to overflow the bar. */
|
|
const LONG_TITLE = 'An Exhaustively Overlong Track Title';
|
|
|
|
/**
|
|
* Read the title line's classes from inside `now-playing`'s shadow root.
|
|
*
|
|
* `will-scroll` is the class that carries both the transition and the
|
|
* `padding-right` the scroll distance is measured against, so its
|
|
* absence is the whole fix: suppressing only the animation leaves the
|
|
* text translated off its own box with nothing to bring it back.
|
|
*/
|
|
async function titleClasses(app: Page): Promise<string> {
|
|
return app.evaluate(() => {
|
|
const np = document.querySelector('now-playing');
|
|
const title = np?.shadowRoot?.querySelector('.track-title');
|
|
|
|
return title?.className ?? '';
|
|
});
|
|
}
|
|
|
|
async function playTheLongOne(app: Page): Promise<void> {
|
|
await app.getByTestId('nav-tracks').click();
|
|
|
|
// The scroll mode defaults to `hover`, and a fresh context has no
|
|
// persisted setting — so without this the positive case never
|
|
// scrolls and reports the same thing a broken build would. Set it
|
|
// rather than hovering, because `always` is also the mode the
|
|
// finding is about: continuous motion for as long as the track
|
|
// plays, with nothing the user has to do to provoke it.
|
|
await app.evaluate(() => {
|
|
localStorage.setItem('yj-now-playing-scroll-mode', 'always');
|
|
window.dispatchEvent(new CustomEvent('yj-scroll-mode-changed'));
|
|
});
|
|
|
|
const paths: string[] = (await libraryTracks(app))
|
|
.filter((t) => t.TrackName.startsWith(LONG_TITLE))
|
|
.map((t) => t.FilePath);
|
|
|
|
expect(paths.length).toBeGreaterThan(0);
|
|
|
|
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
|
await waitForEvent(app, 'TrackChanged');
|
|
|
|
// The scroll cycle is armed 1500 ms after the geometry is measured,
|
|
// and the geometry is measured after the render that puts the title
|
|
// on screen. Reading before that reports "not scrolling" on a build
|
|
// that scrolls — the same shape as every probe read too early in
|
|
// plan 007.
|
|
await expect
|
|
.poll(() => titleClasses(app), { timeout: 10_000 })
|
|
.toContain('track-title');
|
|
}
|
|
|
|
test.describe('the marquee under prefers-reduced-motion', () => {
|
|
test.use({ contextOptions: { reducedMotion: 'reduce' } });
|
|
|
|
test('does not scroll the now-playing text at all', async ({ app }) => {
|
|
await playTheLongOne(app);
|
|
|
|
// Give the cycle longer than the 1500 ms arming delay to prove it
|
|
// never arms, rather than catching it before it would have.
|
|
await app.waitForTimeout(2500);
|
|
|
|
expect(await titleClasses(app)).not.toContain('will-scroll');
|
|
});
|
|
});
|
|
|
|
test.describe('the marquee without a motion preference', () => {
|
|
test.use({ contextOptions: { reducedMotion: 'no-preference' } });
|
|
|
|
test('still scrolls an overflowing title', async ({ app }) => {
|
|
await playTheLongOne(app);
|
|
|
|
await expect
|
|
.poll(() => titleClasses(app), { timeout: 10_000 })
|
|
.toContain('will-scroll');
|
|
});
|
|
});
|