[test](trx-frontend-http): assert header geometry in the browser smoke test
CI / test (push) Successful in 7m27s
CI / frontend (push) Successful in 3m1s
CI / reuse (push) Successful in 3s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 7m24s
CI / frontend (pull_request) Successful in 2m11s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m15s
CI / test (push) Successful in 7m27s
CI / frontend (push) Successful in 3m1s
CI / reuse (push) Successful in 3s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 7m24s
CI / frontend (pull_request) Successful in 2m11s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m15s
Several layout faults shipped while every gate passed, because nothing looked at geometry: a header whose height tracked the viewport, controls at four different heights, a tab strip that ran under the controls, and a dropdown that opened underneath the page. Assert the invariants behind those at four widths — the header stays one row, the tabs do not reach the controls, the controls share a height, the page does not scroll sideways — and that the menu renders with real dimensions and wins a hit test at its own centre. The overlap check measures the tabs rather than the strip: with the strip allowed to overflow, its box shrinks while its content paints across the controls, so the container's own rect never registers the collision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space>
This commit was merged in pull request #33.
This commit is contained in:
@@ -9,6 +9,9 @@ import path from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { chromium } from "playwright-core";
|
import { chromium } from "playwright-core";
|
||||||
|
|
||||||
|
// page.evaluate callbacks run in the browser, not in this Node process.
|
||||||
|
/* global document */
|
||||||
|
|
||||||
const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||||
const webDir = path.resolve(frontendDir, "../assets/web");
|
const webDir = path.resolve(frontendDir, "../assets/web");
|
||||||
const generatedDir = path.join(webDir, "generated");
|
const generatedDir = path.join(webDir, "generated");
|
||||||
@@ -195,6 +198,55 @@ try {
|
|||||||
await page.locator("#tab-main").waitFor({ state: "visible" });
|
await page.locator("#tab-main").waitFor({ state: "visible" });
|
||||||
assert.equal(new URL(page.url()).pathname, "/");
|
assert.equal(new URL(page.url()).pathname, "/");
|
||||||
assert.deepEqual(runtimeErrors, []);
|
assert.deepEqual(runtimeErrors, []);
|
||||||
|
|
||||||
|
// --- Layout regressions -------------------------------------------------
|
||||||
|
// Every fault below shipped at some point while the rest of this file
|
||||||
|
// passed, because nothing here looked at geometry: a header whose height
|
||||||
|
// tracked the viewport, controls that stretched, a tab strip that ran under
|
||||||
|
// the controls, and a dropdown that opened underneath the spectrum.
|
||||||
|
for (const width of [1440, 1280, 1100, 900]) {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const header = await page.evaluate(() => {
|
||||||
|
const bar = document.querySelector(".tab-bar");
|
||||||
|
const nav = document.querySelector(".tab-bar-nav");
|
||||||
|
const actions = document.querySelector(".top-bar-actions");
|
||||||
|
const controls = [...actions.children]
|
||||||
|
.filter((el) => !el.hidden && el.getBoundingClientRect().height > 0)
|
||||||
|
.map((el) => Math.round(el.getBoundingClientRect().height));
|
||||||
|
return {
|
||||||
|
barHeight: Math.round(bar.getBoundingClientRect().height),
|
||||||
|
// The tabs, not the strip: with the strip allowed to overflow its box
|
||||||
|
// shrinks while its content paints across the controls, so the
|
||||||
|
// container's own rect never registers the collision.
|
||||||
|
overlap: Math.round(Math.max(...[...nav.querySelectorAll(".tab")]
|
||||||
|
.filter((tab) => tab.offsetParent !== null)
|
||||||
|
.map((tab) => tab.getBoundingClientRect().right))
|
||||||
|
- actions.getBoundingClientRect().left),
|
||||||
|
heights: [...new Set(controls)],
|
||||||
|
pageScrollsSideways: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.ok(header.barHeight <= 96, `header is ${header.barHeight}px at ${width}px; it should stay one row`);
|
||||||
|
assert.ok(header.overlap <= 0, `tab strip overlaps the controls by ${header.overlap}px at ${width}px`);
|
||||||
|
assert.ok(header.heights.length <= 2, `controls have heights ${header.heights.join(", ")} at ${width}px`);
|
||||||
|
assert.equal(header.pageScrollsSideways, false, `page scrolls sideways at ${width}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dropdown must paint over the page, not inside the header: fixed
|
||||||
|
// positioning escapes clipping but not the header's stacking context.
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const menu = await page.evaluate(() => {
|
||||||
|
document.getElementById("mobile-more-btn").click();
|
||||||
|
const element = document.getElementById("mobile-more-menu");
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||||
|
return { width: Math.round(rect.width), height: Math.round(rect.height), onTop: element.contains(hit) };
|
||||||
|
});
|
||||||
|
assert.ok(menu.height > 40 && menu.width > 80, `menu rendered ${menu.width}x${menu.height}`);
|
||||||
|
assert.ok(menu.onTop, "menu is painted underneath the page");
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||||
|
|||||||
Reference in New Issue
Block a user