import { test } from "node:test"; import assert from "node:assert/strict"; import { branchCanvasNode, canvasReducer, decodeCanvas, emptyCanvas, makeCanvasNode } from "../src/lib/canvas.ts"; import { CanvasElement, CardElement, IconElement } from "../src/lib/canvas-elements.ts"; import { resizeFromPointer } from "../src/lib/canvas-geometry.ts"; function seeded() { return { ...emptyCanvas(), nodes: [makeCanvasNode("text", { x: 100, y: 200 })] }; } test("branch type selection creates the chosen text, script or uploaded image and keeps the link in one undo step", () => { for (const kind of ["text", "script", "image"]) { const original = seeded(); const src = kind === "image" ? "data:image/png;base64,YQ==" : undefined; const result = branchCanvasNode(original, original.nodes[0].id, "bottom", kind, src); assert.equal(result.nodes[1].data.kind, kind); assert.equal(result.nodes[1].data.src, src); assert.equal(result.edges[0].sourceHandle, "bottom"); assert.equal(result.edges[0].targetHandle, "top"); const restored = decodeCanvas(JSON.stringify(result)); assert.equal(restored.nodes[1].data.kind, kind); const history = canvasReducer({ past: [], present: original, future: [] }, { type: "replace", document: result }); assert.deepEqual(canvasReducer(history, { type: "undo" }).present, original); } }); test("a connection released on the canvas creates the chosen node at the release point", () => { const original = seeded(); const at = { x: -300, y: 700 }; const result = branchCanvasNode(original, original.nodes[0].id, "left", "script", undefined, at); assert.deepEqual(result.nodes[1].position, at); assert.equal(result.edges.length, 1); }); test("all text styles and font size survive serialization, undo and redo", () => { const original = seeded(); const style = { color: "#123456", background: "#fedcba", bold: true, italic: true, underline: true, align: "right", fontFamily: "serif", lineHeight: 1.8 }; const edited = canvasReducer({ past: [], present: original, future: [] }, { type: "style", id: original.nodes[0].id, style, fontSize: 32 }); const restored = decodeCanvas(JSON.stringify(edited.present)); assert.deepEqual(restored.nodes[0].data.textStyle, style); assert.equal(restored.nodes[0].data.fontSize, 32); const undo = canvasReducer(edited, { type: "undo" }); assert.equal(undo.present.nodes[0].data.textStyle, undefined); assert.equal(canvasReducer(undo, { type: "redo" }).present.nodes[0].data.textStyle.bold, true); const cleared = canvasReducer(edited, { type: "style", id: original.nodes[0].id, style: { bold: false, background: "transparent" } }); assert.equal(cleared.present.nodes[0].data.textStyle.color, style.color); assert.equal(decodeCanvas(JSON.stringify(cleared.present)).nodes[0].data.textStyle.bold, false); }); test("unsafe CSS, unknown styles and invalid icon identifiers cannot enter a restored canvas", () => { for (const style of [{ color: "url(https://example.com)" }, { background: "red" }, { fontFamily: "custom" }, { lineHeight: Infinity }, { bold: "true" }, { align: "justify" }, { align: ["left"] }, { fontFamily: ["sans"] }, { position: "fixed" }, []]) { const doc = seeded(); doc.nodes[0].data.textStyle = style; assert.throws(() => decodeCanvas(JSON.stringify(doc)), /样式/); } const icon = makeCanvasNode("icon", { x: 0, y: 0 }); icon.data.icon = ""; assert.throws(() => decodeCanvas(JSON.stringify({ ...emptyCanvas(), nodes: [icon] })), /图标/); }); test("icon elements inherit transforms without card branching, preserve their identity and resize proportionally", () => { const element = new IconElement({ x: 10, y: 20 }); assert(element instanceof CanvasElement); assert.equal(element instanceof CardElement, false); const icon = element.toNode(); icon.data.icon = "Rocket"; icon.data.textStyle = { color: "#cc6633" }; icon.data.rotation = 45; const result = decodeCanvas(JSON.stringify({ ...emptyCanvas(), nodes: [icon] })).nodes[0]; assert.equal(result.type, "icon"); assert.equal(result.data.icon, "Rocket"); assert.equal(result.data.textStyle.color, "#cc6633"); assert.equal(result.data.rotation, 45); icon.data.rotation = 0; const larger = resizeFromPointer(icon, "bottom-right", { x: 80, y: 80 }); assert.equal(larger.width, 160); assert.equal(larger.height, 160); const smaller = resizeFromPointer(icon, "bottom-right", { x: -1000, y: -1000 }); assert.equal(smaller.width, 24); assert.equal(smaller.height, 24); assert.throws(() => branchCanvasNode({ ...emptyCanvas(), nodes: [icon] }, icon.id, "right")); });