Files
moteva/frontend/tests/canvasHistory.test.mjs
T
root be0d0888ea feat(canvas): add canvas history snapshot module
Add canvasHistory helpers that diff two node snapshots into a labeled
operation (boolean, add, delete, path edit, move, resize, style,
rename, reorder), keep the five most recent states, and truncate newer
entries when an older state is restored. Workspace wiring lands in a
follow-up commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 00:23:28 +08:00

55 lines
3.2 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { appendCanvasHistory, canvasHistoryOperation, truncateCanvasHistory } from "../src/ui/pages/CanvasWorkspace/canvas/canvasHistory.ts";
const rectangle = {
id: "shape-1",
type: "frame",
title: "Rectangle",
x: 0,
y: 0,
width: 100,
height: 100,
content: JSON.stringify({ kind: "rectangle" }),
tone: "shape",
status: "draft",
layerRole: "shape"
};
test("describes major canvas operations", () => {
assert.deepEqual(canvasHistoryOperation([], [rectangle]), { kind: "add", subject: "Rectangle" });
assert.deepEqual(canvasHistoryOperation([rectangle], [{ ...rectangle, x: 40 }]), { kind: "move", subject: "Rectangle" });
assert.deepEqual(canvasHistoryOperation([rectangle], [{ ...rectangle, width: 160 }]), { kind: "resize", subject: "Rectangle" });
assert.deepEqual(canvasHistoryOperation([rectangle], [{ ...rectangle, fillColor: "#ff0000" }]), { kind: "style", subject: "Rectangle" });
assert.deepEqual(canvasHistoryOperation([rectangle], [{ ...rectangle, content: JSON.stringify({ kind: "path", pathData: [1, 0, 0, 2, 1, 1] }) }]), { kind: "path", subject: "Rectangle" });
});
test("recognizes a boolean result as one major operation", () => {
const second = { ...rectangle, id: "shape-2", x: 50 };
const result = { ...rectangle, id: "boolean-result", title: "UNION", width: 150, content: JSON.stringify({ kind: "path", pathData: [1, 0, 0, 2, 1, 0, 2, 1, 1, 11] }) };
assert.deepEqual(canvasHistoryOperation([rectangle, second], [result]), { kind: "boolean", subject: "union" });
});
test("keeps five recent states and removes newer states when restoring", () => {
const entries = Array.from({ length: 5 }, (_, index) => ({ id: `history-${5 - index}`, label: String(5 - index), createdAt: 5 - index, nodes: [] }));
const appended = appendCanvasHistory(entries, { id: "history-6", label: "6", createdAt: 6, nodes: [] }, null);
const restored = truncateCanvasHistory(entries, "history-3");
const branched = appendCanvasHistory(restored, { id: "history-branch", label: "branch", createdAt: 7, nodes: [] }, "history-3");
assert.deepEqual(appended.map((entry) => entry.id), ["history-6", "history-5", "history-4", "history-3", "history-2"]);
assert.deepEqual(restored.map((entry) => entry.id), ["history-3", "history-2", "history-1"]);
assert.deepEqual(branched.map((entry) => entry.id), ["history-branch", "history-3", "history-2", "history-1"]);
});
test("previews history from the row and restores only from the rollback button", async () => {
const source = await readFile(new URL("../src/ui/components/CanvasPanels/index.tsx", import.meta.url), "utf8");
const historyMarkup = source.slice(source.indexOf('<ol className="layer-history-list">'), source.indexOf("</ol>", source.indexOf('<ol className="layer-history-list">')));
assert.match(historyMarkup, /<div className=\{`layer-history-entry/);
assert.match(historyMarkup, /<button[\s\S]*?className="layer-history-preview"[\s\S]*?onClick=\{\(\) => onPreviewHistory\(entry\.id\)\}/);
assert.match(historyMarkup, /<button[\s\S]*?className="layer-history-restore"[\s\S]*?onClick=\{\(\) => onRestoreHistory\(entry\.id\)\}/);
});