153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import { StudioSession, importDesign, exportDesign, diffDesign } from "../src/index.ts";
|
|
import { CORE_DESIGN_SYSTEMS, validateDesignSystem } from "../../design-system/src/index.ts";
|
|
|
|
test("invalid edits preserve a valid preview, raw input, undo/redo and reload", () => {
|
|
const data = new Map<string, string>();
|
|
const storage = {
|
|
getItem: (k: string) => data.get(k) ?? null,
|
|
setItem: (k: string, v: string) => {
|
|
data.set(k, v);
|
|
},
|
|
};
|
|
const session = new StudioSession(CORE_DESIGN_SYSTEMS.soft, storage);
|
|
session.mutate((s) => {
|
|
s.tokens["shape.control"]!.value = "13px";
|
|
}, "Changed radius");
|
|
session.edit('{"tokens":', "Incomplete JSON");
|
|
expect(session.diagnostics[0]!.code).toBe("WRN-STUDIO-IMPORT");
|
|
expect(session.valid.tokens["shape.control"]!.value).toBe("13px");
|
|
session.undo();
|
|
expect(session.diagnostics).toEqual([]);
|
|
session.redo();
|
|
expect(session.source).toBe('{"tokens":');
|
|
const restored = new StudioSession(undefined, storage);
|
|
expect(restored.restore()).toBe(true);
|
|
expect(restored.source).toBe(session.source);
|
|
expect(restored.valid.tokens["shape.control"]!.value).toBe("13px");
|
|
expect(() => restored.markApplied()).toThrow();
|
|
expect(() => restored.mutate(() => {}, "x")).toThrow();
|
|
});
|
|
|
|
test("history stays bounded, branches after undo, and realistic full-preset history reloads", () => {
|
|
const data = new Map<string, string>();
|
|
const storage = {
|
|
getItem: (k: string) => data.get(k) ?? null,
|
|
setItem: (k: string, v: string) => {
|
|
data.set(k, v);
|
|
},
|
|
};
|
|
const session = new StudioSession(undefined, storage);
|
|
for (let i = 0; i < 30; i++)
|
|
session.mutate((s) => {
|
|
s.name = "Revision " + i;
|
|
}, "Name");
|
|
expect(session.history).toHaveLength(25);
|
|
const restored = new StudioSession(undefined, storage);
|
|
expect(restored.restore()).toBe(true);
|
|
restored.undo();
|
|
restored.mutate((s) => {
|
|
s.name = "Branch";
|
|
}, "Branched");
|
|
restored.redo();
|
|
expect(restored.valid.name).toBe("Branch");
|
|
expect(restored.history.at(-1)!.label).toBe("Branched");
|
|
});
|
|
|
|
test("corrupted storage and quota failures do not destroy in-memory drafts", () => {
|
|
const broken = new StudioSession(undefined, {
|
|
getItem: () => "{",
|
|
setItem: () => {
|
|
throw Error("quota");
|
|
},
|
|
});
|
|
expect(broken.restore()).toBe(false);
|
|
expect(broken.storageError).toContain("could not be restored");
|
|
broken.mutate((s) => {
|
|
s.name = "Kept";
|
|
}, "Edit");
|
|
expect(broken.valid.name).toBe("Kept");
|
|
expect(broken.storageError).toContain("Autosave unavailable");
|
|
});
|
|
|
|
test("canonical import/export round trips all recipes, modes, aliases and metadata", () => {
|
|
for (const system of Object.values(CORE_DESIGN_SYSTEMS)) {
|
|
const artifact = exportDesign(system);
|
|
const report = importDesign(artifact.canonical);
|
|
expect(report.diagnostics).toEqual([]);
|
|
expect(report.system).toEqual(system);
|
|
expect(artifact.css).toContain("data-wrn-owner");
|
|
expect(JSON.parse(artifact.theme).designSystem.system).toEqual(system);
|
|
}
|
|
});
|
|
|
|
test("legacy import reports unsupported fields and preserves literal semantic bindings", () => {
|
|
const report = importDesign(
|
|
JSON.stringify({
|
|
default: "light",
|
|
palette: "violet",
|
|
themes: { light: { "color-primary": "#112233", unknown: "4px" } },
|
|
}),
|
|
"legacy",
|
|
);
|
|
expect(report.diagnostics).toEqual([]);
|
|
expect(report.unsupported).toContain("palette");
|
|
expect(report.unsupported).toContain("themes.light.unknown");
|
|
expect(report.system!.modes!.light!["color.primary"]).toEqual({
|
|
type: "color",
|
|
value: "#112233",
|
|
legacy: "color-primary",
|
|
});
|
|
});
|
|
|
|
test("exchange subset preserves aliases and reports ambiguous semantic mapping and unsupported composites", () => {
|
|
const report = importDesign(
|
|
JSON.stringify({
|
|
brand: {
|
|
$type: "color",
|
|
base: { $value: "#112233" },
|
|
action: { $value: "{brand.base}" },
|
|
complex: { $value: { colorSpace: "srgb", components: [0, 0, 0] } },
|
|
},
|
|
$extensions: { x: 1 },
|
|
}),
|
|
"tokens",
|
|
);
|
|
expect(report.diagnostics).toEqual([]);
|
|
expect(report.system!.tokens["brand.action"]!.value).toEqual({ ref: "brand.base" });
|
|
expect(report.unsupported).toContain("brand.complex");
|
|
expect(report.unsupported).toContain("$extensions");
|
|
expect(report.ambiguous).toHaveLength(2);
|
|
});
|
|
|
|
test("imports reject oversized, cyclic, executable, polluted and missing-reference documents", () => {
|
|
expect(importDesign(" ".repeat(2_000_001)).diagnostics).not.toEqual([]);
|
|
expect(
|
|
importDesign('{"__proto__":{"$type":"color","$value":"#000"}}', "tokens").diagnostics,
|
|
).not.toEqual([]);
|
|
for (const tokens of [
|
|
{ a: { type: "color", value: { ref: "missing" } } },
|
|
{ a: { type: "color", value: { ref: "b" } }, b: { type: "color", value: { ref: "a" } } },
|
|
{ a: { type: "color", value: "red;}body{display:none" } },
|
|
]) {
|
|
const system = { schemaVersion: 1, id: "bad", name: "Bad", version: "1", tokens };
|
|
expect(importDesign(JSON.stringify(system)).diagnostics).not.toEqual([]);
|
|
}
|
|
});
|
|
|
|
test("review diffs identify affected paths and applied snapshots are detached", () => {
|
|
const session = new StudioSession();
|
|
session.mutate((s) => {
|
|
s.tokens["shape.control"]!.value = "6px";
|
|
}, "Radius");
|
|
expect(session.changes()).toContain("$.tokens.shape.control.value");
|
|
session.markApplied();
|
|
expect(session.changes()).toEqual([]);
|
|
session.mutate((s) => {
|
|
s.name = "Other";
|
|
}, "Name");
|
|
expect(session.applied.name).not.toBe("Other");
|
|
expect(diffDesign({ a: 1 }, { a: 2, b: 3 })).toEqual(["$.a", "$.b"]);
|
|
expect(validateDesignSystem(session.valid)).toEqual([]);
|
|
});
|