135 lines
5.7 KiB
TypeScript
135 lines
5.7 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import {
|
|
CORE_DESIGN_SYSTEMS,
|
|
validateDesignSystem,
|
|
compileDesignSystem,
|
|
} from "@wrnexus/design-system";
|
|
import { manualDesign } from "../src/manual.ts";
|
|
import { StudioSession } from "../src/session.ts";
|
|
import { checkDecisionLocks } from "../src/decisions.ts";
|
|
|
|
test("manual colors update only their mode and maintain readable contrast and legacy bindings", () => {
|
|
const source = CORE_DESIGN_SYSTEMS.soft;
|
|
const original = JSON.stringify(source);
|
|
const next = manualDesign(source, {
|
|
kind: "color",
|
|
mode: "light",
|
|
name: "primary",
|
|
value: "#000000",
|
|
});
|
|
expect(next.modes!.light!["color.primary"]!.value).toBe("#000000");
|
|
expect(next.modes!.light!["color.primary-contrast"]!.value).toBe("#FFFFFF");
|
|
expect(next.modes!.light!["color.focus"]!.legacy).toBe("color-focus");
|
|
expect(next.modes!.dark).toEqual(source.modes!.dark);
|
|
expect(JSON.stringify(source)).toBe(original);
|
|
expect(validateDesignSystem(next)).toEqual([]);
|
|
expect(() => compileDesignSystem(next, { mode: "light" })).not.toThrow();
|
|
expect(() =>
|
|
manualDesign(source, {
|
|
kind: "color",
|
|
mode: "light",
|
|
name: "primary",
|
|
value: "red; display:none",
|
|
}),
|
|
).toThrow();
|
|
});
|
|
test("manual foundations are recoverable and decision locks still detect changes", () => {
|
|
const session = new StudioSession(CORE_DESIGN_SYSTEMS.soft);
|
|
const before = JSON.stringify(session.valid);
|
|
const next = manualDesign(session.valid, { kind: "token", name: "shape.control", value: "8px" });
|
|
expect(checkDecisionLocks(session.valid, next, ["/tokens/shape.control"])).not.toHaveLength(0);
|
|
session.replace(next, "Manual shape");
|
|
expect(session.valid.tokens["shape.control"]!.value).toBe("8px");
|
|
session.undo();
|
|
expect(JSON.stringify(session.valid)).toBe(before);
|
|
session.redo();
|
|
expect(session.valid.tokens["shape.control"]!.value).toBe("8px");
|
|
expect(() =>
|
|
manualDesign(next, { kind: "token", name: "shape.control", value: "-2px" }),
|
|
).toThrow();
|
|
});
|
|
test("manual typography, spacing, surfaces and semantic colors validate for all core themes", () => {
|
|
for (const original of Object.values(CORE_DESIGN_SYSTEMS)) {
|
|
let system = structuredClone(original);
|
|
for (const [name, value] of [
|
|
["type.body", "Arial, sans-serif"],
|
|
["type.heading", "Georgia, serif"],
|
|
["space.panel", "24px"],
|
|
["shape.panel", "12px"],
|
|
["elevation.panel", "none"],
|
|
])
|
|
system = manualDesign(system, { kind: "token", name: name!, value: value! });
|
|
for (const mode of ["light", "dark"])
|
|
for (const name of ["secondary", "success", "warning", "danger", "info"] as const)
|
|
system = manualDesign(system, { kind: "color", mode, name, value: "#00677B" });
|
|
expect(validateDesignSystem(system)).toEqual([]);
|
|
expect(() => compileDesignSystem(system, { mode: "dark" })).not.toThrow();
|
|
}
|
|
});
|
|
|
|
test("manual button styles, tilt, pill shapes, default mode and advanced colors compile", () => {
|
|
let system = manualDesign(CORE_DESIGN_SYSTEMS.soft, { kind: "buttonStyle", value: "outline" });
|
|
system = manualDesign(system, { kind: "tilt", value: "none" });
|
|
system = manualDesign(system, { kind: "token", name: "shape.control", value: "999px" });
|
|
system = manualDesign(system, { kind: "mode", value: "dark" });
|
|
system = manualDesign(system, {
|
|
kind: "color",
|
|
mode: "dark",
|
|
name: "primary-contrast",
|
|
value: "#FFFFFF",
|
|
});
|
|
expect(system.defaultMode).toBe("dark");
|
|
expect(system.recipes!.Button!.variants!.at(-1)!.parts.control!.backgroundColor).toBe(
|
|
"transparent",
|
|
);
|
|
expect(system.recipes!.Button!.base!.control!.rotate).toBe("0deg");
|
|
expect(() => compileDesignSystem(system)).not.toThrow();
|
|
});
|
|
|
|
test("manual form saves one atomic revision and rejects locked changes without corrupting drafts", async () => {
|
|
const { Window } = await import("happy-dom");
|
|
const { manualHtml, setupManual } = await import("../src/manual-editor.ts");
|
|
const win = new Window();
|
|
const names = ["document", "HTMLInputElement", "Option"] as const;
|
|
const previous = Object.fromEntries(names.map((name) => [name, (globalThis as any)[name]]));
|
|
try {
|
|
(globalThis as any).document = win.document;
|
|
(globalThis as any).HTMLInputElement = win.HTMLInputElement;
|
|
(globalThis as any).Option = (win as any).Option;
|
|
win.document.body.innerHTML =
|
|
manualHtml + '<select id="preview-mode"><option value="light">light</option></select>';
|
|
const session = new StudioSession(CORE_DESIGN_SYSTEMS.soft);
|
|
let notice = "";
|
|
const editor = setupManual(session, {
|
|
changed: async () => {
|
|
editor.render();
|
|
},
|
|
message: (text) => {
|
|
notice = text;
|
|
},
|
|
mode: () => {},
|
|
});
|
|
editor.render();
|
|
const input = win.document.querySelector('[data-manual-token="shape.control"]') as any;
|
|
input.value = "12";
|
|
(win.document.querySelector('input[type=text][data-manual-color="primary"]') as any).value =
|
|
"#6336AD";
|
|
(win.document.querySelector("[data-manual-apply]") as any).click();
|
|
await Bun.sleep(10);
|
|
expect(session.valid.tokens["shape.control"]!.value).toBe("12px");
|
|
expect(session.valid.modes!.light!["color.primary"]!.value).toBe("#6336AD");
|
|
expect(session.history).toHaveLength(2);
|
|
session.setLocks(["/tokens/shape.control"]);
|
|
editor.render();
|
|
(win.document.querySelector('[data-manual-token="shape.control"]') as any).value = "24";
|
|
(win.document.querySelector("[data-manual-apply]") as any).click();
|
|
await Bun.sleep(10);
|
|
expect(notice).toContain("locked decision");
|
|
expect(session.valid.tokens["shape.control"]!.value).toBe("12px");
|
|
expect(session.diagnostics).toEqual([]);
|
|
} finally {
|
|
for (const name of names) (globalThis as any)[name] = previous[name];
|
|
await win.happyDOM.close();
|
|
}
|
|
});
|