make e2e is green on chromium: 92 passed. The harness is rebuilt on
what v3 actually offers, and three of the four things it replaced turn
out to be better than what they replaced.
The headless launch is v3's own server mode. scripts/dev-headless.sh
ran a `-tags dev` binary whose app_dev.go parsed -devserver/-assetdir
out of os.Args; that file went with v2, so the harness had no server at
all. `-tags dev,server` is a first-class mode and needs no display, so
Xvfb is gone from the script and from CI.
The bridge hooks two places, neither of them EventsOn. Inbound is
window._wails.dispatchWailsEvent, wrapped by pre-creating the object
the runtime keeps and putting an accessor on the one property.
Outbound is fetch: v3 routes every runtime call through one POST, so
the bridge sees binding calls and event emits from any module, needs no
walk of an object graph, and cannot miss a call made before it looked.
__yjEvents.call posts to that endpoint by method name, so it depends on
nothing in the app's bundle and works on a page with no init script.
That is what lets seed-sandbox.sh drop playwright-cli entirely — it
drove AddLibrary through a browser only because window.go was v2's one
way in — and with it a global npm install and a second Chromium in CI.
measure.mjs and one spec lose their window.go walks and read the
bridge's log instead; e2e/support/method-ids.mjs derives id -> name
from frontend/bindings/ (phase 6b option 1, so it cannot go stale
silently). Plain .mjs because measure.mjs runs under bare node and one
derivation beats two that can disagree.
Four bugs surfaced, and the migration is how.
The cross-service wiring never ran headless. It hung off
Common.ApplicationStarted, which server mode never emits —
setupCommonEvents is an explicit no-op there — so the queue had no
TrackLoader and playing a track changed the queue and then silently did
nothing. It is a service registered last now (backend/startup.go):
services start in registration order, which is the ordering the wiring
needs, in every mode.
Six specs called SetQueue with 3 of its 4 arguments. v2 accepted that
and filled the gap; v3 answers "expects 4 arguments, got 3".
requested-badge's cleanup read window.go and returned early on
`if (!svc)` — the silent cleanup its own comment was written to
prevent, one migration later. It posts to the runtime endpoint now,
which any page can do.
SearchIndex.Search trusted a startup latch, so rows a spec staged
afterwards were unsearchable and three specs passed only when an
earlier one happened to flip it. shelves.go fixed exactly this and left
hasCatalogRows behind; the search path now uses it as the fallback,
with the latch still the fast path.
Two spec edits are deletions of assertions about v2. harness.spec
checked Object.keys(window.go) and that a bad call *hung*; it now
checks the real runtime is loaded and that the backend rejects with a
TypeError naming the argument. album-actions asserted a tracklist
legend that dcc40b1 deleted on main — that spec has been failing since,
and what replaced it is covered in frontend/test/components.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
import { test, expect, callBinding } from '../support/fixtures.js';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* Plan 007 phase 5: `H-13` — the album page can be played from.
|
|
*
|
|
* The page is `explore-album-details`; there is no library-side album
|
|
* detail page at all, so this catalog page is where a Play button has
|
|
* to live, and what it can honestly claim depends on how much of the
|
|
* album the user owns.
|
|
*
|
|
* What this tier adds over the component tests is that the paths
|
|
* resolve to something the queue accepts. The first version of this
|
|
* feature keyed the lookup on recording MBIDs — which is how the
|
|
* backend decides a track is `inLibrary` — and a library-only album has
|
|
* none, so Play was wired, labelled correctly, clicked cleanly and
|
|
* queued **nothing**. Every component test still passed.
|
|
*/
|
|
// There is no test for the tracklist legend, and there should not be:
|
|
// `dcc40b1` inverted the mark — rows *not* in the library are dimmed in
|
|
// place and nothing marks the ones that are — and deleted
|
|
// `.tracklist-legend` with it. The spec asserting it survived that
|
|
// commit and has been failing on main ever since. What replaced it
|
|
// (the dimming, and the `aria-disabled` that carries it to anyone not
|
|
// seeing the page) is covered at the component tier, in
|
|
// frontend/test/components/album-actions.test.ts.
|
|
test.describe('playing an album from its page', () => {
|
|
test.beforeEach(async ({ app }) => {
|
|
await openFirstAlbum(app);
|
|
});
|
|
|
|
test.afterEach(async ({ app }) => {
|
|
// The suite shares one backend process in file order, and a queue
|
|
// left full is state the next spec did not ask for.
|
|
await callBinding(app, 'queue.Queue.Clear', []);
|
|
await app.getByTestId('nav-tracks').click();
|
|
});
|
|
|
|
test('Play queues what the user owns of it', async ({ app }) => {
|
|
const play = app
|
|
.locator('explore-album-details')
|
|
.locator('[data-testid="album-play"]');
|
|
|
|
// The fixture library is untagged, so this album is wholly local
|
|
// and the button carries no count.
|
|
await expect(play).toContainText('Play');
|
|
|
|
await play.click();
|
|
|
|
await expect.poll(() => queueLength(app)).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('Add to queue appends rather than replacing', async ({ app }) => {
|
|
const details = app.locator('explore-album-details');
|
|
|
|
await details.locator('[data-testid="album-play"]').click();
|
|
await expect.poll(() => queueLength(app)).toBeGreaterThan(0);
|
|
|
|
const before = await queueLength(app);
|
|
|
|
await details.locator('[data-testid="album-queue"]').click();
|
|
|
|
await expect.poll(() => queueLength(app)).toBe(before * 2);
|
|
});
|
|
|
|
test('the ticks are badges, not keyboard stops', async ({ app }) => {
|
|
// Every one of them was a <button> whose click handler was a
|
|
// stopPropagation() and a comment saying to wire up the download
|
|
// client later: on an Explore results page, 20 of 66 tab stops
|
|
// promised an action and performed none. Counted here rather than
|
|
// reasoned about, because the count is the finding.
|
|
const stops = await app.evaluate(() => {
|
|
const badges = [
|
|
...(document
|
|
.querySelector('explore-album-details')
|
|
?.shadowRoot?.querySelectorAll('library-status-indicator') ?? []),
|
|
];
|
|
|
|
return {
|
|
badges: badges.length,
|
|
focusable: badges.filter((b) =>
|
|
b.shadowRoot?.querySelector('button, [tabindex]:not([tabindex="-1"])'),
|
|
).length,
|
|
labelled: badges.filter((b) =>
|
|
b.shadowRoot?.querySelector('[role="img"][aria-label]'),
|
|
).length,
|
|
};
|
|
});
|
|
|
|
expect(stops.badges).toBeGreaterThan(0);
|
|
expect(stops.focusable).toBe(0);
|
|
expect(stops.labelled).toBe(stops.badges);
|
|
});
|
|
});
|
|
|
|
/** Albums → click the second card, which navigates to the album page. */
|
|
async function openFirstAlbum(app: Page): Promise<void> {
|
|
await app.getByTestId('nav-albums').click();
|
|
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
|
'data-active-view',
|
|
'albums',
|
|
);
|
|
|
|
// The cards come from a virtualizer, so they are not there when the
|
|
// view is: a click dispatched into an empty grid hits nothing and
|
|
// silently leaves the app on Albums, which reads as a broken
|
|
// navigation rather than a race.
|
|
await expect.poll(() => cardCount(app)).toBeGreaterThan(1);
|
|
|
|
// A plain click on a card navigates here; Enter expands the dropdown
|
|
// instead. Dispatched rather than clicked because the card lives in a
|
|
// virtualizer inside a shadow root.
|
|
await app.evaluate(() => {
|
|
document
|
|
.querySelector('cover-grid')
|
|
?.shadowRoot?.querySelectorAll('.album-card')[1]
|
|
?.dispatchEvent(
|
|
new MouseEvent('click', { bubbles: true, composed: true }),
|
|
);
|
|
});
|
|
|
|
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
|
'data-active-view',
|
|
'explore-album-details',
|
|
);
|
|
await expect(
|
|
app.locator('explore-album-details').locator('[data-testid="album-play"]'),
|
|
).toBeVisible();
|
|
}
|
|
|
|
async function cardCount(app: Page): Promise<number> {
|
|
return app.evaluate(
|
|
() =>
|
|
document
|
|
.querySelector('cover-grid')
|
|
?.shadowRoot?.querySelectorAll('.album-card').length ?? 0,
|
|
);
|
|
}
|
|
|
|
async function queueLength(app: Page): Promise<number> {
|
|
const state = (await callBinding(app, 'queue.Queue.GetState', [])) as {
|
|
tracks?: unknown[];
|
|
};
|
|
|
|
return state?.tracks?.length ?? 0;
|
|
}
|