Organize/.cdp/mobile-shots.cjs

137 lines
6.4 KiB
JavaScript

/* Mobile responsive verification: login, then screenshot the app at a
phone viewport (390x844) in the key mobile states, plus a desktop
sanity shot. Uses the Playwright-cached chromium headless shell. */
const { spawn } = require("node:child_process");
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const CHROME = "/home/brianfertig/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome";
const PORT = 9351;
const BASE = "http://localhost:3000";
const OUT = path.join(__dirname, "..", "shots");
function httpJson(p) {
return new Promise((resolve, reject) => {
const req = http.request({ host: "127.0.0.1", port: PORT, path: p }, (res) => {
let d = ""; res.on("data", (c) => (d += c));
res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(d); } });
});
req.on("error", reject); req.end();
});
}
(async () => {
const profile = fs.mkdtempSync("/tmp/chrome-prof-");
const proc = spawn(CHROME, ["--headless=new", "--no-sandbox", `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" });
let targets = null;
for (let i = 0; i < 40; i++) { await new Promise((r) => setTimeout(r, 500)); try { targets = await httpJson("/json/list"); break; } catch {} }
if (!targets) { console.error("no targets"); proc.kill(); process.exit(1); }
const page = targets.find((t) => t.type === "page");
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((r) => (ws.onopen = r));
let id = 0;
const send = (method, params = {}) => new Promise((resolve) => {
const myId = ++id; ws.send(JSON.stringify({ id: myId, method, params }));
const t = setInterval(() => { const m = ws._buf.find((b) => b.id === myId); if (m) { clearInterval(t); resolve(m.result ?? m.error); } }, 10);
});
ws._buf = [];
ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id) { ws._buf.push(msg); return; }
if (msg.method === "Runtime.consoleAPICalled" && msg.params.type === "error") {
const txt = msg.params.args.map((a) => a.value ?? a.description ?? a.type).join(" ");
if (!/hydrated|React DevTools|HMR|404/.test(txt)) console.log("[err]", txt.slice(0, 160));
} };
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function shot(name, w, h, mobile) {
await send("Emulation.setDeviceMetricsOverride", { width: w, height: h, deviceScaleFactor: 1, mobile, hasTouch: mobile });
const s = await send("Page.captureScreenshot", { format: "png" });
fs.writeFileSync(path.join(OUT, `${name}.png`), Buffer.from(s.data, "base64"));
console.log(`saved ${name} (${w}x${h})`);
}
async function click(css) {
const res = await send("Runtime.evaluate", { expression: `(() => { const el = document.querySelector(${JSON.stringify(css)}); if (!el) return "missing"; el.click(); return "clicked"; })()`, returnByValue: true });
console.log(`click(${css}) ->`, res?.result?.value ?? res);
}
async function evalJs(expr) {
return (await send("Runtime.evaluate", { expression: expr, returnByValue: true }))?.result?.value;
}
await send("Runtime.enable"); await send("Page.enable");
await send("Emulation.setDeviceMetricsOverride", { width: 390, height: 844, deviceScaleFactor: 1, mobile: true, hasTouch: true });
// ---- login (phone viewport) ----
await send("Page.navigate", { url: `${BASE}/login` });
await wait(3000);
const loginRes = await evalJs(`(() => {
const setVal = (el, v) => {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
setter.call(el, v);
el.dispatchEvent(new Event("input", { bubbles: true }));
};
const email = document.querySelector("#email");
const password = document.querySelector("#password");
if (!email || !password) return "inputs missing";
setVal(email, "dev@example.com");
setVal(password, "password123");
email.form.requestSubmit();
return "submitted";
})()`);
console.log("login ->", loginRes);
await wait(4000);
console.log("now at", await evalJs("location.href"));
// ---- 1. home board, mobile ----
await send("Page.navigate", { url: `${BASE}/` });
await wait(3500);
await shot("m01-home-board", 390, 844, true);
// ---- 2. nav drawer open ----
await click('button[aria-label="Open menu"]');
await wait(900);
await shot("m02-home-drawer", 390, 844, true);
// close via backdrop: click the backdrop element (base-ui renders it as a button/div behind)
await evalJs(`(() => { const b = document.querySelector('[data-slot="dialog-overlay"], .fixed.inset-0.z-40'); if (b) b.click(); const pop = document.querySelector('[data-slot="dialog-content"]'); return "done"; })()`);
await wait(500);
// ensure closed: dispatch Escape
await send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
await wait(600);
// ---- 3. second lane via chip ----
const chips = await evalJs(`Array.from(document.querySelectorAll("button[aria-pressed]")).map(b => b.textContent).join(" | ")`);
console.log("chips:", chips);
await click('button[aria-pressed="false"]');
await wait(1200);
await shot("m03-home-lane2", 390, 844, true);
// ---- 4. scheduled sheet ----
const dock = await evalJs(`(() => { const b = Array.from(document.querySelectorAll("nav[aria-label='Scheduled to-dos'] button")).pop(); if (!b) return "missing"; b.click(); return "clicked"; })()`);
console.log("dock ->", dock);
await wait(1000);
await shot("m04-scheduled-sheet", 390, 844, true);
await send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
await wait(600);
// ---- 5. projects page ----
await send("Page.navigate", { url: `${BASE}/projects` });
await wait(2500);
await shot("m05-projects", 390, 844, true);
// ---- 6. chat page ----
await send("Page.navigate", { url: `${BASE}/chat` });
await wait(2500);
await shot("m06-chat", 390, 844, true);
// ---- 7. tablet width (768, desktop shell boundary) ----
await send("Page.navigate", { url: `${BASE}/` });
await wait(2500);
await shot("m07-tablet-768", 768, 1024, false);
// ---- 8. desktop sanity ----
await send("Page.navigate", { url: `${BASE}/` });
await wait(2500);
await shot("m08-desktop-1440", 1440, 900, false);
proc.kill(); process.exit(0);
})().catch((e) => { console.error(e); process.exit(1); });