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 + ''; 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(); } });