Restore default template binding across browser sessions
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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,
|
||||
@@ -22,6 +23,7 @@ test("template edit baseline distinguishes clean, dirty, and unbound scenes", ()
|
||||
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",
|
||||
@@ -31,6 +33,71 @@ test("template edit baseline distinguishes clean, dirty, and unbound scenes", ()
|
||||
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"),
|
||||
@@ -54,6 +121,7 @@ test("template manager uses a monitor and unified four-row card actions", async
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user