Files
Endoleap/tests/canvas-style.test.mjs

74 lines
4.5 KiB
JavaScript

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 = "<svg onload=evil>";
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"));
});