// Programmatic HOVER test for the win7 title-bar close button. // // bun scripts/hover-probe.ts // // Why this exists: hovering the X paints 7.css's red glow ::before // (box-shadow: 0 0 7px 3px #e68e75), and a stray red mark was reported on the // window border's far edge. Hover cannot be captured by any tool on this // machine — Firefox --screenshot has no pointer, `import -window` grabs // whatever the real pointer is (not) doing — so this script makes hover a // CLASS: it copies the built stylesheet with every `:hover` rewritten to // `.simhover`, renders the app's exact frame markup with the X force-hovered, // and then PIXEL-SCANS the regions where the artifact could live, printing a // verdict table. Re-run after any change to the title-bar/hover styling; the // artifact regions must stay at zero. // // Inherits shot.ts's constraints: dedicated Firefox profile (a running Firefox // holds the default profile's lock), and the page must be written INTO the // dist folder or strict_origin_policy silently unstyles it. Gecko vs WebKitGTK // is fine here — box-shadow/border-radius compositing is engine-agnostic. import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; import path from "node:path"; const ROOT = path.join(import.meta.dir, ".."); const DIST = path.join(ROOT, "ui", "dist", "ui", "browser"); const WORK = path.join(ROOT, ".shot"); function fail(message: string): never { console.error(`[hover-probe] ERROR: ${message}`); process.exit(1); } // ---- stylesheet with :hover rewritten to .simhover ---- if (!existsSync(DIST)) fail(`no build output at ${DIST} — run: bun run build:ui`); const styles = readdirSync(DIST).find((f) => f.startsWith("styles-") && f.endsWith(".css")); if (!styles) fail(`no styles-*.css in ${DIST} — run: bun run build:ui`); const css = await Bun.file(path.join(DIST, styles)).text(); // Global on purpose: EVERY hover rule becomes assertable markup. `:hover` has // no partial-token hazard (no `:hoverish` exists in CSS). await Bun.write(path.join(DIST, "_probe.css"), css.replaceAll(":hover", ".simhover")); // ---- the probe page: the app's frame markup, X force-hovered ---- // Mirrors ui/src/app/skins/7like/app.html + title-bar.ts EXACTLY where it // matters: .window.glass.w7-frame.active with .title-bar as a DIRECT child // (7.css couples the glass with `.window.glass > .title-bar`), controls keyed // by aria-label. Fixed-size windows instead of h-screen so two fit one shot. const win = (id: string, hover: boolean) => `
miniSCP
`; const page = `
${win("control", false)}
${win("hovered", true)}
`; const pagePath = path.join(DIST, "_hover-probe.html"); await Bun.write(pagePath, page); // ---- shoot ---- mkdirSync(WORK, { recursive: true }); const profile = path.join(WORK, "profile"); mkdirSync(profile, { recursive: true }); rmSync(path.join(profile, "parent.lock"), { force: true }); const out = path.join(WORK, "hover-probe.png"); const proc = Bun.spawnSync( ["firefox", "--headless", "--profile", profile, "--screenshot", out, "--window-size", "560,420", `file://${pagePath}`], { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, ); if (!existsSync(out)) { console.error(proc.stderr.toString().trim()); fail("firefox produced no image"); } // ---- pixel scan ---- // Page geometry (CSS px = image px; no DPR scaling in headless Firefox): // body padding 40, window at x:40..460 (width 420), first window y:40..160, // second y:200..320. The window's own 1px border is AT x=40 and x=459. const WIN_X = 40; const WIN_W = 420; const CONTROL_Y = 40; const HOVER_Y = 200; const WIN_H = 120; interface Region { name: string; x: number; y: number; w: number; h: number; artifact: boolean; // true = red here is a DEFECT; false = red here is the intended glow } // The close button: last control, min-width 48px (3rem), controls end 6px // (--w7-w-space padding) from the window's right edge — so the button spans // roughly x = right-6-48 .. right-6. The glow reaches up to blur+spread = 10px // beyond any edge of it. const regions = (top: number): Region[] => [ // the window's right border column (2px wide: border + inset ring), full title-bar height { name: "right border (A)", x: WIN_X + WIN_W - 2, y: top + 2, w: 2, h: 30, artifact: true }, // OUTSIDE the window entirely, right of the border, title-bar height { name: "outside right (B)", x: WIN_X + WIN_W, y: top, w: 12, h: 34, artifact: true }, // OUTSIDE above the top edge / rounded corner { name: "outside top (B)", x: WIN_X + WIN_W - 80, y: top - 12, w: 92, h: 12, artifact: true }, // the gutter between the button and the border — glow HERE is the intended look { name: "gutter (glow ok)", x: WIN_X + WIN_W - 7, y: top + 2, w: 5, h: 30, artifact: false }, ]; async function redCount(img: string, r: Region): Promise { // txt: gives one "x,y: (r,g,b[,a])" line per pixel const p = Bun.spawnSync( ["magick", img, "-crop", `${r.w}x${r.h}+${r.x}+${r.y}`, "+repage", "txt:"], { stdout: "pipe", stderr: "pipe" }, ); let count = 0; for (const line of p.stdout.toString().split("\n").slice(1)) { const m = /\((\d+),(\d+),(\d+)/.exec(line); if (!m) continue; const [red, green, blue] = [Number(m[1]), Number(m[2]), Number(m[3])]; // "red" = the red channel dominating both others decisively. The Aero // chrome is blue/white, so any genuine red here is the glow. if (red > 120 && red > green + 30 && red > blue + 30) count++; } return count; } console.log("[hover-probe] region scan (red-dominant pixels):\n"); console.log(" region control hovered verdict"); let defects = 0; for (const [i, region] of regions(CONTROL_Y).entries()) { const hoverRegion = regions(HOVER_Y)[i]; const inControl = await redCount(out, region); const inHover = await redCount(out, hoverRegion); const introduced = inHover - inControl; let verdict: string; if (!region.artifact) verdict = introduced > 0 ? "glow present (intended)" : "no glow"; else if (introduced > 0) { verdict = "DEFECT: red introduced by hover"; defects++; } else verdict = "clean"; console.log( ` ${region.name.padEnd(20)}${String(inControl).padStart(7)}${String(inHover).padStart(9)} ${verdict}`, ); } console.log(`\n[hover-probe] image: ${out}`); if (defects > 0) { console.log(`[hover-probe] ${defects} artifact region(s) show hover-introduced red`); process.exit(2); } console.log("[hover-probe] clean");