Organize/.cdp/final-check.cjs

174 lines
10 KiB
JavaScript

/* 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); });