GetTracks answered with one JSON object per track: 20.5 MB at 26k tracks, ~350 bytes a row of repeated key names, four cover URLs that are identical across an album, and the album, artist and genre strings repeated on every track. Encoding it was ~170 MB of transient Go allocation, and parsing it the WebView's memory peak. GetTrackTable replaces it: one array per column, every repeated string stored once and sent as an index, genre lists interned, and only the fields the Tracks view reads. LastPlayed and the three larger cover tiers are left to the details dialog, which now fetches whole tracks by path. Each row still goes through trackFromRow, so this is an encoding of the one projection, not a second one. 167 bytes a track against 929 in the test library; a Go test holds it under a quarter of the object encoding, and a Vitest test reads the Go columns and the generated Track interface and fails if they drift. An empty library is now an empty table rather than the "no tracks in library" error the old binding returned. Closes #281
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import {
|
|
libraryTracks,
|
|
test,
|
|
expect,
|
|
callBinding,
|
|
resetEvents,
|
|
waitForEvent,
|
|
LONG_TRACK,
|
|
NO_QUEUE_SOURCE,
|
|
} from '../support/fixtures.js';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* The phone's progress line (#58).
|
|
*
|
|
* The component tier already pins what the line *says* — that it
|
|
* renders the backend's reported position and never a count of its own.
|
|
* What only a real shell can answer is **where it is**: the issue asks
|
|
* for a line on the border between the mini player and the tab bar, and
|
|
* "on the border" is two adjacencies in a grid that no component-level
|
|
* render has around it.
|
|
*
|
|
* It also asserts the line is not there on a desktop, which is the
|
|
* other half of the same fact: above 600px there is no tab bar for it
|
|
* to sit on the border of, and the bar carries a real seek bar.
|
|
*/
|
|
type Rect = { x: number; y: number; width: number; height: number };
|
|
|
|
/** The reference device's real viewport. */
|
|
const DEVICE = { width: 424, height: 439 };
|
|
const DESKTOP = { width: 1280, height: 800 };
|
|
|
|
async function rectOf(app: Page, selector: string): Promise<Rect | null> {
|
|
return app.evaluate((sel) => {
|
|
const el = document.querySelector(sel);
|
|
|
|
if (!el) return null;
|
|
|
|
const r = el.getBoundingClientRect();
|
|
|
|
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
}, selector);
|
|
}
|
|
|
|
/**
|
|
* Put the 90-second fixture on and wait for the first position report.
|
|
*
|
|
* The long track rather than any track: every other fixture is 2-6
|
|
* seconds, which is shorter than the time this spec takes to measure
|
|
* three rectangles.
|
|
*/
|
|
async function play(app: Page): Promise<void> {
|
|
const tracks = await libraryTracks(app);
|
|
|
|
// `TrackName`, not `Title`: that is what the library model calls it.
|
|
const long = tracks.find((t) => t.TrackName === LONG_TRACK);
|
|
|
|
expect(long, `no fixture track named ${LONG_TRACK}`).toBeTruthy();
|
|
|
|
await callBinding(app, 'queue.Queue.Clear');
|
|
await resetEvents(app);
|
|
await callBinding(app, 'queue.Queue.SetQueue', [
|
|
[long!.FilePath],
|
|
0,
|
|
false,
|
|
NO_QUEUE_SOURCE,
|
|
]);
|
|
await waitForEvent(app, 'QueueChanged');
|
|
await callBinding(app, 'queue.Queue.Play');
|
|
await waitForEvent(app, 'PlaybackPositionChanged', { timeoutMs: 15_000 });
|
|
}
|
|
|
|
test.describe('the progress line sits on the border between the bars', () => {
|
|
test.beforeEach(async ({ app }) => {
|
|
await app.setViewportSize(DEVICE);
|
|
await play(app);
|
|
});
|
|
|
|
/*
|
|
* Every test here starts a LONG_TRACK and the suite is workers: 1,
|
|
* fullyParallel: false against one long-lived app — so without this
|
|
* the four phone-* specs that follow alphabetically inherit a playing
|
|
* queue. phone-transport.spec.ts records where that lesson came from:
|
|
* the fault first showed up as a flake in a spec about something else.
|
|
*/
|
|
test.afterEach(async ({ app }) => {
|
|
await callBinding(app, 'queue.Queue.Clear').catch(() => {
|
|
/* already empty */
|
|
});
|
|
await app.setViewportSize(DESKTOP);
|
|
});
|
|
|
|
test('spans the width, between the mini player and the tab bar', async ({
|
|
app,
|
|
}) => {
|
|
const line = await rectOf(app, 'player-progress-line');
|
|
const bar = await rectOf(app, '.bottom-bar');
|
|
const nav = await rectOf(app, 'bottom-nav');
|
|
|
|
expect(line, 'no progress line on the phone').not.toBeNull();
|
|
expect(bar).not.toBeNull();
|
|
expect(nav).not.toBeNull();
|
|
|
|
// A border, not a band: 2px, the full width, and touching both.
|
|
expect(line!.height).toBeCloseTo(2, 0);
|
|
expect(line!.width).toBeCloseTo(bar!.width, 0);
|
|
expect(line!.y).toBeCloseTo(bar!.y + bar!.height, 0);
|
|
expect(nav!.y).toBeCloseTo(line!.y + line!.height, 0);
|
|
});
|
|
|
|
/**
|
|
* It is 2px on the top edge of the tab bar, which is exactly where a
|
|
* thumb aiming at a tab lands. A line that sometimes seeks is worse
|
|
* than one that never does, so it must take no part in hit testing
|
|
* at all.
|
|
*/
|
|
test('takes no taps', async ({ app }) => {
|
|
const line = await rectOf(app, 'player-progress-line');
|
|
|
|
const hit = await app.evaluate(
|
|
({ x, y }) => document.elementFromPoint(x, y)?.tagName ?? '',
|
|
{ x: line!.x + line!.width / 2, y: line!.y + 1 },
|
|
);
|
|
|
|
expect(hit).not.toBe('PLAYER-PROGRESS-LINE');
|
|
});
|
|
|
|
test('is not there on a desktop', async ({ app }) => {
|
|
await app.setViewportSize(DESKTOP);
|
|
|
|
await expect(app.locator('player-progress-line')).toBeHidden();
|
|
});
|
|
});
|