Compare commits

..
Author SHA1 Message Date
sjgandClaude Opus 5 b409c57296 [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
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>
2026-08-03 00:18:16 +02:00
@@ -9,6 +9,9 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
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 webDir = path.resolve(frontendDir, "../assets/web");
const generatedDir = path.join(webDir, "generated");
@@ -195,6 +198,55 @@ try {
await page.locator("#tab-main").waitFor({ state: "visible" });
assert.equal(new URL(page.url()).pathname, "/");
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 {
await browser.close();
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));