const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); const cdpPort = process.env.CDP_PORT || "9222"; const connect = async () => { const pages = await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`)).json(); const page = pages.find((item) => item.type === "page" && item.url.startsWith("http://localhost:5173")); if (!page) throw new Error("Chrome debugging has no localhost:5173 page"); const socket = new WebSocket(page.webSocketDebuggerUrl); await new Promise((resolve, reject) => { socket.addEventListener("open", resolve, { once: true }); socket.addEventListener("error", reject, { once: true }); }); let id = 0; const pending = new Map(); const exceptions = []; socket.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.method === "Runtime.exceptionThrown") exceptions.push(message.params.exceptionDetails.text); const request = pending.get(message.id); if (!request) return; pending.delete(message.id); if (message.error) request.reject(new Error(message.error.message)); else request.resolve(message.result); }); const send = (method, params = {}) => new Promise((resolve, reject) => { id += 1; pending.set(id, { resolve, reject }); socket.send(JSON.stringify({ id, method, params })); }); return { socket, send, exceptions }; }; const valueOf = async (send, expression) => { const result = await send("Runtime.evaluate", { expression, returnByValue: true }); if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text); return result.result?.value; }; const waitFor = async (send, expression, message) => { for (let attempt = 0; attempt < 40; attempt += 1) { if (await valueOf(send, expression)) return; await sleep(100); } throw new Error(message); }; const origin = process.argv[2] || "http://localhost:5173"; const openG03 = async (send, suffix = "") => { const url = `${origin}/?g03CreateAudit=${Date.now()}${suffix}#${"/pages/genealogy/g03-create-genealogy"}`; await send("Page.navigate", { url }); await waitFor(send, `location.href === ${JSON.stringify(url)}`, "G03 navigation failed"); await waitFor(send, "Boolean(document.querySelector('.create-card'))", "G03 create form did not render"); }; const run = async () => { const { socket, send, exceptions } = await connect(); try { await send("Page.enable"); await send("Runtime.enable"); await openG03(send); const text = await valueOf(send, "document.querySelector('.create-card')?.textContent || ''"); for (const required of ["立谱信息", "所在地区", "确认创建家谱"]) { if (!text.includes(required)) throw new Error(`G03 create form missing: ${required}`); } const inputCount = await valueOf(send, "document.querySelectorAll('.create-card input').length"); if (inputCount !== 5) throw new Error(`G03 expected five text inputs plus region and upload selectors, got ${inputCount}`); const regionSelector = await valueOf(send, "Boolean(document.querySelector('.field-row--selector'))"); if (!regionSelector) throw new Error("G03 region selector did not render"); const uploadControl = await valueOf(send, "Boolean(document.querySelector('.upload-button'))"); if (!uploadControl) throw new Error("G03 cover upload control did not render"); const introControl = await valueOf(send, "Boolean(document.querySelector('.create-card textarea'))"); if (!introControl) throw new Error("G03 optional intro field did not render"); const fakeControl = await valueOf(send, "Boolean(document.querySelector('.flow-success-dialog'))"); if (fakeControl) throw new Error("G03 must not expose the retired local bootstrap flow"); await valueOf(send, `(() => { const input = document.querySelector('.create-card input'); const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(input, 'discard-check'); input.dispatchEvent(new Event('input', { bubbles: true })); document.querySelector('.header-back')?.click(); return true; })()`); await waitFor(send, "Boolean(document.querySelector('.app-dialog-layer'))", "G03 dirty back did not require discard confirmation"); await valueOf(send, "document.querySelector('.app-dialog__actions .app-button:first-child')?.click()"); await waitFor(send, "!document.querySelector('.app-dialog-layer')", "G03 discard cancellation did not close the dialog"); if ((await valueOf(send, "document.querySelector('.create-card input')?.value")) !== "discard-check") { throw new Error("G03 discard cancellation lost the draft"); } for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) { await send("Emulation.setDeviceMetricsOverride", { ...size, deviceScaleFactor: 1, mobile: true }); await openG03(send, `&width=${size.width}`); const scrollWidth = await valueOf(send, "document.documentElement.scrollWidth"); if (scrollWidth > size.width + 1) throw new Error(`G03 create form has horizontal overflow at ${size.width}x${size.height}`); } if (exceptions.length) throw new Error(`G03 raised browser exceptions: ${exceptions.join("; ")}`); process.stdout.write("G03-CREATE-FLOW-RUNTIME-SMOKE PASS\n"); } finally { try { await send("Emulation.clearDeviceMetricsOverride"); } catch (_) {} socket.close(); } }; run().catch((error) => { process.stderr.write(`${error.stack || error.message}\n`); process.exit(1); });