be0d0888ea
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>
55 lines
3.2 KiB
JavaScript
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\)\}/);
|
|
});
|