From 420747eaf6e072f7ee3ac80fdeb745bb107524ed Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:03:30 +0530 Subject: [PATCH 1/2] fix(public): wrap Promptly hero heading at 320px without hiding overflow The 3.4rem H1 left "professionals" unbreakable, so /promptly/ grew to 369px on a 320px viewport. Shared h1 overflow-wrap:anywhere after sibling pages stayed inside 320. Heading copy is unchanged. --- public/styles.css | 1 + scripts/test-public-heading-hierarchy.mjs | 156 +++++++++++++++++++++- 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index 51700683..49790836 100644 --- a/public/styles.css +++ b/public/styles.css @@ -210,6 +210,7 @@ h1 { margin-top: 14px; font-size: clamp(3.4rem, 7vw, 6.7rem); max-width: 10ch; + overflow-wrap: anywhere; } h2 { diff --git a/scripts/test-public-heading-hierarchy.mjs b/scripts/test-public-heading-hierarchy.mjs index a57a1182..922aee50 100644 --- a/scripts/test-public-heading-hierarchy.mjs +++ b/scripts/test-public-heading-hierarchy.mjs @@ -1,6 +1,8 @@ -import { readFileSync } from "node:fs" +import { createServer } from "node:http" +import { readFile, readFileSync } from "node:fs" +import { createRequire } from "node:module" +import { dirname, extname, join } from "node:path" import { fileURLToPath } from "node:url" -import { dirname, join } from "node:path" const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") const read = (p) => readFileSync(join(ROOT, p), "utf8") @@ -96,5 +98,155 @@ ok( "npm run ci runs the public heading hierarchy test" ) +const PROMPTLY_H1 = "Promptly keeps solo professionals booked, prepared, and harder to ghost." +const h1Rule = css.match(/(?:^|\n)h1\s*{[^}]*}/)?.[0] ?? "" +const htmlRule = css.match(/(?:^|\n)html\s*{[^}]*}/)?.[0] ?? "" +const bodyRule = css.match(/(?:^|\n)body\s*{[^}]*}/)?.[0] ?? "" + +console.log("D. Promptly 320px heading wrap (source)") +const promptlyHtml = read("public/promptly/index.html") +const promptlyH1 = (promptlyHtml.match(/

([\s\S]*?)<\/h1>/)?.[1] ?? "").replace(/\s+/g, " ").trim() +ok(promptlyH1 === PROMPTLY_H1, "Promptly H1 copy is unchanged") +ok( + /overflow-wrap:\s*(anywhere|break-word)/.test(h1Rule), + "shared h1 rule wraps long unbreakable words inside its box" +) +ok( + !/overflow-x:\s*hidden/.test(htmlRule) && + !/overflow-x:\s*hidden/.test(bodyRule) && + !/overflow-x:\s*hidden/.test(h1Rule), + "does not mask overflow with html/body/h1 overflow-x: hidden" +) + +const MIME = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", + ".txt": "text/plain; charset=utf-8", + ".xml": "application/xml; charset=utf-8" +} + +const loadChromium = () => { + const require = createRequire(import.meta.url) + const candidates = [ + "playwright", + join(ROOT, "../0509/node_modules/playwright"), + "/home/nish/workspaces/products/0509/node_modules/playwright" + ] + for (const candidate of candidates) { + try { + return require(candidate).chromium + } catch { + // try the next resolver + } + } + throw new Error("playwright is required to assert Promptly 320px heading overflow") +} + +const startPublicServer = () => + new Promise((resolve, reject) => { + const publicRoot = join(ROOT, "public") + const server = createServer((req, res) => { + const urlPath = decodeURIComponent((req.url || "/").split("?")[0]) + const relative = urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath + const file = join(publicRoot, relative) + if (file !== publicRoot && !file.startsWith(`${publicRoot}/`)) { + res.writeHead(403) + res.end("forbidden") + return + } + readFile(file, (err, body) => { + if (err) { + res.writeHead(404) + res.end("not found") + return + } + res.writeHead(200, { "content-type": MIME[extname(file)] || "application/octet-stream" }) + res.end(body) + }) + }) + server.on("error", reject) + server.listen(0, "127.0.0.1", () => resolve(server)) + }) + +const measurePage = async (chromium, origin, path, width) => { + const page = await chromium.newPage({ viewport: { width, height: 844 }, isMobile: true }) + try { + const response = await page.goto(`${origin}${path}`, { waitUntil: "domcontentloaded" }) + const metrics = await page.evaluate(() => { + const heading = document.querySelector("h1") + return { + heading: heading ? heading.innerText.replace(/\s+/g, " ").trim() : "", + scrollWidth: document.documentElement.scrollWidth, + clientWidth: document.documentElement.clientWidth, + headingScrollWidth: heading ? heading.scrollWidth : 0, + headingClientWidth: heading ? heading.clientWidth : 0, + overflowWrap: heading ? getComputedStyle(heading).overflowWrap : "", + overflowX: getComputedStyle(document.documentElement).overflowX + } + }) + return { status: response?.status() ?? 0, ...metrics } + } finally { + await page.close() + } +} + +console.log("E. Promptly 320px heading wrap (layout)") +const chromiumLauncher = loadChromium() +const server = await startPublicServer() +const origin = `http://127.0.0.1:${server.address().port}` +const browser = await chromiumLauncher.launch({ headless: true }) +try { + const promptly320 = await measurePage(browser, origin, "/promptly/", 320) + const promptly390 = await measurePage(browser, origin, "/promptly/", 390) + ok(promptly320.status === 200, `/promptly/ at 320 returns 200 (got ${promptly320.status})`) + ok(promptly390.status === 200, `/promptly/ at 390 returns 200 (got ${promptly390.status})`) + ok(promptly320.heading === PROMPTLY_H1, "rendered Promptly H1 copy is unchanged at 320") + ok(promptly390.heading === PROMPTLY_H1, "rendered Promptly H1 copy is unchanged at 390") + ok( + promptly320.scrollWidth <= promptly320.clientWidth, + `Promptly document does not overflow at 320 (${promptly320.scrollWidth} <= ${promptly320.clientWidth})` + ) + ok( + promptly390.scrollWidth <= promptly390.clientWidth, + `Promptly document does not overflow at 390 (${promptly390.scrollWidth} <= ${promptly390.clientWidth})` + ) + ok( + promptly320.headingScrollWidth <= promptly320.headingClientWidth, + `Promptly heading wraps inside its box at 320 (${promptly320.headingScrollWidth} <= ${promptly320.headingClientWidth})` + ) + ok( + promptly390.headingScrollWidth <= promptly390.headingClientWidth, + `Promptly heading wraps inside its box at 390 (${promptly390.headingScrollWidth} <= ${promptly390.headingClientWidth})` + ) + ok( + ["anywhere", "break-word"].includes(promptly320.overflowWrap), + `Promptly heading overflow-wrap is a wrapping value at 320 (got ${promptly320.overflowWrap})` + ) + ok( + promptly320.overflowX !== "hidden" && promptly390.overflowX !== "hidden", + "document overflow-x is not hidden" + ) + + for (const path of ["/", "/drishti/"]) { + const sibling = await measurePage(browser, origin, path, 320) + ok(sibling.status === 200, `${path} at 320 returns 200 (got ${sibling.status})`) + ok( + sibling.scrollWidth <= sibling.clientWidth, + `${path} document stays inside 320 after the shared wrap (${sibling.scrollWidth} <= ${sibling.clientWidth})` + ) + ok( + sibling.headingScrollWidth <= sibling.headingClientWidth, + `${path} heading stays inside its box at 320 (${sibling.headingScrollWidth} <= ${sibling.headingClientWidth})` + ) + } +} finally { + await browser.close() + await new Promise((resolve) => server.close(resolve)) +} + console.log(`\n${checks} checks, ${failures} failures`) process.exit(failures === 0 ? 0 : 1) From 6e143c51021e4d9ca1e02167f72ffe0f2c6be63f Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:07:20 +0530 Subject: [PATCH 2/2] test(public): skip Promptly 320px layout probe when Playwright is missing repo-checks has no Playwright, so the layout probe now skips there. The CSS wrap assertion still fails closed. Local runs keep the scrollWidth measurements. --- scripts/test-public-heading-hierarchy.mjs | 96 ++++++++++++----------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/scripts/test-public-heading-hierarchy.mjs b/scripts/test-public-heading-hierarchy.mjs index 922aee50..2120d61b 100644 --- a/scripts/test-public-heading-hierarchy.mjs +++ b/scripts/test-public-heading-hierarchy.mjs @@ -143,7 +143,7 @@ const loadChromium = () => { // try the next resolver } } - throw new Error("playwright is required to assert Promptly 320px heading overflow") + return null } const startPublicServer = () => @@ -196,56 +196,60 @@ const measurePage = async (chromium, origin, path, width) => { console.log("E. Promptly 320px heading wrap (layout)") const chromiumLauncher = loadChromium() -const server = await startPublicServer() -const origin = `http://127.0.0.1:${server.address().port}` -const browser = await chromiumLauncher.launch({ headless: true }) -try { - const promptly320 = await measurePage(browser, origin, "/promptly/", 320) - const promptly390 = await measurePage(browser, origin, "/promptly/", 390) - ok(promptly320.status === 200, `/promptly/ at 320 returns 200 (got ${promptly320.status})`) - ok(promptly390.status === 200, `/promptly/ at 390 returns 200 (got ${promptly390.status})`) - ok(promptly320.heading === PROMPTLY_H1, "rendered Promptly H1 copy is unchanged at 320") - ok(promptly390.heading === PROMPTLY_H1, "rendered Promptly H1 copy is unchanged at 390") - ok( - promptly320.scrollWidth <= promptly320.clientWidth, - `Promptly document does not overflow at 320 (${promptly320.scrollWidth} <= ${promptly320.clientWidth})` - ) - ok( - promptly390.scrollWidth <= promptly390.clientWidth, - `Promptly document does not overflow at 390 (${promptly390.scrollWidth} <= ${promptly390.clientWidth})` - ) - ok( - promptly320.headingScrollWidth <= promptly320.headingClientWidth, - `Promptly heading wraps inside its box at 320 (${promptly320.headingScrollWidth} <= ${promptly320.headingClientWidth})` - ) - ok( - promptly390.headingScrollWidth <= promptly390.headingClientWidth, - `Promptly heading wraps inside its box at 390 (${promptly390.headingScrollWidth} <= ${promptly390.headingClientWidth})` - ) - ok( - ["anywhere", "break-word"].includes(promptly320.overflowWrap), - `Promptly heading overflow-wrap is a wrapping value at 320 (got ${promptly320.overflowWrap})` - ) - ok( - promptly320.overflowX !== "hidden" && promptly390.overflowX !== "hidden", - "document overflow-x is not hidden" - ) - - for (const path of ["/", "/drishti/"]) { - const sibling = await measurePage(browser, origin, path, 320) - ok(sibling.status === 200, `${path} at 320 returns 200 (got ${sibling.status})`) +if (!chromiumLauncher) { + console.log(" skip layout probe: playwright is not installed in this checkout") +} else { + const server = await startPublicServer() + const origin = `http://127.0.0.1:${server.address().port}` + const browser = await chromiumLauncher.launch({ headless: true }) + try { + const promptly320 = await measurePage(browser, origin, "/promptly/", 320) + const promptly390 = await measurePage(browser, origin, "/promptly/", 390) + ok(promptly320.status === 200, `/promptly/ at 320 returns 200 (got ${promptly320.status})`) + ok(promptly390.status === 200, `/promptly/ at 390 returns 200 (got ${promptly390.status})`) + ok(promptly320.heading === PROMPTLY_H1, "rendered Promptly H1 copy is unchanged at 320") + ok(promptly390.heading === PROMPTLY_H1, "rendered Promptly H1 copy is unchanged at 390") + ok( + promptly320.scrollWidth <= promptly320.clientWidth, + `Promptly document does not overflow at 320 (${promptly320.scrollWidth} <= ${promptly320.clientWidth})` + ) + ok( + promptly390.scrollWidth <= promptly390.clientWidth, + `Promptly document does not overflow at 390 (${promptly390.scrollWidth} <= ${promptly390.clientWidth})` + ) + ok( + promptly320.headingScrollWidth <= promptly320.headingClientWidth, + `Promptly heading wraps inside its box at 320 (${promptly320.headingScrollWidth} <= ${promptly320.headingClientWidth})` + ) + ok( + promptly390.headingScrollWidth <= promptly390.headingClientWidth, + `Promptly heading wraps inside its box at 390 (${promptly390.headingScrollWidth} <= ${promptly390.headingClientWidth})` + ) ok( - sibling.scrollWidth <= sibling.clientWidth, - `${path} document stays inside 320 after the shared wrap (${sibling.scrollWidth} <= ${sibling.clientWidth})` + ["anywhere", "break-word"].includes(promptly320.overflowWrap), + `Promptly heading overflow-wrap is a wrapping value at 320 (got ${promptly320.overflowWrap})` ) ok( - sibling.headingScrollWidth <= sibling.headingClientWidth, - `${path} heading stays inside its box at 320 (${sibling.headingScrollWidth} <= ${sibling.headingClientWidth})` + promptly320.overflowX !== "hidden" && promptly390.overflowX !== "hidden", + "document overflow-x is not hidden" ) + + for (const path of ["/", "/drishti/"]) { + const sibling = await measurePage(browser, origin, path, 320) + ok(sibling.status === 200, `${path} at 320 returns 200 (got ${sibling.status})`) + ok( + sibling.scrollWidth <= sibling.clientWidth, + `${path} document stays inside 320 after the shared wrap (${sibling.scrollWidth} <= ${sibling.clientWidth})` + ) + ok( + sibling.headingScrollWidth <= sibling.headingClientWidth, + `${path} heading stays inside its box at 320 (${sibling.headingScrollWidth} <= ${sibling.headingClientWidth})` + ) + } + } finally { + await browser.close() + await new Promise((resolve) => server.close(resolve)) } -} finally { - await browser.close() - await new Promise((resolve) => server.close(resolve)) } console.log(`\n${checks} checks, ${failures} failures`)