const { spawn } = require("node:child_process"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const http = require("node:http"); const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"; const PORT = 9394; const BASE = "http://localhost:3210"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 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(path.join(os.tmpdir(), "edge-prof-")); const proc = spawn(EDGE, ["--headless=new", "--remote-debugging-port=" + PORT, "--user-data-dir=" + profile, "--no-first-run", "--disable-gpu", "--window-size=1440,900", "about:blank"], { stdio: "ignore" }); let targets = null; for (let i = 0; i < 60; i++) { await sleep(500); try { targets = await httpJson("/json/list"); break; } catch {} } 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, reject) => { const myId = ++id; pending.set(myId, { resolve, reject }); ws.send(JSON.stringify({ id: myId, method, params })); setTimeout(() => { if (pending.has(myId)) { pending.delete(myId); reject(new Error("timeout " + method)); } }, 20000); }); ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id) { const p = pending.get(msg.id); if (p) { pending.delete(msg.id); msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result); } return; } if (msg.method === "Runtime.exceptionThrown") console.log("[EXCEPTION]", (msg.params.exceptionDetails.exception?.description ?? msg.params.exceptionDetails.text ?? "").slice(0, 200)); }; await send("Runtime.enable"); await send("Page.enable"); const js = (expression) => send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }).then((r) => (r && r.result ? r.result.value : undefined)); const clickAt = async (x, y) => { const opts = { x, y, button: "left", clickCount: 1 }; await send("Input.dispatchMouseEvent", { type: "mouseMoved", ...opts }); await send("Input.dispatchMouseEvent", { type: "mousePressed", ...opts }); await sleep(80); await send("Input.dispatchMouseEvent", { type: "mouseReleased", ...opts }); await sleep(400); }; const state = () => js(`(() => ({ dialogOpen: !!document.querySelector('[data-slot="dialog-content"]'), toggle: (Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Edit' || b.textContent.trim() === 'Preview') || {}).textContent, noDetails: document.body.innerText.includes('No details yet'), textareas: document.querySelectorAll('.w-md-editor textarea').length, toolbarDisabled: document.querySelectorAll('.w-md-editor-toolbar button[disabled]').length }))()`); await send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false }); await send("Page.navigate", { url: BASE + "/repro-notes" }); await sleep(5000); // Flow A: to-do -> Edit dialog -> Edit -> click in box -> type const todoBtn = await js(`(() => { const b = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Todo with no details'); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()`); await clickAt(todoBtn.x, todoBtn.y); await sleep(1000); const editBtn = await js(`(() => { const b = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Edit'); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()`); await clickAt(editBtn.x, editBtn.y); await sleep(2500); console.log("1 after Edit click:", JSON.stringify(await state())); // click into the editor box and type const ta = await js(`(() => { const t = document.querySelector('.w-md-editor textarea'); if (!t) return null; t.focus(); const r = t.getBoundingClientRect(); return { x: r.x + 50, y: r.y + 10, w: r.width, h: r.height }; })()`); console.log(" textarea rect:", JSON.stringify(ta)); if (ta) { await clickAt(ta.x, ta.y); await send("Input.insertText", { text: "hello from the fix" }); await sleep(500); } const v1 = await state(); const typed = await js(`(() => { const t = document.querySelector('.w-md-editor textarea'); return t ? t.value : null; })()`); console.log("2 after click+type:", JSON.stringify(v1), "textarea value:", JSON.stringify(typed)); // toggle to preview and back const toggle = await js(`(() => { const b = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Edit' || b.textContent.trim() === 'Preview'); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2, text: b.textContent.trim() }; })()`); await clickAt(toggle.x, toggle.y); await sleep(600); console.log("3 after toggle (preview):", JSON.stringify(await state())); const toggle2 = await js(`(() => { const b = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Edit' || b.textContent.trim() === 'Preview'); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2, text: b.textContent.trim() }; })()`); await clickAt(toggle2.x, toggle2.y); await sleep(2500); const v2 = await state(); const typed2 = await js(`(() => { const t = document.querySelector('.w-md-editor textarea'); return t ? t.value : null; })()`); console.log("4 after toggle back (edit):", JSON.stringify(v2), "textarea value:", JSON.stringify(typed2)); ws.close(); proc.kill(); process.exit(0); })().catch((e) => { console.error("FATAL", e); process.exit(1); });