From b409c57296f5c74400f804a788f996ae155584da Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Mon, 3 Aug 2026 00:17:42 +0200 Subject: [PATCH] [test](trx-frontend-http): assert header geometry in the browser smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- .../frontend/tests/browser-smoke.mjs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs index 0a08d871..560f7465 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs @@ -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()));