Skip to content

Commit b8f032a

Browse files
Snapshotted the pages at phone and tablet sizes and fixed the header overflow they found (#51)
* Snapshotted the pages at phone and tablet sizes and fixed the header overflow they found The Chromatic run only ever captured the pages at the Desktop Chrome default. Each snapshot is now taken at desktop, tablet and phone sizes in both orientations, set per test because Chromatic keys an archive by the test's title, and each asserts nothing reaches past the right edge of the viewport, naming the offender. At mobile portrait that named the header: its three columns cannot hold the logo, the title and the two controls side by side, so the theme toggle hung off the right edge and the page scrolled sideways. The title now drops to a full-width second row on a phone, as clip-extractor's header does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kp4fg9Hm3W86zgumKHbP9s * Linked the changelog entries to their pull request Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kp4fg9Hm3W86zgumKHbP9s --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0615cbb commit b8f032a

6 files changed

Lines changed: 118 additions & 18 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 1.4.4
4+
5+
#### 🐛 Bug Fix
6+
7+
- On a phone the title now sits on its own row under the logo and the two header controls, instead of the theme toggle hanging off the right edge of the page ([#51](https://github.com/brain-bbqs/encoding-helper/pull/51))
8+
9+
#### 🏠 Internal
10+
11+
- The Chromatic snapshots now capture each page at phone and tablet sizes in both orientations as well as desktop, and fail by naming any element that runs off the side of the screen ([#51](https://github.com/brain-bbqs/encoding-helper/pull/51))
12+
313
## 1.4.1
414

515
#### 🏠 Internal

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "encoding-helper",
3-
"version": "1.4.3",
3+
"version": "1.4.4",
44
"description": "A didactic, in-browser video encoding lab: inspect MP4/H.264 structure, learn how encoding works, run empirical seeking tests, and re-encode video directly in the browser.",
55
"type": "module",
66
"license": "MIT",

‎src/style.css‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,26 @@ table.data.matrix-table th.matrix-row-head {
23312331
body {
23322332
padding: 12px 12px 90px;
23332333
}
2334+
/* The header's three columns cannot hold the logo, the title and the two controls side by side
2335+
on a phone: the side columns will not shrink below their controls, so the grid overflowed and
2336+
the theme toggle hung off the right edge of the page, giving it a sideways scroll. As in
2337+
clip-extractor's header, the title drops to a full-width second row under the other two. */
2338+
.site-header {
2339+
grid-template-columns: auto 1fr;
2340+
}
2341+
.brand-logo {
2342+
grid-column: 1;
2343+
grid-row: 1;
2344+
}
2345+
.header-actions {
2346+
grid-column: 2;
2347+
grid-row: 1;
2348+
}
2349+
.site-header h1 {
2350+
grid-column: 1 / -1;
2351+
grid-row: 2;
2352+
text-align: center;
2353+
}
23342354
.drop-zone {
23352355
padding: 26px 12px;
23362356
}
Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
import { test, expect } from "@chromatic-com/playwright";
22
import { gotoDemos, loadDemo } from "../integration/demoArchive";
3+
import { VIEWPORTS, expectNoHorizontalOverflow } from "../integration/layout";
34

4-
test("Main page - default", async ({ page }) => {
5-
await page.goto("/");
6-
await expect(page.locator("#dropZone")).toBeVisible();
7-
await expect(page.locator("#app")).toBeHidden();
8-
});
5+
// One test per viewport rather than one per Playwright project: see VIEWPORTS for why. The Chromatic
6+
// fixture snapshots the page after each test body, named by the test's title, so the viewport in
7+
// the title is what tells the captures apart.
8+
for (const viewport of VIEWPORTS) {
9+
const size = { width: viewport.width, height: viewport.height };
910

10-
test("Demos page", async ({ page }) => {
11-
await gotoDemos(page);
12-
await expect(page.locator("#dropZone")).toBeHidden();
13-
await expect(page.locator(".demos-group").first()).toBeVisible();
14-
});
11+
test(`Main page - default [${viewport.name}]`, async ({ page }) => {
12+
await page.setViewportSize(size);
13+
await page.goto("/");
14+
await expect(page.locator("#dropZone")).toBeVisible();
15+
await expect(page.locator("#app")).toBeHidden();
16+
await expectNoHorizontalOverflow(page);
17+
});
1518

16-
test("Main page - demo file loaded", async ({ page }) => {
17-
await loadDemo(page);
18-
await expect(page.locator("#app")).toBeVisible();
19-
await expect(page.locator(".tab.on")).toContainText("Inspect");
20-
});
19+
test(`Demos page [${viewport.name}]`, async ({ page }) => {
20+
await page.setViewportSize(size);
21+
await gotoDemos(page);
22+
await expect(page.locator("#dropZone")).toBeHidden();
23+
await expect(page.locator(".demos-group").first()).toBeVisible();
24+
await expectNoHorizontalOverflow(page);
25+
});
26+
27+
test(`Main page - demo file loaded [${viewport.name}]`, async ({ page }) => {
28+
await page.setViewportSize(size);
29+
await loadDemo(page);
30+
await expect(page.locator("#app")).toBeVisible();
31+
await expect(page.locator(".tab.on")).toContainText("Inspect");
32+
await expectNoHorizontalOverflow(page);
33+
});
34+
}

‎tests/integration/layout.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect, type Page } from "@playwright/test";
2+
3+
// Layout checks shared by the Chromatic snapshots and any spec that wants to look at the page at
4+
// something other than the Desktop Chrome default the suite runs under.
5+
6+
/**
7+
* The viewports a page is looked at across: the two device classes in both orientations, plus the
8+
* desktop width the suite started at (the Desktop Chrome default in playwright.shared.ts).
9+
*
10+
* The Chromatic run applies these per test rather than as Playwright projects, since Chromatic
11+
* keys an archive by the test's title alone: run as projects, each viewport would write over the
12+
* last one's manifest and only the project that ran last would reach Chromatic at all. Naming the
13+
* viewport in the test title keeps them apart.
14+
*/
15+
export const VIEWPORTS = [
16+
{ name: "desktop", width: 1280, height: 720 },
17+
{ name: "tablet portrait", width: 768, height: 1024 },
18+
{ name: "tablet landscape", width: 1024, height: 768 },
19+
{ name: "mobile portrait", width: 390, height: 844 },
20+
{ name: "mobile landscape", width: 844, height: 390 },
21+
] as const;
22+
23+
/**
24+
* Asserts that the page does not scroll sideways at the current viewport, and names the elements
25+
* that reach past its right edge when it does. This is the failure a narrow viewport hits first
26+
* (a row of controls that cannot wrap runs off the screen) and it is far easier to act on as a
27+
* named element than as a pixel diff in a snapshot.
28+
*
29+
* The right edge rather than the width: a control pushed out of a grid it no longer fits is not
30+
* itself wider than the screen, only hanging off the side of it, and it is the sideways scroll
31+
* that the visitor meets either way.
32+
*/
33+
export async function expectNoHorizontalOverflow(page: Page): Promise<void> {
34+
const { scrollWidth, clientWidth, offenders } = await page.evaluate(() => {
35+
const root = document.documentElement;
36+
const offenders: string[] = [];
37+
document.querySelectorAll("body *").forEach((el) => {
38+
// Shapes inside an inline <svg> are drawn in its own coordinate space and can report widths
39+
// in the thousands; the <svg> that holds them is what is checked. ownerSVGElement is null on
40+
// that outermost <svg> and set only on what is nested inside it.
41+
if ((el as SVGElement).ownerSVGElement) return;
42+
const { width, right } = el.getBoundingClientRect();
43+
if (width > 0 && right > root.clientWidth + 1) {
44+
const id = el.id ? `#${el.id}` : "";
45+
const names = typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean) : [];
46+
const classes = names.length ? `.${names.join(".")}` : "";
47+
offenders.push(
48+
`${el.tagName.toLowerCase()}${id}${classes} (${Math.round(width)}px wide, ends at ${Math.round(right)}px)`,
49+
);
50+
}
51+
});
52+
return { scrollWidth: root.scrollWidth, clientWidth: root.clientWidth, offenders };
53+
});
54+
expect(offenders, `Elements past the right edge of the ${clientWidth}px viewport`).toEqual([]);
55+
expect(scrollWidth, `Page scrolls sideways at ${clientWidth}px`).toBeLessThanOrEqual(clientWidth + 1);
56+
}

0 commit comments

Comments
 (0)