/* FINAL CHECK: login via UI, verify Home renders, cycle all 4 themes live, * confirm card colors change instantly, zero console errors, screenshots. */ const { spawn } = require("node:child_process"); const fs = require("node:fs"); const http = require("node:http"); const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"; const PORT = 9380; function httpJson(p) { return new Promise((res, rej) => { const r = http.request({ host: "127.0.0.1", port: PORT, path: p }, (x) => { let d = ""; x.on("data", (c) => { d += c; }); x.on("end", () => { try { res(JSON.parse(d)); } catch (e) { res(d); } }); }); r.on("error", rej); r.end(); }); } (async () => { const prof = fs.mkdtempSync(require("node:os").tmpdir() + "/edge-prof-"); const proc = spawn(EDGE, ["--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${prof}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" }); let tg = null; for (let i = 0; i < 40; i++) { await new Promise(r => setTimeout(r, 500)); try { tg = await httpJson("/json/list"); break; } catch (e) {} } const page = tg.find(t => t.type === "page"); const ws = new WebSocket(page.webSocketDebuggerUrl); await new Promise(r => (ws.onopen = r)); let id = 0; const send = (m, p = {}) => new Promise(res => { const i2 = ++id; ws.send(JSON.stringify({ id: i2, method: m, params: p })); const t = setInterval(() => { const f = ws._buf.find(b => b.id === i2); if (f) { clearInterval(t); res(f.result ?? f.error); } }, 10); }); ws._buf = []; const errors = []; ws.onmessage = (m) => { const j = JSON.parse(m.data); if (j.id) { ws._buf.push(j); return; } if (j.method === "Runtime.consoleAPICalled" && j.params.type === "error") { errors.push(j.params.args.map(a => (a.value ?? a.description ?? "")).join(" ").slice(0, 140)); } if (j.method === "Runtime.exceptionThrown") { errors.push("EXC: " + (j.params.exceptionDetails?.exception?.description || j.params.exceptionDetails?.text || "").slice(0, 140)); } }; await send("Runtime.enable"); await send("Page.enable"); await send("Network.enable"); await send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1 }); const ev = (e) => send("Runtime.evaluate", { expression: e, returnByValue: true }).then(r => (r.result ? r.result.value : "ERR")); const click = async (x, y) => { await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 }); await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 }); }; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const shot = async (name) => { const s = await send("Page.captureScreenshot", { format: "png" }); fs.writeFileSync("shots/" + name, Buffer.from(s.data, "base64")); }; // --- 1. login ----------------------------------------------------------- await send("Page.navigate", { url: "http://localhost:3210/" }); await sleep(4000); const fill = (sel, v) => ev(`(() => { const el = document.querySelector(${JSON.stringify(sel)}); if (!el) return false; const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; set.call(el, ${JSON.stringify(v)}); el.dispatchEvent(new Event("input", { bubbles: true })); return true; })()`); console.log("1. login form found:", await ev("!!document.querySelector('input[type=email]') && !!document.querySelector('input[type=password]')")); await fill("input[type=email]", "dev@example.com"); await fill("input[type=password]", "password123"); const sub = JSON.parse(await ev(`(() => { const b = document.querySelector("form button[type=submit]") || document.querySelector("form button"); const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); })()`)); await click(sub.x, sub.y); let home = false; for (let i = 0; i < 20; i++) { await sleep(1000); const url = await ev("location.href"); const btns = await ev("document.querySelectorAll('button').length"); if (url.endsWith("3210/") && btns > 8) { home = true; break; } } console.log("2. logged in & on Home:", home, "| URL:", await ev("location.href"), "| buttons:", await ev("document.querySelectorAll('button').length")); console.log("3. board rendered:", await ev(`(() => { const lanes = document.querySelectorAll('[data-slot="dropdown-menu-trigger"], [class*="lane"]').length; const groups = Array.from(document.querySelectorAll("h3")).length; return "headings=" + groups; })()`)); // --- 2. helpers --------------------------------------------------------- const cardState = () => ev(`(() => { const cs = Array.from(document.querySelectorAll('[class*=rounded-2xl]')).filter(c => /background-color/.test(c.getAttribute('style') || '')); if (!cs.length) return { cards: 0 }; const s = cs[0].getAttribute('style'); return { cards: cs.length, fill: (s.match(/background-color:([^;]+)/) || [])[1], stroke: (s.match(/border-color:([^;]+)/) || [])[1], html: document.documentElement.className.split(' ').filter(Boolean).slice(-1).join(','), bodyBg: getComputedStyle(document.body).backgroundColor, }; })()`); const selectTheme = async (name) => { for (let attempt = 1; attempt <= 3; attempt++) { const v = await ev(`(() => { const b = Array.from(document.querySelectorAll('button')).find(x => (x.textContent || '').trim() === 'Theme'); if (!b) return null; const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); })()`); if (!v) { await sleep(400); continue; } const tp = JSON.parse(v); await click(tp.x, tp.y); let labels = null; for (let i = 0; i < 20; i++) { await sleep(150); labels = await ev(`(() => { const m = document.querySelector('[data-slot="dropdown-menu-content"]'); if (!m || m.querySelectorAll('button').length < 4) return null; return JSON.stringify(Array.from(m.querySelectorAll('button')).map(b => (b.textContent || '').trim())); })()`); if (labels) break; } if (!labels) { await sleep(500); continue; } const idx = JSON.parse(labels).indexOf(name); if (idx === -1) return "option missing"; const p = JSON.parse(await ev(`(() => { const m = document.querySelector('[data-slot="dropdown-menu-content"]'); const b = m.querySelectorAll('button')[${idx}]; const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + 12), y: Math.round(r.y + r.height / 2) }); })()`)); await click(p.x, p.y); await sleep(900); return "ok"; } return "failed after retries"; }; // --- 3. cycle themes: card fill must match light/dark variant live --- const seq = ["Sunset", "Default", "Dark", "Ocean", "Sunset"]; const expectedFill = (theme) => theme === "Dark" || theme === "Ocean" ? "#3a2320" /* dark tint (softDark) */ : "#fae4e1"; /* light tint (soft) */; let prev = await cardState(); let allOk = true; for (const name of seq) { const r = await selectTheme(name); await sleep(300); const now = await cardState(); const correct = r === "ok" && now.fill && now.fill.includes(expectedFill(name)) && prev.fill !== undefined; // a live switch = the html class changed and the fill is the right variant for the new theme if (!correct) allOk = false; console.log(`4. ${name.padEnd(7)}:`, r, "| fill correct for theme:", now.fill.includes(expectedFill(name)), "| fill:", now.fill && now.fill.trim().slice(0, 40), "| html:", now.html); prev = now; } console.log("5. ALL theme switches correct & live:", allOk); // --- 4. other pages smoke test ------------------------------------------ const pages = ["/projects", "/chat", "/admin"]; let adminBadgeOk = null; for (const p of pages) { const before = errors.length; await ev(`location.href = ${JSON.stringify("http://localhost:3210" + p)}; true`); await sleep(3500); const ok = await ev(`document.body.innerText.length > 50 && document.querySelectorAll('button').length > 3`); const newErr = errors.length - before; console.log(`7. page ${p}: renders=${!!ok}, new console errors=${newErr}`); if (p === "/admin") { // role badge must be visible & its menu must open on a trusted click await ev(`(() => { const b = Array.from(document.querySelectorAll('button')).find(x => /Change role/.test(x.getAttribute('aria-label') || '')); if (b) b.scrollIntoView({ block: 'center' }); return true; })()`); await sleep(400); const badgePos = await ev(`(() => { const b = Array.from(document.querySelectorAll('button')).find(x => /Change role/.test(x.getAttribute('aria-label') || '')); if (!b) return null; const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); })()`); if (badgePos) { const bp = JSON.parse(badgePos); await click(bp.x, bp.y); let opened = false; for (let i = 0; i < 10; i++) { await sleep(150); opened = !!(await ev(`document.querySelector('[data-slot="dropdown-menu-content"]')`)); if (opened) break; } adminBadgeOk = opened; console.log("7b. admin role-badge menu opens:", opened); await ev(`document.body.click(); true`); await sleep(300); } else { adminBadgeOk = false; console.log("7b. admin role badge not found"); } } } // --- 5. final state + errors -------------------------------------------- await ev(`location.href = "http://localhost:3210/"; true`); await sleep(4000); await shot("final-home.png"); const realErrors = errors.filter(e => !/favicon|manifest|icon|404|DevTools|HMR/i.test(e)); console.log("8. CONSOLE ERRORS (filtered):", realErrors.length); realErrors.slice(0, 8).forEach(e => console.log(" -", e)); console.log("9. hydration warnings:", errors.filter(e => /hydrat/i.test(e)).length); console.log("10. Base UI crashes:", errors.filter(e => /Base UI/i.test(e)).length); proc.kill(); const pass = allOk && realErrors.length === 0 && home && adminBadgeOk === true; console.log(pass ? "\nFINAL RESULT: ALL PASS ✔" : "\nFINAL RESULT: FAIL"); process.exit(pass ? 0 : 2); })().catch(e => { console.error("FAIL", e.message); process.exit(1); });