Four of these eight fail on the build before the fix, with the numbers the issue is about: 263x39 at 424x439, 358x315 at 390x700, 300x36 at 900x500, and no row at all below 500px. The fifth viewport, 412x869, passes on both -- which is the boundary landing exactly where the arithmetic says it should, since the leftover only exceeds the width above ~843. Three of them cannot fail on the old build and are said to be guards rather than evidence: that the transport does not scroll off (the old build shrank the art instead, so it did not scroll either), that a tall phone keeps its column, and -- after a first draft that passed on the defect because the subtraction went negative -- a floor on the art at the device's own viewport instead of a comparison with a layout that is no longer there. The wait is on the arrangement rather than on a non-zero box: a previous test leaves the other layout on screen and a stale column satisfies "has a size" perfectly, which showed up as one test passing alone and failing in file order. What this tier cannot see is the device's engine. Nothing here depends on Chrome 113 behaviour -- the sizing rules were chosen by measuring that engine directly, and the numbers are on the issue.
268 lines
9.2 KiB
TypeScript
268 lines
9.2 KiB
TypeScript
import { test, expect } from '../support/fixtures.js';
|
|
|
|
/**
|
|
* Now Playing on a short screen (#51).
|
|
*
|
|
* #51 asks for a layout that "survives" ~424x439 with the controls
|
|
* never scrolling off. #172 measured why it did not — the stacked
|
|
* layout's budget is fixed, so the art gets whatever is left, and that
|
|
* was 39px before #64 and 53px after it.
|
|
*
|
|
* **Two separate claims are asserted here, and only one of them is
|
|
* about the phone.**
|
|
*
|
|
* The first is that the art is *square*. It was not: `aspect-ratio` is
|
|
* specified not to re-derive the width when `max-height` clamps the
|
|
* height, so the art was drawn as a letterbox band and `object-fit:
|
|
* cover` cropped the cover to it — 264x53 on the reference device. The
|
|
* leftover only exceeds the width above ~843px of viewport, so this
|
|
* was every height from ~500 to ~843 as well: most phones, and any
|
|
* short window. That is ordinary CSS rather than a Chrome 113 quirk,
|
|
* so this tier can see it, and the heights below are chosen to cover
|
|
* the range rather than the one device.
|
|
*
|
|
* The second is the reflow: below 500px the art and the names sit side
|
|
* by side, which is what takes the art from 53px to 143px. That is
|
|
* asserted as a *relation between boxes* — the art beside the names,
|
|
* not above them — because the pixel count is a consequence of the
|
|
* arrangement and would pin this file to one device's chrome.
|
|
*
|
|
* **What this tier cannot see** is the device's engine: CI's Chromium
|
|
* and WebKit are current, and #60's clipping showed what that costs.
|
|
* Nothing here depends on Chrome 113 behaviour — the sizing rules were
|
|
* checked against the device itself, at column heights of 288, 300,
|
|
* 451, 600 and 800, and the numbers are on #51.
|
|
*/
|
|
type Page = import('@playwright/test').Page;
|
|
|
|
/** The reference device's real viewport. */
|
|
const DEVICE = { width: 424, height: 439 };
|
|
|
|
/**
|
|
* A tall phone, above the reflow's 500px. Roughly a Pixel 7, which is
|
|
* #51's other named device and was not attached — so what is checked
|
|
* here is the layout it *should* get, not that device.
|
|
*/
|
|
const TALL_PHONE = { width: 412, height: 869 };
|
|
|
|
/** Inside the crop's old range and above the reflow: a short window. */
|
|
const SHORT_WINDOW = { width: 390, height: 700 };
|
|
|
|
/**
|
|
* The height the layout reflows at. Written down once here because the
|
|
* specs have to know which arrangement to *wait* for, not only which
|
|
* to assert.
|
|
*/
|
|
const REFLOW_AT = 500;
|
|
|
|
/** Put a track in the player, so the view has art and names to lay out. */
|
|
async function stageATrack(page: Page): Promise<void> {
|
|
await page.evaluate(async () => {
|
|
const tracks = (await window.__yjEvents.call(
|
|
'library.Library.GetTracks',
|
|
[0],
|
|
10_000,
|
|
)) as { FilePath: string }[];
|
|
|
|
await window.__yjEvents.call(
|
|
'queue.Queue.SetQueue',
|
|
[tracks.slice(0, 4).map((t) => t.FilePath), 0, false, { type: '', id: 0, label: '' }],
|
|
10_000,
|
|
);
|
|
});
|
|
}
|
|
|
|
/** Open the full-screen view and wait for the shell to say so. */
|
|
async function openNowPlaying(page: Page): Promise<void> {
|
|
await page.evaluate(() => {
|
|
document.dispatchEvent(
|
|
new CustomEvent('navigate', {
|
|
detail: { view: 'now-playing' },
|
|
bubbles: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
await expect(page.getByTestId('main-content')).toHaveAttribute(
|
|
'data-active-view',
|
|
'now-playing',
|
|
);
|
|
|
|
// The attribute is the shell's bookkeeping and lands before the view
|
|
// has a track, so measuring on it alone races the first layout --
|
|
// which showed up as a 60x5 art on the first spec of a cold run.
|
|
//
|
|
// Waiting for a non-zero box is not enough on its own either: a
|
|
// previous test leaves the *other* arrangement on screen, and a
|
|
// stale column satisfies "has a size" perfectly. So the wait is for
|
|
// the arrangement this viewport should have, which is the thing
|
|
// every assertion below depends on. Found by this file passing one
|
|
// test at a time and failing in file order.
|
|
const wantRow = (page.viewportSize()?.height ?? 0) <= REFLOW_AT;
|
|
|
|
await page.waitForFunction(
|
|
(row: boolean) => {
|
|
const v = document.querySelector('now-playing-view');
|
|
const stack = v?.shadowRoot?.querySelector('.stack');
|
|
const el = v?.shadowRoot?.querySelector('.art img, .art .placeholder');
|
|
const t = v?.shadowRoot?.querySelector('.transport');
|
|
|
|
if (!stack || !el || !t) return false;
|
|
|
|
const dir = getComputedStyle(stack).flexDirection;
|
|
|
|
if (dir !== (row ? 'row' : 'column')) return false;
|
|
|
|
const r = el.getBoundingClientRect();
|
|
|
|
return r.width > 0 && r.height > 0 && t.getBoundingClientRect().height > 0;
|
|
},
|
|
wantRow,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The boxes this file reasons about, read in one evaluate.
|
|
*
|
|
* It reaches into the view's shadow root rather than using locators
|
|
* because the question is geometric — where these boxes are *relative
|
|
* to each other* — and a testid per edge would be four locators and
|
|
* four round trips to say one thing.
|
|
*/
|
|
async function boxes(page: Page) {
|
|
return page.evaluate(() => {
|
|
const v = document.querySelector('now-playing-view');
|
|
|
|
if (!v || !v.shadowRoot) return null;
|
|
|
|
const rect = (sel: string) => {
|
|
const el = v.shadowRoot!.querySelector(sel);
|
|
|
|
if (!el) return null;
|
|
|
|
const r = el.getBoundingClientRect();
|
|
|
|
return {
|
|
left: r.left, right: r.right, top: r.top, bottom: r.bottom,
|
|
width: r.width, height: r.height,
|
|
};
|
|
};
|
|
|
|
return {
|
|
// Whichever of the two the track has; both carry the sizing.
|
|
art: rect('.art img') ?? rect('.art .placeholder'),
|
|
artBox: rect('.art'),
|
|
meta: rect('.meta'),
|
|
transport: rect('.transport'),
|
|
scrollHeight: v.scrollHeight,
|
|
clientHeight: v.clientHeight,
|
|
};
|
|
});
|
|
}
|
|
|
|
test.describe('Now Playing survives a short screen', () => {
|
|
test.beforeEach(async ({ app }) => {
|
|
await stageATrack(app);
|
|
});
|
|
|
|
/**
|
|
* The crop, at four heights spanning the range it covered. This is
|
|
* the assertion that fails on the build before this change: at
|
|
* 424x439 the art measured 264x53.
|
|
*/
|
|
for (const vp of [DEVICE, SHORT_WINDOW, TALL_PHONE, { width: 900, height: 500 }]) {
|
|
test(`draws the art square at ${vp.width}x${vp.height}`, async ({ app }) => {
|
|
await app.setViewportSize(vp);
|
|
await openNowPlaying(app);
|
|
|
|
const b = await boxes(app);
|
|
|
|
expect(b, 'now-playing-view did not mount').not.toBeNull();
|
|
expect(b!.art, 'neither art nor placeholder rendered').not.toBeNull();
|
|
|
|
const { width, height } = b!.art!;
|
|
|
|
expect(width, 'the art has no width').toBeGreaterThan(0);
|
|
// One pixel of slack for sub-pixel layout, and no more: the
|
|
// defect this guards was a 5:1 band.
|
|
expect(
|
|
Math.abs(width - height),
|
|
`art is ${Math.round(width)}x${Math.round(height)}, not square`,
|
|
).toBeLessThanOrEqual(1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The promise #51 states and plan 018's matrix repeats. A floor on
|
|
* the art with the block scrolling was the other option on #172 and
|
|
* this is why it was not taken.
|
|
*/
|
|
test('never scrolls the transport off the bottom', async ({ app }) => {
|
|
await app.setViewportSize(DEVICE);
|
|
await openNowPlaying(app);
|
|
|
|
const b = await boxes(app);
|
|
|
|
expect(b!.transport!.bottom).toBeLessThanOrEqual(DEVICE.height);
|
|
expect(
|
|
b!.scrollHeight,
|
|
'the view scrolls, so the transport can be moved off screen',
|
|
).toBeLessThanOrEqual(b!.clientHeight + 1);
|
|
});
|
|
|
|
/**
|
|
* The reflow itself, as a relation rather than a measurement: below
|
|
* 500px the names are *beside* the art, above it they are below.
|
|
*/
|
|
test('puts the names beside the art below 500px', async ({ app }) => {
|
|
await app.setViewportSize(DEVICE);
|
|
await openNowPlaying(app);
|
|
|
|
const b = await boxes(app);
|
|
|
|
expect(
|
|
b!.meta!.left,
|
|
'the names are not to the right of the art',
|
|
).toBeGreaterThanOrEqual(b!.artBox!.right - 1);
|
|
});
|
|
|
|
test('keeps the names below the art on a tall phone', async ({ app }) => {
|
|
await app.setViewportSize(TALL_PHONE);
|
|
await openNowPlaying(app);
|
|
|
|
const b = await boxes(app);
|
|
|
|
expect(
|
|
b!.meta!.top,
|
|
'the names are not below the art',
|
|
).toBeGreaterThanOrEqual(b!.artBox!.bottom - 1);
|
|
});
|
|
|
|
/**
|
|
* The reflow is only worth having if it buys something, and the
|
|
* honest form of that is a floor on the device's own viewport
|
|
* rather than a comparison with a layout that is no longer there.
|
|
*
|
|
* The first draft compared the art against the column's leftover
|
|
* computed from the boxes on screen, and it passed on the build
|
|
* before this change as well -- the subtraction goes negative when
|
|
* the names are taller than the art, which is precisely the broken
|
|
* state. A test that cannot fail on the defect is not evidence.
|
|
*
|
|
* 100 is a floor, not the measurement: the device draws 143 and CI's
|
|
* chrome differs by whatever the volume control adds, so pinning the
|
|
* exact number would make this a test about the runner.
|
|
*/
|
|
test('gives the art a real size on the reference device', async ({ app }) => {
|
|
await app.setViewportSize(DEVICE);
|
|
await openNowPlaying(app);
|
|
|
|
const b = await boxes(app);
|
|
|
|
expect(
|
|
b!.art!.height,
|
|
'the art is still a sliver at the size #51 is about',
|
|
).toBeGreaterThan(100);
|
|
});
|
|
});
|