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