// Programmatic POSITIONING test for the win7 context menu. // // bun scripts/menu-probe.ts // // Why this exists: 7.css declares `position: relative` on ul[role=menu] (it // needs it for the gutter stripe and submenus), and the `skin` layer outranks // `utilities` — so Tailwind's `.fixed` on the app's own menu lost, its // [style.left/top.px] became FLOW offsets, and every right-click menu rendered // below the viewport of the h-screen frame. Nothing about the idle window // changes when that breaks, so the screenshot harness (scripts/shot.ts) cannot // see it, and hover-probe.ts tests a different axis. This script renders the // menu markup at declared coordinates and pixel-scans whether it actually // landed there. // // It carries its own control: a second menu WITHOUT the `w7-ctx` marker class, // i.e. the bug verbatim. That one must stay invisible — if it ever shows up, // 7.css stopped forcing `relative` and this probe's premise is stale. // // 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. 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(`[menu-probe] ERROR: ${message}`); process.exit(1); } 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`); // ---- the probe page ---- // Geometry (CSS px = image px; headless Firefox applies no DPR scaling). const MENU_X = 200; const MENU_Y = 100; const CONTROL_X = 560; const MENU_W = 200; // Mirrors ui/src/app/skins/7like/context-menu.ts EXACTLY where it matters: // ul[role=menu] > li[role=menuitem] > button, the `w7-ctx fixed` class pair, // and left/top as inline pixel styles. `w7-ctx` is what the skinfix rule in // 7like.css keys on; the control deliberately omits it. const menu = (x: number, marker: boolean) => ` `; // The frame reproduces the app's: .window.w7-frame is a 100vh flex column and // is one of its LAST children, so a menu that falls back to // static flow lands past the bottom edge — which is the whole bug. // The body gets an explicit white background (inline, so no layer can touch // it) purely so "is anything drawn here?" is an unambiguous question. const page = `
miniSCP
${menu(MENU_X, true)} ${menu(CONTROL_X, false)}
`; const pagePath = path.join(DIST, "_menu-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, "menu-probe.png"); const proc = Bun.spawnSync( ["firefox", "--headless", "--profile", profile, "--screenshot", out, "--window-size", "800,400", `file://${pagePath}`], { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, ); if (!existsSync(out)) { console.error(proc.stderr.toString().trim()); fail("firefox produced no image"); } // ---- pixel scan ---- /** * Count pixels that are visibly NOT the white window body: the menu's border * (#0006 over white ≈ #999), its shadow and its black item text all qualify. * A menu that rendered where it was told fills this with hundreds of them; an * empty white region scores 0. */ async function inkCount(img: string, x: number, y: number, w: number, h: number): Promise { const p = Bun.spawnSync(["magick", img, "-crop", `${w}x${h}+${x}+${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 [r, g, b] = [Number(m[1]), Number(m[2]), Number(m[3])]; if (Math.max(r, g, b) < 200) count++; } return count; } // Inset by 2px so the scan cannot straddle the region's own edge. const BOX = { w: MENU_W - 4, h: 60 }; const placed = await inkCount(out, MENU_X + 2, MENU_Y + 2, BOX.w, BOX.h); const control = await inkCount(out, CONTROL_X + 2, MENU_Y + 2, BOX.w, BOX.h); console.log("[menu-probe] ink at the declared coordinates:\n"); console.log(" menu px verdict"); console.log( ` w7-ctx (position: fixed)${String(placed).padStart(6)} ${placed > 0 ? "placed" : "MISSING: menu did not render where it was told"}`, ); console.log( ` control (no marker) ${String(control).padStart(6)} ${control > 0 ? "NOTE: 7.css no longer forces relative — premise stale" : "off-viewport, as expected"}`, ); console.log(`\n[menu-probe] image: ${out}`); if (placed === 0) { console.log("[menu-probe] the context menu is positioned in FLOW again — see 7like.css .w7-ctx"); process.exit(2); } console.log("[menu-probe] clean");