/** * The panel's height ceiling, checked in a real browser layout. * * The unit tests prove the arithmetic; this proves the geometry. It writes a page * shaped like DSH's conversation panel — the composer inside an `overflow-y: auto` * scroll port, with a sticky seat at the bottom — and loads the **shipped** * stylesheet and the **shipped** ceiling functions into it (extracted from * lib/client.js, never retyped). The page then reports three geometries: * * natural — no ceiling at all: the shape of the bug, where the panel's own * header ends up above the edge the scroll port clips at; * stylesheet — the first-paint ceiling the CSS declares; * measured — the ceiling the readout measures off the DOM. * * Run: * node test/ceiling-check.mjs # writes the page, says how to open it * node test/ceiling-check.mjs # also runs it and checks the result * * The browser run is optional on purpose: no test in this package needs a browser, * and this file is a script, not a `.test.mjs`, so `npm test` never picks it up. */ import { spawnSync } from "node:child_process"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname, join } from "node:path"; const SOURCE = fileURLToPath(new URL("../lib/client.js", import.meta.url)); const OUT = fileURLToPath(new URL("./ceiling-check.html", import.meta.url)); /** The window sizes the check runs at: a maximized window, then a half-height one. */ const WINDOWS = ["2582,1485", "1600,780"]; const text = readFileSync(SOURCE, "utf8"); /** The stylesheet, exactly as the bundle ships it, with the class prefix resolved. */ function shippedCss() { const start = text.indexOf("const CSS = `"); if (start < 0) throw new Error("the bundle has no CSS literal"); const from = start + "const CSS = `".length; const end = text.indexOf("`;", from); if (end < 0) throw new Error("the CSS literal is unterminated"); return text.slice(from, end).replaceAll("${NS}", "dsh-cost"); } /** The brace-balanced source of one function declaration. */ function shippedFunction(name) { const start = text.indexOf(`function ${name}(`); if (start < 0) throw new Error(`the bundle has no ${name}`); // The body starts at the first `{` after the parameter list closes — not at the // first `{` overall, which a destructured parameter object would claim. let parens = 0; let body = -1; for (let index = text.indexOf("(", start); index < text.length; index += 1) { if (text[index] === "(") parens += 1; else if (text[index] === ")") { parens -= 1; if (parens === 0) { body = text.indexOf("{", index); break; } } } if (body < 0) throw new Error(`${name} has no body`); let depth = 0; for (let index = body; index < text.length; index += 1) { if (text[index] === "{") depth += 1; else if (text[index] === "}") { depth -= 1; if (depth === 0) return text.slice(start, index + 1); } } throw new Error(`${name} is not brace-balanced`); } /** One constant declaration, verbatim. */ function shippedConstant(name) { const match = new RegExp(`const ${name} = [^;]+;`).exec(text); if (match === null) throw new Error(`the bundle has no ${name}`); return match[0]; } const logic = [ shippedConstant("PANEL_GAP"), shippedConstant("PANEL_FLOOR"), shippedFunction("clipTopOf"), shippedFunction("panelCeiling"), ].join("\n\n"); const page = ` ceiling check
composer card
`; mkdirSync(dirname(OUT), { recursive: true }); writeFileSync(OUT, page, "utf8"); const browser = process.argv[2]; if (browser === undefined) { console.log(`wrote ${OUT}`); console.log("open it in any Chromium browser — append ?ceiling=none to see the bug — or"); console.log("pass a browser path to check it here: node test/ceiling-check.mjs "); process.exit(0); } /** Run the page in the browser and return its report. */ function run(size) { const result = spawnSync(browser, [ "--headless=new", "--disable-gpu", "--no-first-run", `--window-size=${size}`, "--dump-dom", "--virtual-time-budget=2000", pathToFileURL(OUT).href, ], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }); const found = /CEILING-REPORT (\{.*?\})<\/pre>/s.exec(result.stdout ?? ""); if (found === null) { console.error(`no report from ${size}:\n${(result.stderr ?? "").slice(0, 400)}`); process.exitCode = 1; return undefined; } return { size, report: JSON.parse(found[1]) }; } let failed = false; for (const size of WINDOWS) { const outcome = run(size); if (outcome === undefined) continue; const { natural, stylesheet, measured } = outcome.report; console.log(`\n== window ${size} (viewport ${outcome.report.viewport.join("x")}) ==`); console.log(` natural ${natural.height}px tall, top ${natural.top} vs port ${natural.portTop} → insidePort ${natural.insidePort}`); console.log(` stylesheet ${stylesheet.ceiling} tall, top ${stylesheet.top} → insidePort ${stylesheet.insidePort}, scrollable ${stylesheet.scrollable}`); console.log(` measured ${measured.ceiling}px tall, top ${measured.top} → insidePort ${measured.insidePort}, scrollable ${measured.scrollable}, reachedBottom ${measured.reachedBottom}`); const checks = [ ["an uncapped panel crosses the port's edge", natural.insidePort === false], ["the stylesheet alone keeps it inside", stylesheet.insidePort === true], ["the measurement keeps it inside", measured.insidePort === true], ["the capped panel scrolls instead", measured.scrollable === true], ["its last row is reachable", measured.reachedBottom === true], ]; for (const [what, ok] of checks) { console.log(` ${ok ? "ok " : "FAIL"} ${what}`); if (!ok) failed = true; } } process.exit(failed ? 1 : 0);