133 lines
6.5 KiB
JavaScript
133 lines
6.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import { BOARD_SESSION_KEY, readBoardSession, writeBoardSession, loadDefaultBoard, initializeDefaultBoard } from "../app/static/board-startup-model.js";
|
|
|
|
import {
|
|
createTemplateEditContext,
|
|
sceneNeedsTemplateSave,
|
|
templateEditSnapshot,
|
|
} from "../app/static/template-edit-model.js";
|
|
|
|
const staticRoot = new URL("../app/static/", import.meta.url);
|
|
|
|
test("template edit baseline distinguishes clean, dirty, and unbound scenes", () => {
|
|
const context = createTemplateEditContext({
|
|
id: "template-1",
|
|
name: "测试模板",
|
|
revision: "revision-1",
|
|
read_only: true,
|
|
}, "scene-a");
|
|
|
|
assert.equal(sceneNeedsTemplateSave(null, "scene-a"), true);
|
|
assert.equal(sceneNeedsTemplateSave(context, "scene-a"), false);
|
|
assert.equal(sceneNeedsTemplateSave(context, "scene-b"), true);
|
|
assert.deepEqual(templateEditSnapshot(context, "scene-a"), {
|
|
type: "template",
|
|
id: "template-1",
|
|
name: "测试模板",
|
|
revision: "revision-1",
|
|
readOnly: true,
|
|
dirty: false,
|
|
});
|
|
assert.equal(templateEditSnapshot(context, "scene-b").dirty, true);
|
|
});
|
|
|
|
test("same boot restores binding and original dirty baseline; new boot or mismatched draft does not", () => {
|
|
const values = new Map();
|
|
const storage = { getItem: (key) => values.get(key), setItem: (key, value) => values.set(key, value) };
|
|
const context = createTemplateEditContext({ id: "saved", name: "默认画面", revision: "original" }, "clean-scene");
|
|
writeBoardSession(storage, "boot-a", "dirty-scene", context);
|
|
const restored = readBoardSession(storage, "dirty-scene", "boot-a");
|
|
assert.deepEqual(restored.context, context);
|
|
assert.equal(templateEditSnapshot(restored.context, "dirty-scene").dirty, true);
|
|
assert.equal(restored.context.revision, "original");
|
|
assert.equal(readBoardSession(storage, "dirty-scene", "boot-b").context, null);
|
|
assert.equal(readBoardSession(storage, "another-scene", "boot-a").context, null);
|
|
assert.equal(readBoardSession(storage, "another-scene", "boot-a").bootId, "boot-a");
|
|
values.set(BOARD_SESSION_KEY, '{"version":99}');
|
|
assert.throws(() => readBoardSession(storage, "dirty-scene", "boot-a"), /原值已保留/);
|
|
assert.equal(values.get(BOARD_SESSION_KEY), '{"version":99}');
|
|
});
|
|
|
|
test("default static loads its complete latest scene and both demos keep read-only ownership", async () => {
|
|
const staticDetail = { id: "static", name: "默认静态", revision: "r2", scene: "complete-scene" };
|
|
const calls = [];
|
|
const normal = await loadDefaultBoard(async (url) => {
|
|
calls.push(url);
|
|
return url.endsWith("default-content") ? { type: "template", id: "static" } : staticDetail;
|
|
});
|
|
assert.deepEqual(normal, { ...staticDetail, type: "template" });
|
|
assert.deepEqual(calls, ["/api/display/default-content", "/api/templates/static"]);
|
|
const staticDemo = createTemplateEditContext({ ...normal, read_only: true }, "baseline");
|
|
assert.equal(staticDemo.readOnly, true);
|
|
const demo = await loadDefaultBoard(async (url) => {
|
|
if (url.endsWith("default-content")) return { type: "animation", id: "demo", is_demo: true };
|
|
if (url.endsWith("/frames")) return { id: "demo", name: "演示动图", revision: "r1", frames: [{ id: "first" }, { id: "second" }] };
|
|
assert.equal(url, "/api/animations/demo/frames/first");
|
|
return { scene: "first-scene" };
|
|
});
|
|
assert.equal(demo.scene, "first-scene");
|
|
const context = createTemplateEditContext(demo, demo.scene);
|
|
assert.equal(context.type, "animation");
|
|
assert.equal(context.readOnly, true);
|
|
assert.equal(context.id, "demo");
|
|
assert.equal(await loadDefaultBoard(async () => ({ type: "animation", is_demo: false })), null);
|
|
});
|
|
|
|
test("startup skips protected drafts and never replaces intervening edits or failed reads", async () => {
|
|
let allowed = false, applied = 0, retained = 0, loaded = 0;
|
|
const options = {
|
|
canLoad: () => allowed,
|
|
load: async () => { loaded++; return { scene: "default" }; },
|
|
apply: () => applied++, retain: () => retained++,
|
|
};
|
|
assert.equal(await initializeDefaultBoard(options), false);
|
|
assert.equal(loaded, 0);
|
|
allowed = true;
|
|
assert.equal(await initializeDefaultBoard(options), true);
|
|
assert.equal(applied, 1);
|
|
options.load = async () => { allowed = false; return { scene: "late-default" }; };
|
|
assert.equal(await initializeDefaultBoard(options), false);
|
|
assert.equal(applied, 1);
|
|
assert.equal(retained, 1);
|
|
allowed = true;
|
|
options.load = async () => { throw Error("deleted or unavailable"); };
|
|
await assert.rejects(initializeDefaultBoard(options), /deleted or unavailable/);
|
|
assert.equal(applied, 1);
|
|
assert.equal(retained, 1);
|
|
});
|
|
|
|
test("template manager uses a monitor and unified four-row card actions", async () => {
|
|
const [html, board, templates, style] = await Promise.all([
|
|
readFile(new URL("index.html", staticRoot), "utf8"),
|
|
readFile(new URL("scene-board.js", staticRoot), "utf8"),
|
|
readFile(new URL("views/templates.js", staticRoot), "utf8"),
|
|
readFile(new URL("style.css", staticRoot), "utf8"),
|
|
]);
|
|
|
|
assert.match(board, /MONITOR_WORKSPACES = new Set\(\["device", "settings", "templates"\]\)/);
|
|
assert.match(templates, /actions\.append\(play, edit, copy, rename, remove\)/);
|
|
assert.match(templates, /template-card-delete-action/);
|
|
assert.match(templates, /api\/templates\/\$\{template\.id\}\/play/);
|
|
assert.match(templates, /enterTemplateEdit\(full\)/);
|
|
assert.match(templates, /当前内容将会丢失/);
|
|
assert.match(templates, /actionButton\("上移"/);
|
|
assert.match(templates, /actionButton\("下移"/);
|
|
assert.match(templates, /name\.dataset\.hoverTooltip = animation\.name/);
|
|
assert.match(templates, /template-card-name animation-folder-name/);
|
|
assert.match(style, /\.template-card-content-actions,[\s\S]*grid-template-columns: repeat\(2/);
|
|
assert.match(style, /\.template-card-delete-action\s*\{\s*grid-column: 1 \/ -1/);
|
|
assert.match(html, /id="template-save-choice-dialog"/);
|
|
assert.match(html, /value="current">保存到当前模板/);
|
|
assert.match(html, /value="new">另存为新模板/);
|
|
assert.match(style, /\.template-save-choice-actions \.demo-restricted-action\[aria-disabled="true"\][\s\S]*background: #050505/);
|
|
});
|
|
|
|
test("animation editing backs up and restores template edit ownership", async () => {
|
|
const board = await readFile(new URL("scene-board.js", staticRoot), "utf8");
|
|
assert.match(board, /templateEdit: templateEdit \? \{ \.\.\.templateEdit \} : null/);
|
|
assert.match(board, /templateEditContext: backup\.templateEdit/);
|
|
assert.match(board, /markTemplateSaved/);
|
|
});
|