import { 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 { 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 { const tracks = await callBinding<{ FilePath: string; TrackName: string }[]>( app, 'library.Library.GetTracks', [0], ); // `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(); }); });