import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; import { canvasNodeClipboardMime, canvasNodeFromClipboardData, canvasNodeFromClipboardItems, createPastedCanvasNode, parseCanvasNodeClipboard, serializeCanvasNodeClipboard, serializeCanvasNodeClipboardSvg } from "../src/ui/pages/CanvasWorkspace/canvas/canvasNodeClipboard.ts"; const pathNode = { id: "path-1", type: "frame", title: "Circle", x: 100, y: 200, width: 300, height: 180, content: JSON.stringify({ kind: "path", pathData: [1, 0, 0.5, 5, 0.25, 0, 0.75, 1, 1, 0.5, 11] }), tone: "shape", status: "draft", layerRole: "shape", fillColor: "transparent", strokeColor: "#111827", strokeWidth: 2 }; test("round-trips editable path nodes through the internal clipboard format", () => { const parsed = parseCanvasNodeClipboard(serializeCanvasNodeClipboard(pathNode)); assert.deepEqual(parsed, pathNode); assert.equal(JSON.parse(parsed.content).kind, "path"); }); test("creates a pasted path node at the clipboard anchor without rasterizing it", () => { const pasted = createPastedCanvasNode(pathNode, "path-copy", { x: 800, y: 600 }); assert.equal(pasted.id, "path-copy"); assert.equal(pasted.type, "frame"); assert.equal(pasted.layerRole, "shape"); assert.equal(pasted.title, pathNode.title); assert.equal(pasted.status, pathNode.status); assert.equal(JSON.parse(pasted.content).kind, "path"); assert.equal(pasted.x, 650); assert.equal(pasted.y, 510); }); test("prefers internal path data when the clipboard also contains a PNG preview", async () => { const serialized = serializeCanvasNodeClipboard(pathNode); const dataTransfer = { files: [new File(["png"], "preview.png", { type: "image/png" })], getData: (type) => (type === canvasNodeClipboardMime ? serialized : "") }; const clipboardItem = { types: ["image/png", canvasNodeClipboardMime], getType: async (type) => new Blob([type === canvasNodeClipboardMime ? serialized : "png"], { type }) }; assert.deepEqual(canvasNodeFromClipboardData(dataTransfer), pathNode); assert.deepEqual(await canvasNodeFromClipboardItems([clipboardItem]), pathNode); }); test("restores path data from the standard SVG clipboard fallback", () => { const svg = serializeCanvasNodeClipboardSvg(pathNode, ''); const dataTransfer = { getData: (type) => (type === "text/plain" ? svg : "") }; assert.match(svg, /^ { for (const extension of ["png", "webp"]) { const source = { ...pathNode, id: `${extension}-1`, type: "image", title: `source.${extension}`, content: `https://assets.example/source.${extension}`, renderContent: `https://assets.example/render.${extension}`, layerRole: "image", imageAdjustments: JSON.stringify({ brightness: 12 }) }; const pasted = createPastedCanvasNode(parseCanvasNodeClipboard(serializeCanvasNodeClipboard(source)), `${extension}-copy`, { x: 500, y: 400 }); assert.deepEqual(pasted, { ...source, id: `${extension}-copy`, x: 350, y: 310 }); } }); test("rejects unrelated or malformed clipboard text", () => { assert.equal(parseCanvasNodeClipboard("hello"), null); assert.equal(parseCanvasNodeClipboard('{"version":1}'), null); }); test("routes context-menu and keyboard copy-paste through the lossless node clipboard", async () => { const source = await readFile(new URL("../src/ui/pages/CanvasWorkspace/index.tsx", import.meta.url), "utf8"); const dataTransferPaste = source.slice(source.indexOf("const pasteFromClipboardData"), source.indexOf("const pasteFromSystemClipboard")); assert.ok(dataTransferPaste.indexOf("canvasNodeFromClipboardData") < dataTransferPaste.indexOf("latestDataTransferImageFile")); assert.match(source, /copy: \(\) => copyNode\(node\)/); assert.match(source, /paste: \(\) => void pasteFromSystemClipboard\(\)/); assert.match(source, /key === "c"\) return void run\(selectedHandlers\?\.copy\)/); assert.match(source, /pasteFromClipboardData\(event\.clipboardData\)/); assert.match(source, /writeClipboardBlob\(blob, node\)/); assert.match(source, /writeClipboardTextNode\(node\)/); });