import { expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { Window } from "happy-dom"; const code = new Bun.Transpiler({ loader: "ts" }) .transformSync(readFileSync(new URL("../src/canvas.ts", import.meta.url), "utf8")) .replace(/^import .*;\s*$/gm, "") .replace("export function mountBlockCanvas", "function mountBlockCanvas"); test("canvas annotates exact node locations and cleans editor decorations without changing content", () => { const window = new Window(); const root = window.document.body; root.innerHTML = '
'; const original = root.innerHTML; const tree = { id: "root", type: "layout.email-document", slots: { content: [{ id: "column", type: "layout.column", slots: { content: [] } }] }, }; const before = JSON.stringify(tree); let destroyed = 0; const mount = new Function("mountBlockEditor", code + ";return mountBlockCanvas;")(() => ({ destroy: () => destroyed++, refresh() {}, })); const mounted = mount(root, tree, {}); const column = root.querySelector('[data-block-id="column"]')!; expect(column.getAttribute("data-block-parent")).toBe("root"); expect(column.getAttribute("data-block-slot")).toBe("content"); expect(column.getAttribute("draggable")).toBe("false"); expect(root.querySelectorAll("[data-block-handle]")).toHaveLength(1); const handle = root.querySelector("[data-block-handle]")!; let clicks = 0, pointers = 0; column.addEventListener("click", () => clicks++); column.addEventListener("dblclick", () => clicks++); column.addEventListener("pointerdown", () => pointers++); handle.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); handle.dispatchEvent(new window.MouseEvent("dblclick", { bubbles: true })); handle.dispatchEvent(new window.PointerEvent("pointerdown", { bubbles: true })); expect(clicks).toBe(0); expect(pointers).toBe(1); expect(root.querySelector("[data-block-slot-target]")?.getAttribute("data-block-parent")).toBe( "column", ); expect(JSON.stringify(tree)).toBe(before); mounted.destroy(); mounted.destroy(); expect(destroyed).toBe(1); expect(root.innerHTML).toBe(original); }); test("failed canvas mounting rolls back annotations", () => { const root = new Window().document.body; root.innerHTML = '
Content
'; const before = root.innerHTML; const mount = new Function("mountBlockEditor", code + ";return mountBlockCanvas;")(() => { throw new Error("backend unavailable"); }); expect(() => mount( root, { id: "root", type: "root", slots: { content: [{ id: "child", type: "text", slots: {} }] } }, {}, ), ).toThrow("backend unavailable"); expect(root.innerHTML).toBe(before); });