117 lines
4.4 KiB
JavaScript
117 lines
4.4 KiB
JavaScript
|
|
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, '<svg xmlns="http://www.w3.org/2000/svg"><path d="M0 0"/></svg>');
|
||
|
|
const dataTransfer = {
|
||
|
|
getData: (type) => (type === "text/plain" ? svg : "")
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.match(svg, /^<svg data-fluxo-canvas-node=/);
|
||
|
|
assert.deepEqual(canvasNodeFromClipboardData(dataTransfer), pathNode);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("preserves raster source formats and node properties when pasted", () => {
|
||
|
|
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\)/);
|
||
|
|
});
|