Organize/.cdp/verify-project-sync.cjs

110 lines
6.2 KiB
JavaScript

/* Same sync check as verify-sync.cjs, but on a Project board (verifies the
projectId branch of getBoardSnapshot + the provider's scoping). */
const { spawn } = require("node:child_process");
const { execFileSync } = 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 = 9342;
const BASE = "http://localhost:3210";
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();
});
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function sql(statement) {
execFileSync("docker", ["exec", "organize-polltest-db", "psql", "-U", "organize", "-d", "organize", "-c", statement]);
}
(async () => {
const devUserId = execFileSync("docker", ["exec", "organize-polltest-db", "psql", "-U", "organize", "-d", "organize", "-tAc", "select id from \"User\" where email='dev@example.com'"]).toString().trim();
// Test project with one open to-do (clean slate regardless of prior runs).
sql(`delete from "Todo" where id='ptest-todo'; delete from "Group" where id='ptest-grp'; delete from "Category" where id='ptest-cat'; delete from "Project" where id='ptest-proj';`);
sql(`insert into "Project" (id, title, "ownerId", "createdAt", "updatedAt") values ('ptest-proj', 'Sync Test Project', '${devUserId}', now(), now());`);
sql(`insert into "Category" (id, name, "order", "projectId", "createdAt", "updatedAt") values ('ptest-cat', 'Lane A', 0, 'ptest-proj', now(), now());`);
sql(`insert into "Group" (id, title, color, "order", "noteContent", "categoryId", "createdAt", "updatedAt") values ('ptest-grp', 'P group', 'ocean', 0, '', 'ptest-cat', now(), now());`);
sql(`insert into "Todo" (id, title, "order", "groupId", "createdAt", "updatedAt") values ('ptest-todo', 'Project milk', 0, 'ptest-grp', now(), now());`);
const profile = fs.mkdtempSync("/tmp/edge-psync-");
const proc = spawn(EDGE, ["--headless=new", `--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 sleep(500); try { targets = await httpJson("/json/list"); break; } catch {} }
if (!targets) { console.error("no targets"); 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 pending = new Map();
const send = (method, params = {}) => new Promise((resolve) => {
const myId = ++id; pending.set(myId, resolve);
ws.send(JSON.stringify({ id: myId, method, params }));
});
const errors = [];
ws.onmessage = (m) => {
const msg = JSON.parse(m.data);
if (msg.id) { const r = pending.get(msg.id); if (r) { pending.delete(msg.id); r(msg.result ?? msg.error); } 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|Download the React DevTools/.test(txt)) errors.push(txt.slice(0, 200));
}
};
const evalJs = (expression) =>
send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true })
.then((r) => (r?.result ? r.result.value : r));
await send("Runtime.enable");
await send("Page.enable");
await send("Page.navigate", { url: `${BASE}/login` });
for (let i = 0; i < 60; i++) { await sleep(500); if (await evalJs("document.readyState") === "complete") break; }
const login = await evalJs(`(async () => {
const c = await (await fetch('/api/auth/csrf')).json();
const r = await fetch('/api/auth/callback/credentials', { method: 'POST', redirect: 'manual',
body: new URLSearchParams({ csrfToken: c.csrfToken, email: 'dev@example.com', password: 'password123' }) });
return r.status; })()`);
console.log("login status:", login);
await send("Page.navigate", { url: `${BASE}/projects/ptest-proj` });
let checkboxFound = false;
for (let i = 0; i < 60; i++) {
await sleep(500);
checkboxFound = await evalJs(`!!Array.from(document.querySelectorAll('[data-todo-checkbox]'))
.some((el) => (el.getAttribute('aria-label') || '').includes('Project milk'))`);
if (checkboxFound) break;
}
if (!checkboxFound) { console.error("FAIL: project board checkbox not found"); process.exit(1); }
console.log("Project milk before:", await evalJs(`Array.from(document.querySelectorAll('[data-todo-checkbox]'))
.find((el) => (el.getAttribute('aria-label') || '').includes('Project milk')).getAttribute('aria-checked')`));
const t0 = Date.now();
console.log("simulating phone: completing the project's to-do in the DB...");
sql(`update "Todo" set "completed"=true, "completedAt"=now() where id='ptest-todo';`);
let done = false;
for (let i = 0; i < 60; i++) {
await sleep(1000);
const state = await evalJs(`(() => {
const el = Array.from(document.querySelectorAll('[data-todo-checkbox]'))
.find((el) => (el.getAttribute('aria-label') || '').includes('Project milk'));
if (el) return el.getAttribute('aria-checked');
return document.querySelector('[aria-label="Expand P group"]') ? "collapsed-done" : "missing";
})()`);
if (state === "true" || state === "collapsed-done") { done = true; break; }
}
const elapsed = Math.round((Date.now() - t0) / 1000);
console.log(`project to-do after (${elapsed}s): ${done ? "synced" : "NOT synced"}`);
sql(`delete from "Todo" where id='ptest-todo'; delete from "Group" where id='ptest-grp'; delete from "Category" where id='ptest-cat'; delete from "Project" where id='ptest-proj';`);
console.log("console errors:", errors.length ? errors : "none");
const ok = done && errors.length === 0;
console.log(ok ? "ALL PASS" : "FAILURES PRESENT");
ws.close(); proc.kill();
process.exit(ok ? 0 : 1);
})().catch((e) => { console.error(e); process.exit(1); });