// Programmatic visual-QA: walks the rendered DOM and reports likely // layout defects (overflow, clipping, overlap, zero-size, low contrast). import { chromium } from "playwright"; const URL = process.env.URL || "http://127.0.0.1:5173"; const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); const findings = []; function add(kind, msg) { findings.push({ kind, msg }); } await page.goto(URL, { waitUntil: "networkidle" }); async function auditScene(label, prep) { await prep(); await page.waitForTimeout(500); // For each candidate selector, check for overflow / hidden / zero size const results = await page.evaluate(() => { const out = []; const sels = [ ".hero-title", ".hero-sub", ".sc-card-title", ".sc-card-sub", ".mc-hero-title", ".mc-hero-sub", ".mc-tab-label", ".node-name", ".panel-h", ".qcard-title", ".inspector-title h3", ".tour-title", ".tour-body", ".telemetry .t-block", ".rh-row-step", ".graph-overlay-name", ]; for (const sel of sels) { for (const el of Array.from(document.querySelectorAll(sel))) { const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); const clipped = el.scrollWidth > r.width + 1 && cs.overflow !== "visible" && cs.textOverflow === "ellipsis"; const overflowH = el.scrollHeight > r.height + 1 && cs.overflow !== "visible" && cs.overflowY !== "auto" && cs.overflowY !== "scroll"; const offscreen = r.right < 0 || r.left > window.innerWidth || r.bottom < 0 || r.top > window.innerHeight + 1; const zero = (r.width < 2 || r.height < 2) && el.textContent && el.textContent.trim().length > 0; if (clipped || overflowH || offscreen || zero) { out.push({ sel, text: (el.textContent || "").trim().slice(0, 80), w: r.width.toFixed(0), h: r.height.toFixed(0), sw: el.scrollWidth, sh: el.scrollHeight, issues: { clipped, overflowH, offscreen, zero }, }); } } } return out; }); for (const r of results) { add("layout", `[${label}] ${r.sel}: "${r.text}" (${r.w}x${r.h} content ${r.sw}x${r.sh}) ${JSON.stringify(r.issues)}`); } } await auditScene("landing", async () => {}); await auditScene("mission-1st-live", async () => { await page.locator(".sc-card").first().click(); await page.waitForSelector(".mc"); }); await auditScene("mission-ar", async () => { await page.locator(".mc-tab", { hasText: "Accounts Receivable" }).click(); }); await auditScene("inspector-raw", async () => { await page.locator(".itab", { hasText: "Raw" }).click(); }); await auditScene("tour", async () => { await page.locator(".link-btn", { hasText: "Tour" }).click(); await page.waitForSelector(".tour-card"); }); await auditScene("history", async () => { await page.keyboard.press("Escape"); await page.waitForTimeout(100); await page.locator(".tab", { hasText: "Runs" }).click(); await page.waitForSelector(".rh"); }); console.log(`findings: ${findings.length}`); for (const f of findings.slice(0, 50)) console.log(` ยท ${f.msg}`); // Also dump key layout dimensions for sanity const dims = await page.evaluate(() => { const get = (sel) => { const el = document.querySelector(sel); if (!el) return null; const r = el.getBoundingClientRect(); return { sel, w: Math.round(r.width), h: Math.round(r.height), top: Math.round(r.top), left: Math.round(r.left) }; }; return [ get(".rh-head"), get(".rh-list"), get(".rh-row"), ]; }); console.log("\nrh dimensions:", JSON.stringify(dims, null, 2)); await browser.close();