Ten tests, five of which fail on the build before this. The desktop guard is meant to pass there -- that is its job, and it is the one that caught a three-pixel regression nothing else could see. `openTheQueue` moves to the fixtures, because hiding one button failed ten tests in four files about the back stack and about layout: every one of them opened the queue by clicking `#queue-button`, and so was quietly asserting *which* route exists as well as what the queue does. The route differs by width now and that is the feature. Two smaller things. The play button is named for its action, so an exact 'Play' waits out a fixture track -- 11.1s per test, passing by luck, and it would have failed outright against LONG_TRACK. And the "nothing playing" case clears the queue itself rather than trusting the app not to have played anything: `make e2e` runs one long-lived app across every spec (#168), which is how a deterministic bug first showed up as a flake.
188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
import { test, expect, openTheQueue } from '../support/fixtures.js';
|
||
|
||
/**
|
||
* #24 — the queue panel does not take the page's width away from it.
|
||
*
|
||
* The panel is `flex-shrink: 0` in the flow of `.content-area`, so an
|
||
* open queue used to be paid for by the main panel. Measured on
|
||
* Playlists before the fix:
|
||
*
|
||
* | viewport | main panel |
|
||
* |---|---|
|
||
* | 900×600 | 379px — all three header actions clipped |
|
||
* | 390×780 | 69px |
|
||
* | 320×600 | **0px** |
|
||
*
|
||
* **900×600 is the worst desktop case, not the 800×600 minimum**, and
|
||
* that is the trap this file exists to keep closed: the sidebar
|
||
* collapses to icons *below* 900, so the main panel is 843px at 899 and
|
||
* 700px at 900. A spec that checks "the minimum" and stops has not
|
||
* checked the worst case — which is what every viewport list in this
|
||
* suite did before this.
|
||
*
|
||
* These assert the *content's* width rather than the panel's mode
|
||
* wherever they can, because the mode is the mechanism and the width is
|
||
* the complaint.
|
||
*/
|
||
|
||
/** The bands from plan 018's size matrix, plus the pixel above the collapse. */
|
||
const BANDS = [
|
||
{ name: 'a wide desktop (1280×800)', width: 1280, height: 800, inline: true },
|
||
{ name: 'the default window (1100×720)', width: 1100, height: 720, inline: true },
|
||
{ name: 'a laptop (1024×768)', width: 1024, height: 768, inline: true },
|
||
{ name: 'the worst desktop width (900×600)', width: 900, height: 600, inline: false },
|
||
{ name: 'the enforced minimum (800×600)', width: 800, height: 600, inline: false },
|
||
{ name: 'a phone (390×780)', width: 390, height: 780, inline: false },
|
||
{ name: '400% zoom (320×600)', width: 320, height: 600, inline: false },
|
||
];
|
||
|
||
/**
|
||
* How much room the content has, and whether the shell needs scrolling
|
||
* to reach any of itself.
|
||
*/
|
||
const shellGeometry = (page: import('@playwright/test').Page) =>
|
||
page.evaluate(() => {
|
||
const main = document.querySelector('#main-content')!.getBoundingClientRect();
|
||
const panel = document.querySelector('#queue-panel')!;
|
||
|
||
return {
|
||
mainWidth: Math.round(main.width),
|
||
overlay: panel.hasAttribute('overlay'),
|
||
open: panel.hasAttribute('open'),
|
||
bodyScrollWidth: document.body.scrollWidth,
|
||
bodyClientWidth: document.body.clientWidth,
|
||
};
|
||
});
|
||
|
||
/**
|
||
* Opening the queue is `openTheQueue`, which takes the route this
|
||
* viewport offers. It used to be a local helper that clicked
|
||
* `#queue-button` unconditionally, and #59 hid that button below
|
||
* 600px -- so the two phone bands here failed on a build where the
|
||
* queue was working perfectly, having been asserting *how* it opens as
|
||
* much as what it does.
|
||
*/
|
||
const openQueue = openTheQueue;
|
||
|
||
test.describe('an open queue leaves the content its width', () => {
|
||
for (const band of BANDS) {
|
||
test(`at ${band.name}`, async ({ app }) => {
|
||
await app.setViewportSize({ width: band.width, height: band.height });
|
||
await openQueue(app);
|
||
|
||
// The mode is settled by a ResizeObserver, so poll rather than
|
||
// read once: a single read races the resize and reports the
|
||
// previous viewport's answer.
|
||
await expect
|
||
.poll(async () => (await shellGeometry(app)).overlay)
|
||
.toBe(!band.inline);
|
||
|
||
const geo = await shellGeometry(app);
|
||
|
||
// The floor is the point of the whole issue. Inline, the queue is
|
||
// affordable and the content keeps the rest; as an overlay the
|
||
// content keeps *everything*, which is what makes 0px at 320
|
||
// impossible rather than merely unlikely.
|
||
expect(geo.mainWidth).toBeGreaterThanOrEqual(320);
|
||
|
||
if (!band.inline) {
|
||
expect(geo.mainWidth).toBeGreaterThanOrEqual(
|
||
Math.min(band.width, 320),
|
||
);
|
||
}
|
||
|
||
// And opening the queue must not make the shell overflow.
|
||
expect(geo.bodyScrollWidth).toBeLessThanOrEqual(geo.bodyClientWidth);
|
||
});
|
||
}
|
||
});
|
||
|
||
test.describe('an overlaid queue says it is over the content', () => {
|
||
test.beforeEach(async ({ app }) => {
|
||
await app.setViewportSize({ width: 900, height: 600 });
|
||
});
|
||
|
||
test('draws a scrim and closes when it is clicked', async ({ app }) => {
|
||
await openQueue(app);
|
||
|
||
const panel = app.locator('#queue-panel');
|
||
|
||
await expect(panel).toHaveAttribute('overlay', '');
|
||
|
||
// The scrim is `aria-hidden` on purpose — it is a dismissal target,
|
||
// and the named routes out are the close button and Escape — so it
|
||
// is located structurally rather than by role.
|
||
await panel.evaluate((el) =>
|
||
el.shadowRoot!.querySelector<HTMLElement>('.scrim')!.click(),
|
||
);
|
||
|
||
await expect(app.locator('#queue-button')).toHaveAttribute(
|
||
'aria-expanded',
|
||
'false',
|
||
);
|
||
});
|
||
|
||
/**
|
||
* `getByRole`, not a shadow-root query: this repo has shipped a
|
||
* nameless control three times, and a drawer with a scrim is exactly
|
||
* the shape that grows a fourth.
|
||
*/
|
||
test('offers a named close button', async ({ app }) => {
|
||
await openQueue(app);
|
||
|
||
const close = app.getByRole('button', { name: 'Close queue' });
|
||
|
||
await expect(close).toBeVisible();
|
||
await close.click();
|
||
|
||
await expect(app.locator('#queue-button')).toHaveAttribute(
|
||
'aria-expanded',
|
||
'false',
|
||
);
|
||
});
|
||
|
||
test('closes on Escape and gives focus back to the toggle', async ({
|
||
app,
|
||
}) => {
|
||
const toggle = app.locator('#queue-button');
|
||
|
||
await toggle.focus();
|
||
await toggle.click();
|
||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||
|
||
await app.keyboard.press('Escape');
|
||
|
||
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||
await expect(toggle).toBeFocused();
|
||
});
|
||
});
|
||
|
||
/**
|
||
* The inline panel is the mode that already worked, and the one every
|
||
* other queue spec is written against. It keeps its resize handle and
|
||
* gains none of the overlay's chrome.
|
||
*/
|
||
test.describe('a wide window keeps the queue beside the content', () => {
|
||
test('no scrim, no close button, and the content is narrower', async ({
|
||
app,
|
||
}) => {
|
||
await app.setViewportSize({ width: 1280, height: 800 });
|
||
|
||
const widthWithoutQueue = (await shellGeometry(app)).mainWidth;
|
||
|
||
await openQueue(app);
|
||
|
||
await expect(app.locator('#queue-panel')).not.toHaveAttribute(
|
||
'overlay',
|
||
'',
|
||
);
|
||
|
||
const geo = await shellGeometry(app);
|
||
|
||
expect(geo.mainWidth).toBeLessThan(widthWithoutQueue);
|
||
await expect(
|
||
app.getByRole('button', { name: 'Close queue' }),
|
||
).toHaveCount(0);
|
||
});
|
||
});
|