174 lines
10 KiB
JavaScript
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); });
|