The app opened on Tracks — an alphabetical list of everything, which is the one entry point that is identical every time and therefore gives the user nothing to start from. Home is listed first in the nav and is the page built to answer 'what should I play' (H-8). Two things had to be true before that was an improvement. An album with no cover rendered as a small dim icon on a surface the same colour as the page, so a shelf read as having holes in it, while the Albums and Artists grids both drew a letter tile (H-9). It draws the same tile now. And a shelf that repeats the one above it is suppressed, the way an empty one already is — 'On repeat' was 'Pick up where you left off' reordered. The rule fires only when the shelf is not showing the whole library: a repeat is a fault only if a different row was possible, and measured against a fixed shelf size instead this let an 11-album library keep three identical shelves while a 13-album one lost them. The first two versions of that rule were wrong and the *existing* Go tests caught both — it collapsed a four-album library to a single shelf. Nine e2e specs assumed the app starts on Tracks and now navigate there, and one new spec freezes the landing itself. Home's page-header action is 'Shuffle suggestions': 'Shuffle' alone was two different controls with one accessible name, which only became reachable together once a cached Home was always in the tree.
144 lines
4.9 KiB
TypeScript
144 lines
4.9 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 populates the queue panel', 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();
|
|
|
|
await expect(app.getByTestId('queue-row')).toHaveCount(1);
|
|
|
|
const state = await callBinding<{ tracks: unknown[] }>(
|
|
app,
|
|
'queue.Queue.GetState',
|
|
);
|
|
|
|
expect(state.tracks).toHaveLength(1);
|
|
|
|
// 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');
|
|
});
|
|
});
|