test(e2e): sweep the binding names a spec calls
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.
This commit is contained in:
1 parent
2ed9e725e3
commit
655434dcaa
8 files changed
+149
-46
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
import { chromium } from '@playwright/test';
|
||||||
|
const b = await chromium.launch({ executablePath: '/usr/bin/chromium' });
|
||||||
|
const p = await b.newPage();
|
||||||
|
await p.addInitScript({ path: '/mnt/vault/dev/golang/yellowjacket/.playwright/init-events.js' });
|
||||||
|
await p.goto('http://localhost:34115', { waitUntil: 'load' });
|
||||||
|
await p.evaluate(() => window.__yjEvents.ready(30000));
|
||||||
|
const out = await p.evaluate(async () => {
|
||||||
|
const time = async (label, path, args, n = 5) => {
|
||||||
|
const ms = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const t0 = performance.now();
|
||||||
|
await window.__yjEvents.call(path, args, 60000);
|
||||||
|
ms.push(performance.now() - t0);
|
||||||
|
}
|
||||||
|
ms.sort((a, b) => a - b);
|
||||||
|
return { label, medianMs: Math.round(ms[Math.floor(n / 2)]), all: ms.map((m) => Math.round(m)) };
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
await time('GetTrackTable(0)', 'library.Library.GetTrackTable', [0], 4),
|
||||||
|
await time('GetAlbums(0)', 'library.Library.GetAlbums', [0], 4),
|
||||||
|
await time('SearchLocal(tide)', 'explore.Service.SearchLocal', ['tide'], 4),
|
||||||
|
await time('SearchLocal(rock)', 'explore.Service.SearchLocal', ['rock'], 4),
|
||||||
|
await time('SearchTracks(tide)', 'library.Library.SearchTracks', ['tide', 0], 4),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(out, null, 1));
|
||||||
|
await b.close();
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Every bound method the suite names by hand must exist.
|
||||||
|
*
|
||||||
|
* A binding call carries only a method id, so a spec that names a method
|
||||||
|
* the Go side no longer has fails at *runtime*, in whichever spec
|
||||||
|
* happens to call it, with `unknown bound method name` — and nothing
|
||||||
|
* before that. `library.Library.GetTracks` was replaced by
|
||||||
|
* `GetTrackTable` (#281) and four specs kept calling the old name: the
|
||||||
|
* suite reported twenty failures across transport, bottom-bar and
|
||||||
|
* reduced-motion specs, none of which mention the library list.
|
||||||
|
*
|
||||||
|
* The known names come from `support/method-ids.mjs`, which derives them
|
||||||
|
* from the generated bindings tree that `make bindings-check` keeps
|
||||||
|
* current — so this cannot disagree with what the app can answer.
|
||||||
|
*
|
||||||
|
* It asserts first that it read something: a glob that matched nothing
|
||||||
|
* would pass over an empty list.
|
||||||
|
*/
|
||||||
|
import { readFileSync, readdirSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
import { test, expect } from '../support/fixtures.js';
|
||||||
|
import { methodIDs } from '../support/method-ids.mjs';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
/** The specs, and the harness that calls bindings on their behalf. */
|
||||||
|
const DIRS = [here, join(here, '..', 'support')];
|
||||||
|
|
||||||
|
function sources(): Array<[string, string]> {
|
||||||
|
const out: Array<[string, string]> = [];
|
||||||
|
|
||||||
|
for (const dir of DIRS) {
|
||||||
|
for (const entry of readdirSync(dir)) {
|
||||||
|
if (!entry.endsWith('.ts')) continue;
|
||||||
|
if (entry.endsWith('.spec.ts') && entry === 'binding-names.spec.ts') continue;
|
||||||
|
|
||||||
|
const path = join(dir, entry);
|
||||||
|
|
||||||
|
out.push([path, readFileSync(path, 'utf8')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `'library.Library.GetTrackTable'` — package, service, method. */
|
||||||
|
const NAMED_BINDING = /'([a-z][A-Za-z0-9]*\.[A-Z]\w*\.[A-Z]\w*)'/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Names that are deliberately not bindings.
|
||||||
|
*
|
||||||
|
* `harness.spec.ts` calls an unknown method on purpose, to assert that
|
||||||
|
* the bridge rejects with a ReferenceError naming it rather than
|
||||||
|
* hanging — that spec is the reason a bad call is loud.
|
||||||
|
*/
|
||||||
|
const DELIBERATE = new Set(['queue.Queue.Nope']);
|
||||||
|
|
||||||
|
test('every binding named by a spec exists', () => {
|
||||||
|
// `methodIDs()` is the id -> name map the recorder names calls with.
|
||||||
|
const known = new Set(methodIDs().values());
|
||||||
|
const files = sources();
|
||||||
|
|
||||||
|
// A sweep over an empty glob passes and proves nothing.
|
||||||
|
expect(files.length, 'specs and support files read').toBeGreaterThan(20);
|
||||||
|
expect(known.size, 'bound method names derived').toBeGreaterThan(100);
|
||||||
|
|
||||||
|
const unknown: string[] = [];
|
||||||
|
|
||||||
|
for (const [path, source] of files) {
|
||||||
|
for (const [, name] of source.matchAll(NAMED_BINDING)) {
|
||||||
|
if (known.has(name!) || DELIBERATE.has(name!)) continue;
|
||||||
|
|
||||||
|
unknown.push(`${name} (${path.split('/').pop()})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect([...new Set(unknown)]).toEqual([]);
|
||||||
|
});
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import { test, expect, callBinding, NO_QUEUE_SOURCE } from '../support/fixtures.js';
|
import {
|
||||||
|
test,
|
||||||
|
expect,
|
||||||
|
callBinding,
|
||||||
|
libraryTracks,
|
||||||
|
NO_QUEUE_SOURCE,
|
||||||
|
} from '../support/fixtures.js';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,15 +45,7 @@ const geometry = (app: Page) =>
|
|||||||
|
|
||||||
/** Something has to be playing before the transport draws a seek bar. */
|
/** Something has to be playing before the transport draws a seek bar. */
|
||||||
async function play(app: Page): Promise<void> {
|
async function play(app: Page): Promise<void> {
|
||||||
const paths = await app.evaluate(async () => {
|
const paths = (await libraryTracks(app)).slice(0, 3).map((t) => t.FilePath);
|
||||||
const tracks = (await window.__yjEvents.call(
|
|
||||||
'library.Library.GetTracks',
|
|
||||||
[0],
|
|
||||||
10_000,
|
|
||||||
)) as { FilePath: string }[];
|
|
||||||
|
|
||||||
return tracks.slice(0, 3).map((t) => t.FilePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
await callBinding(app, 'queue.Queue.SetQueue', [
|
await callBinding(app, 'queue.Queue.SetQueue', [
|
||||||
paths,
|
paths,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
test,
|
test,
|
||||||
expect,
|
expect,
|
||||||
|
libraryTracks,
|
||||||
callBinding,
|
callBinding,
|
||||||
openTheQueue,
|
openTheQueue,
|
||||||
NO_QUEUE_SOURCE,
|
NO_QUEUE_SOURCE,
|
||||||
@@ -56,18 +57,11 @@ async function menuLabels(app: Page): Promise<string[]> {
|
|||||||
* the wrong "no link".
|
* the wrong "no link".
|
||||||
*/
|
*/
|
||||||
async function queueThree(app: Page): Promise<void> {
|
async function queueThree(app: Page): Promise<void> {
|
||||||
const paths = await app.evaluate(async () => {
|
const tracks = await libraryTracks(app);
|
||||||
const tracks = (await window.__yjEvents.call(
|
const paths = tracks
|
||||||
'library.Library.GetTracks',
|
.filter((t) => t.Album !== '' && t.ArtistName !== '')
|
||||||
[0],
|
.slice(0, 3)
|
||||||
10_000,
|
.map((t) => t.FilePath);
|
||||||
)) as { FilePath: string; Album: string; ArtistName: string }[];
|
|
||||||
|
|
||||||
return tracks
|
|
||||||
.filter((t) => t.Album !== '' && t.ArtistName !== '')
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((t) => t.FilePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
await callBinding(app, 'queue.Queue.SetQueue', [
|
await callBinding(app, 'queue.Queue.SetQueue', [
|
||||||
paths,
|
paths,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { test, expect } from '../support/fixtures.js';
|
import { test, expect, libraryTracks } from '../support/fixtures.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The phone's transport (#59, #56).
|
* The phone's transport (#59, #56).
|
||||||
@@ -64,19 +64,15 @@ async function sizeOf(
|
|||||||
|
|
||||||
/** Put something in the queue, so the transport has a track to act on. */
|
/** Put something in the queue, so the transport has a track to act on. */
|
||||||
async function stageATrack(page: Page): Promise<void> {
|
async function stageATrack(page: Page): Promise<void> {
|
||||||
await page.evaluate(async () => {
|
const paths = (await libraryTracks(page)).slice(0, 4).map((t) => t.FilePath);
|
||||||
const tracks = (await window.__yjEvents.call(
|
|
||||||
'library.Library.GetTracks',
|
|
||||||
[0],
|
|
||||||
10_000,
|
|
||||||
)) as { FilePath: string }[];
|
|
||||||
|
|
||||||
|
await page.evaluate(async (paths) => {
|
||||||
await window.__yjEvents.call(
|
await window.__yjEvents.call(
|
||||||
'queue.Queue.SetQueue',
|
'queue.Queue.SetQueue',
|
||||||
[tracks.slice(0, 4).map((t) => t.FilePath), 0, false, { type: '', id: 0, label: '' }],
|
[paths, 0, false, { type: '', id: 0, label: '' }],
|
||||||
10_000,
|
10_000,
|
||||||
);
|
);
|
||||||
});
|
}, paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('the phone bar carries three controls', () => {
|
test.describe('the phone bar carries three controls', () => {
|
||||||
|
|||||||
@@ -40,6 +40,21 @@ const FINISH_TIMEOUT = 60_000;
|
|||||||
const libraryCalls = async (app: Page): Promise<string[]> =>
|
const libraryCalls = async (app: Page): Promise<string[]> =>
|
||||||
(await bindingCalls(app)).filter((c) => c.startsWith('library.Library.'));
|
(await bindingCalls(app)).filter((c) => c.startsWith('library.Library.'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bindings that fetch a collection.
|
||||||
|
*
|
||||||
|
* A prefix test (GetAll*) used to stand in for this, and matched
|
||||||
|
* exactly one name — GetAllLibrariesWithTrackCounts — so the assertion
|
||||||
|
* below held whatever the app refetched. The sweep in
|
||||||
|
* binding-names.spec.ts is what found it.
|
||||||
|
*/
|
||||||
|
const COLLECTION_FETCHES = [
|
||||||
|
'library.Library.GetTrackTable',
|
||||||
|
'library.Library.GetAlbums',
|
||||||
|
'library.Library.GetArtists',
|
||||||
|
'library.Library.GetGenres',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select rows by dispatching on the row rather than clicking it.
|
* Select rows by dispatching on the row rather than clicking it.
|
||||||
*
|
*
|
||||||
@@ -116,7 +131,7 @@ test.describe('a finished track', () => {
|
|||||||
const refetched = await libraryCalls(app);
|
const refetched = await libraryCalls(app);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
refetched.filter((c) => c.startsWith('library.Library.GetAll')),
|
refetched.filter((c) => COLLECTION_FETCHES.includes(c)),
|
||||||
'a play refetched a collection',
|
'a play refetched a collection',
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
test,
|
test,
|
||||||
expect,
|
expect,
|
||||||
callBinding,
|
callBinding,
|
||||||
|
libraryTracks,
|
||||||
waitForEvent,
|
waitForEvent,
|
||||||
NO_QUEUE_SOURCE,
|
NO_QUEUE_SOURCE,
|
||||||
} from '../support/fixtures.js';
|
} from '../support/fixtures.js';
|
||||||
@@ -56,20 +57,9 @@ async function playTheLongOne(app: Page): Promise<void> {
|
|||||||
window.dispatchEvent(new CustomEvent('yj-scroll-mode-changed'));
|
window.dispatchEvent(new CustomEvent('yj-scroll-mode-changed'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const paths: string[] = await app.evaluate(async (needle) => {
|
const paths: string[] = (await libraryTracks(app))
|
||||||
// One argument, and 0 means every library: the scoped and
|
.filter((t) => t.TrackName.startsWith(LONG_TITLE))
|
||||||
// unscoped list queries collapsed into one when the schema did
|
.map((t) => t.FilePath);
|
||||||
// (plan 013 R3), so `GetTracks()` no longer exists to call.
|
|
||||||
const tracks = await window.__yjEvents.call(
|
|
||||||
'library.Library.GetTracks',
|
|
||||||
[0],
|
|
||||||
10_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (tracks as { TrackName: string; FilePath: string }[])
|
|
||||||
.filter((t) => t.TrackName.startsWith(needle))
|
|
||||||
.map((t) => t.FilePath);
|
|
||||||
}, LONG_TITLE);
|
|
||||||
|
|
||||||
expect(paths.length).toBeGreaterThan(0);
|
expect(paths.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export async function callBinding<T = unknown>(
|
|||||||
export interface LibraryTrack {
|
export interface LibraryTrack {
|
||||||
FilePath: string;
|
FilePath: string;
|
||||||
TrackName: string;
|
TrackName: string;
|
||||||
|
ArtistName: string;
|
||||||
Album: string;
|
Album: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +117,14 @@ export async function libraryTracks(page: Page): Promise<LibraryTrack[]> {
|
|||||||
strings: string[];
|
strings: string[];
|
||||||
filePath: string[];
|
filePath: string[];
|
||||||
trackName: number[];
|
trackName: number[];
|
||||||
|
artistName: number[];
|
||||||
album: number[];
|
album: number[];
|
||||||
}>(page, 'library.Library.GetTrackTable', [0]);
|
}>(page, 'library.Library.GetTrackTable', [0]);
|
||||||
|
|
||||||
return (t.filePath ?? []).map((FilePath, i) => ({
|
return (t.filePath ?? []).map((FilePath, i) => ({
|
||||||
FilePath,
|
FilePath,
|
||||||
TrackName: t.strings[t.trackName[i]!] ?? '',
|
TrackName: t.strings[t.trackName[i]!] ?? '',
|
||||||
|
ArtistName: t.strings[t.artistName[i]!] ?? '',
|
||||||
Album: t.strings[t.album[i]!] ?? '',
|
Album: t.strings[t.album[i]!] ?? '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user