e7748f1 made double-click and single-row Play queue the list as
displayed with startIndex on that row; this frozen spec still asserted
a queue of one, and was the only failure in both the chromium and
webkit runs on main.
It asserts the new contract instead: more than one track queued,
currentIndex on the row that was activated, and the panel showing that
queue rather than some other one.
187 lines
6.5 KiB
TypeScript
187 lines
6.5 KiB
TypeScript
import {
|
|
test,
|
|
expect,
|
|
callBinding,
|
|
resetEvents,
|
|
waitForEvent,
|
|
LONG_TRACK,
|
|
} from '../support/fixtures.js';
|
|
|
|
/** The one fixture long enough to still be playing on the next line. */
|
|
const longRow = (app: import('@playwright/test').Page) =>
|
|
app.getByTestId('track-row').filter({ hasText: LONG_TRACK }).first();
|
|
|
|
/**
|
|
* Playback and the queue, driven through the UI and asserted on the
|
|
* events the backend actually emits.
|
|
*
|
|
* Audio really is initialised here: under `dbus-run-session` + Xvfb the
|
|
* PulseAudio socket in /run/user is untouched, so InitSpeaker succeeds
|
|
* and these tracks genuinely play. A CI container without /run/user
|
|
* needs a null sink; everything except the audio itself still works
|
|
* without one.
|
|
*/
|
|
test.describe('playback', () => {
|
|
test.beforeEach(async ({ app }) => {
|
|
// The app lands on Home now (H-8), so the track list is a
|
|
// navigation away rather than the first thing on screen.
|
|
await app.getByTestId('nav-tracks').click();
|
|
await callBinding(app, 'queue.Queue.Clear').catch(() => {
|
|
/* older builds may not expose Clear; the specs below do not need it */
|
|
});
|
|
await resetEvents(app);
|
|
});
|
|
|
|
test('double-clicking a track plays it', async ({ app }) => {
|
|
await longRow(app).dblclick();
|
|
|
|
const changed = await waitForEvent(app, 'TrackChanged');
|
|
|
|
expect(changed.data[0]).toBeTruthy();
|
|
|
|
// The transport flips to Pause, which is the only place the UI
|
|
// states "we are playing" in a way a user can see. `exact` is not
|
|
// optional: "Add queue to playlist" also matches /play/i.
|
|
await expect(
|
|
app.getByRole('button', { name: 'Pause', exact: true }),
|
|
).toBeVisible();
|
|
|
|
await expect(app.getByTestId('now-playing-title')).toContainText(
|
|
LONG_TRACK,
|
|
);
|
|
});
|
|
|
|
test('the elapsed time advances', async ({ app }) => {
|
|
await longRow(app).dblclick();
|
|
await waitForEvent(app, 'TrackChanged');
|
|
|
|
// Not a fixed sleep on a fixed value: assert the observable
|
|
// outcome, which is that the clock is no longer at zero.
|
|
await expect(app.getByTestId('elapsed-time')).not.toHaveText('--:--');
|
|
await expect(app.getByTestId('elapsed-time')).not.toHaveText('00:00', {
|
|
timeout: 15_000,
|
|
});
|
|
});
|
|
|
|
test('pause and play round-trip through the backend', async ({ app }) => {
|
|
await longRow(app).dblclick();
|
|
await waitForEvent(app, 'TrackChanged');
|
|
|
|
await resetEvents(app);
|
|
await app.getByRole('button', { name: 'Pause', exact: true }).click();
|
|
await waitForEvent(app, 'PlaybackStateChanged');
|
|
|
|
await expect(
|
|
app.getByRole('button', { name: 'Play', exact: true }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('volume changes are pushed back from Go', async ({ app }) => {
|
|
await resetEvents(app);
|
|
await callBinding(app, 'player.Player.SetVolume', [55]);
|
|
|
|
const ev = await waitForEvent(app, 'VolumeChanged');
|
|
|
|
expect(ev.data).toEqual([55]);
|
|
});
|
|
});
|
|
|
|
test.describe('queue', () => {
|
|
test.beforeEach(async ({ app }) => {
|
|
// The app lands on Home now (H-8).
|
|
await app.getByTestId('nav-tracks').click();
|
|
});
|
|
|
|
test('playing a track queues the list it is in', async ({ app }) => {
|
|
await resetEvents(app);
|
|
await longRow(app).dblclick();
|
|
await waitForEvent(app, 'QueueChanged');
|
|
|
|
// The panel has to be open to have rows. A closed one is `width: 0`
|
|
// and now renders no list at all (perf.m7) — before that it kept a
|
|
// virtualizer measuring its window on every queue change, and this
|
|
// assertion passed against a panel nobody could see.
|
|
const queueToggle = app.getByRole('button', { name: 'Toggle queue' });
|
|
|
|
await queueToggle.click();
|
|
|
|
// Activating one row plays the list that row is in, from that row —
|
|
// the library as displayed, not a queue of one. The row's own
|
|
// position is what `currentIndex` points at.
|
|
const state = await callBinding<{
|
|
tracks: { title: string }[];
|
|
currentIndex: number;
|
|
}>(app, 'queue.Queue.GetState');
|
|
|
|
expect(state.tracks.length).toBeGreaterThan(1);
|
|
expect(state.tracks[state.currentIndex]?.title).toContain(LONG_TRACK);
|
|
|
|
// The panel shows that queue rather than some other one: every
|
|
// fixture track fits on screen, so the counts are comparable.
|
|
await expect(app.getByTestId('queue-row')).toHaveCount(
|
|
state.tracks.length,
|
|
);
|
|
|
|
// Shut it again, and wait until it really is shut. These specs
|
|
// share one backend process in file order, the panel's width is
|
|
// animated, and the transport slides while it closes — a click
|
|
// issued during that lands on whichever button has moved under the
|
|
// pointer, which for the very next test was Repeat rather than
|
|
// Shuffle. Both emit QueueModeChanged, so it failed on the
|
|
// assertion rather than on the wait, one run in two.
|
|
//
|
|
// Waiting on the row count rather than a timeout is also the m7
|
|
// assertion: a closed panel renders no list at all.
|
|
await queueToggle.click();
|
|
await expect(app.getByTestId('queue-row')).toHaveCount(0);
|
|
});
|
|
|
|
test('shuffle and repeat toggles report their state', async ({ app }) => {
|
|
const shuffle = app.getByRole('button', { name: 'Shuffle' });
|
|
|
|
await resetEvents(app);
|
|
await shuffle.click();
|
|
await waitForEvent(app, 'QueueModeChanged');
|
|
|
|
await expect(shuffle).toHaveAttribute('aria-pressed', 'true');
|
|
|
|
await shuffle.click();
|
|
await expect(shuffle).toHaveAttribute('aria-pressed', 'false');
|
|
});
|
|
});
|
|
|
|
test.describe('the playing row is findable without colour vision', () => {
|
|
test('the queue marks its current row with a shape and aria-current', async ({
|
|
app,
|
|
}) => {
|
|
await app.getByTestId('nav-tracks').click();
|
|
await app.getByTestId('track-row').first().dblclick();
|
|
|
|
const queueToggle = app.locator('#queue-button');
|
|
|
|
await queueToggle.click();
|
|
|
|
const row = app.getByTestId('queue-row').first();
|
|
|
|
await expect(row).toBeVisible();
|
|
|
|
// Played *from the queue*, because a track started from the list
|
|
// leaves `currentIndex` at -1 — so the panel has no current row at
|
|
// all in that flow, which is what made this marker look broken the
|
|
// first time it was checked.
|
|
await row.dblclick();
|
|
await expect(row).toHaveAttribute('aria-current', 'true');
|
|
|
|
const marker = await row.evaluate(
|
|
(el) => getComputedStyle(el, '::before').borderLeftWidth,
|
|
);
|
|
|
|
// `a11y.22`: a background tint and a text colour were the only two
|
|
// signals, and both are hue (WCAG 1.4.1).
|
|
expect(parseFloat(marker)).toBeGreaterThan(0);
|
|
|
|
await queueToggle.click();
|
|
await expect(app.getByTestId('queue-row')).toHaveCount(0);
|
|
});
|
|
});
|