diff --git a/.cdp/verify-fix.cjs b/.cdp/verify-fix.cjs new file mode 100644 index 0000000..60e2afb --- /dev/null +++ b/.cdp/verify-fix.cjs @@ -0,0 +1,91 @@ +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); }); diff --git a/components/board/todo-create-dialog.tsx b/components/board/todo-create-dialog.tsx index d5278d8..53c319e 100644 --- a/components/board/todo-create-dialog.tsx +++ b/components/board/todo-create-dialog.tsx @@ -136,6 +136,7 @@ export function TodoCreateDialog({ value={details} onChange={(v) => setDetails(v ?? "")} height={160} + preview="edit" textareaProps={{ id: `create-todo-details-${groupId}` }} /> diff --git a/components/board/todo-create-popover.tsx b/components/board/todo-create-popover.tsx index b62bacc..4f3e5b5 100644 --- a/components/board/todo-create-popover.tsx +++ b/components/board/todo-create-popover.tsx @@ -78,6 +78,7 @@ export function TodoCreatePopover({ value={details} onChange={(v) => setDetails(v ?? "")} height={160} + preview="edit" textareaProps={{ id: `todo-details-${groupId}` }} /> diff --git a/components/board/todo-edit-dialog.tsx b/components/board/todo-edit-dialog.tsx index 83bb55b..15094ce 100644 --- a/components/board/todo-edit-dialog.tsx +++ b/components/board/todo-edit-dialog.tsx @@ -113,6 +113,7 @@ export function TodoEditDialog({ value={details} onChange={(v) => setDetails(v ?? "")} height={220} + preview="edit" /> ) : details ? (