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`)