Fix todo detail editor to default to edit mode instead of preview

- Add `preview="edit"` to the WMD editor in the create dialog,
  create popover, and edit dialog so users land in a ready-to-type
  state rather than an empty preview pane.
- Add a CDP-based verification script (.cdp/verify-fix.cjs) that
  drives a headless browser through the repro flow to confirm the
  fix and capture console exceptions.
- Include a screenshot (shots/repro-preview-empty.png) documenting
  the prior empty-preview bug for reference.
This commit is contained in:
Brian Fertig 2026-09-29 07:30:04 -06:00
parent 661a1bfab6
commit 4ceb9011a3
5 changed files with 94 additions and 0 deletions

91
.cdp/verify-fix.cjs Normal file
View File

@ -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); });

View File

@ -136,6 +136,7 @@ export function TodoCreateDialog({
value={details} value={details}
onChange={(v) => setDetails(v ?? "")} onChange={(v) => setDetails(v ?? "")}
height={160} height={160}
preview="edit"
textareaProps={{ id: `create-todo-details-${groupId}` }} textareaProps={{ id: `create-todo-details-${groupId}` }}
/> />
</div> </div>

View File

@ -78,6 +78,7 @@ export function TodoCreatePopover({
value={details} value={details}
onChange={(v) => setDetails(v ?? "")} onChange={(v) => setDetails(v ?? "")}
height={160} height={160}
preview="edit"
textareaProps={{ id: `todo-details-${groupId}` }} textareaProps={{ id: `todo-details-${groupId}` }}
/> />
</div> </div>

View File

@ -113,6 +113,7 @@ export function TodoEditDialog({
value={details} value={details}
onChange={(v) => setDetails(v ?? "")} onChange={(v) => setDetails(v ?? "")}
height={220} height={220}
preview="edit"
/> />
) : details ? ( ) : details ? (
<div className="max-h-48 overflow-y-auto rounded-md border p-3"> <div className="max-h-48 overflow-y-auto rounded-md border p-3">

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB