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

109 lines
5.8 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { canvasReducer, decodeCanvas, emptyCanvas, makeCanvasNode } from "../src/lib/canvas.ts";
import { assignFrameMembership, frameDropFeedback } from "../src/lib/canvas-frames.ts";
function seeded() {
const frame = makeCanvasNode("frame", { x: 100, y: 100 });
const member = makeCanvasNode("text", { x: 160, y: 160 });
const outsider = makeCanvasNode("icon", { x: 900, y: 120 });
return { frame, member, outsider, document: { ...emptyCanvas(), nodes: [frame, member, outsider] } };
}
function state(document) { return { past: [], present: document, future: [] }; }
test("drop previews match actual assignment for every content type, exits and overlapping frames", () => {
const { frame } = seeded();
const smaller = makeCanvasNode("frame", { x: 120, y: 120 }); smaller.width = 300; smaller.height = 240;
for (const kind of ["text", "script", "image", "plain-text", "plain-image", "icon"]) {
const node = makeCanvasNode(kind, { x: 140, y: 140 });
const nodes = [frame, smaller, node];
let preview = frameDropFeedback(nodes, [node.id, frame.id]);
assert.equal(preview.length, 1); assert.equal(preview[0].to, smaller.id);
assert.equal(preview[0].to, assignFrameMembership(nodes, [node.id])[2].data.frameId);
node.data.frameId = smaller.id; node.position = { x: 2000, y: 2000 };
preview = frameDropFeedback(nodes, [node.id]); assert.equal(preview[0].from, smaller.id); assert.equal(preview[0].to, undefined);
assert.equal(assignFrameMembership(nodes, [node.id])[2].data.frameId, undefined);
}
});
test("only dropped content joins a frame; pre-existing overlapping content stays outside", () => {
const { frame, member, outsider, document } = seeded();
assert.equal(document.nodes[1].data.frameId, undefined);
const nodes = assignFrameMembership(document.nodes, [member.id]);
assert.equal(nodes[1].data.frameId, frame.id);
assert.equal(nodes[2].data.frameId, undefined);
const untouched = assignFrameMembership(document.nodes, [frame.id]);
assert.equal(untouched[1].data.frameId, undefined);
outsider.position = { x: 200, y: 200 };
assert.equal(assignFrameMembership([frame, member, outsider], [member.id])[2].data.frameId, undefined);
});
test("frames carry existing members without collecting newly overlapped content and undo restores positions", () => {
const { frame, member, outsider, document } = seeded();
member.data.frameId = frame.id;
let history = canvasReducer(state(document), { type: "checkpoint" });
history = canvasReducer(history, { type: "nodes", changes: [{ type: "position", id: frame.id, position: { x: 700, y: 100 } }] });
assert.deepEqual(history.present.nodes[1].position, { x: 760, y: 160 });
assert.equal(history.present.nodes[2].data.frameId, undefined);
assert.deepEqual(history.present.nodes[2].position, outsider.position);
history = canvasReducer(history, { type: "undo" });
assert.deepEqual(history.present.nodes[1].position, member.position);
assert.equal(history.present.nodes[1].data.frameId, frame.id);
});
test("dragging out detaches content and dragging between frames reassigns membership", () => {
const { frame, member } = seeded();
member.data.frameId = frame.id;
member.position = { x: 900, y: 900 };
assert.equal(assignFrameMembership([frame, member], [member.id])[1].data.frameId, undefined);
const second = makeCanvasNode("frame", { x: 800, y: 800 });
assert.equal(assignFrameMembership([frame, second, member], [member.id])[2].data.frameId, second.id);
});
test("multi-selection does not move frame members twice", () => {
const { frame, member, document } = seeded();
member.data.frameId = frame.id;
const result = canvasReducer(state(document), { type: "nodes", changes: [
{ type: "position", id: frame.id, position: { x: 200, y: 200 } },
{ type: "position", id: member.id, position: { x: 260, y: 260 } },
] });
assert.deepEqual(result.present.nodes[1].position, { x: 260, y: 260 });
});
test("deleting a frame keeps its content and undo restores membership", () => {
const { frame, member, document } = seeded();
member.data.frameId = frame.id;
const removed = canvasReducer(state(document), { type: "nodes", changes: [{ type: "remove", id: frame.id }] });
assert.equal(removed.present.nodes.length, 2);
assert.equal(removed.present.nodes[0].data.frameId, undefined);
assert.deepEqual(removed.present.nodes[0].position, member.position);
assert.equal(canvasReducer(removed, { type: "undo" }).present.nodes[1].data.frameId, frame.id);
});
test("all label modes, frame names and memberships survive save/restore and settings undo", () => {
const { frame, member, document } = seeded();
member.data.frameId = frame.id;
for (const labelMode of ["hidden", "fixed", "scaled"]) {
const result = canvasReducer(state(document), { type: "frame", id: frame.id, title: "灵感分区", labelMode });
const restored = decodeCanvas(JSON.stringify(result.present));
assert.equal(restored.nodes[0].type, "frame");
assert.equal(restored.nodes[0].data.title, "灵感分区");
assert.equal(restored.nodes[0].data.labelMode, labelMode);
assert.equal(restored.nodes[1].data.frameId, frame.id);
assert.equal(restored.nodes[0].zIndex, -1);
assert.equal(canvasReducer(result, { type: "undo" }).present.nodes[0].data.title, frame.data.title);
}
});
test("invalid frame references, label modes, frame nesting and rotation are rejected", () => {
for (const mutate of [
({ member }) => { member.data.frameId = "missing"; },
({ frame, member }) => { frame.data.frameId = member.id; },
({ frame }) => { frame.data.labelMode = "unexpected"; },
({ frame }) => { frame.data.rotation = 20; },
]) {
const input = seeded(); mutate(input);
assert.throws(() => decodeCanvas(JSON.stringify(input.document)));
}
});