/** * Variable read APIs: getVariableDeclarations / getVariableValues / * validateVariableValues. Semantics contract: declarations use the strict * canonical parser; values mirror the runtime's loose defaults + overrides * merge; validation matches --strict-variables. */ import { describe, it, expect } from "vitest"; import { openComposition } from "./session.js"; const DECLS = [ { id: "title", type: "string", label: "Title", default: "Hello" }, { id: "accent", type: "color", label: "Accent", default: "#00C3FF" }, { id: "count", type: "number", label: "Count", default: 3, min: 0, max: 10 }, { id: "dark", type: "boolean", label: "Dark mode", default: false }, { id: "layout", type: "enum", label: "Layout", default: "wide", options: [ { value: "wide", label: "Wide" }, { value: "tall", label: "Tall" }, ], }, ]; function htmlWithVariables(attr: string): string { return `

Hello

`; } const BASE_HTML = htmlWithVariables(JSON.stringify(DECLS)); describe("getVariableDeclarations", () => { it("returns the declared schema with per-type metadata intact", async () => { const comp = await openComposition(BASE_HTML); const decls = comp.getVariableDeclarations(); expect(decls.map((d) => d.id)).toEqual(["title", "accent", "count", "dark", "layout"]); const count = decls.find((d) => d.id === "count"); expect(count).toMatchObject({ type: "number", min: 0, max: 10, default: 3 }); const layout = decls.find((d) => d.id === "layout"); expect(layout).toMatchObject({ type: "enum", default: "wide" }); }); it("returns [] when the attribute is absent", async () => { const comp = await openComposition( `

x

`, ); expect(comp.getVariableDeclarations()).toEqual([]); }); it("returns [] for invalid JSON and drops malformed entries", async () => { const invalid = await openComposition(htmlWithVariables("{not json")); expect(invalid.getVariableDeclarations()).toEqual([]); const mixed = JSON.stringify([ { id: "ok", type: "string", label: "Ok", default: "yes" }, { id: "bad-type", type: "gradient", label: "Nope", default: "x" }, { id: "bad-default", type: "number", label: "Nope", default: "not-a-number" }, "not-an-object", ]); const mixedComp = await openComposition(htmlWithVariables(mixed)); const decls = mixedComp.getVariableDeclarations(); expect(decls.map((d) => d.id)).toEqual(["ok"]); }); }); describe("getVariableValues", () => { it("returns declared defaults when no overrides given", async () => { const comp = await openComposition(BASE_HTML); expect(comp.getVariableValues()).toEqual({ title: "Hello", accent: "#00C3FF", count: 3, dark: false, layout: "wide", }); }); it("overrides win and undeclared override keys pass through (runtime parity)", async () => { const comp = await openComposition(BASE_HTML); const values = comp.getVariableValues({ title: "Custom", extra: 42 }); expect(values.title).toBe("Custom"); expect(values.accent).toBe("#00C3FF"); expect(values.extra).toBe(42); }); it("uses the loose runtime defaults filter, not the strict declaration parser", async () => { // A number variable with a string default is dropped by the strict parser // but its default still flows through the runtime's readDeclaredDefaults — // getVariableValues must match what a composition script actually reads. const attr = JSON.stringify([ { id: "loose", type: "number", label: "Loose", default: "not-a-number" }, ]); const comp = await openComposition(htmlWithVariables(attr)); expect(comp.getVariableDeclarations()).toEqual([]); expect(comp.getVariableValues()).toEqual({ loose: "not-a-number" }); }); it("returns {} for a composition with no declarations", async () => { const comp = await openComposition( `

x

`, ); expect(comp.getVariableValues()).toEqual({}); expect(comp.getVariableValues({ a: 1 })).toEqual({ a: 1 }); }); }); describe("validateVariableValues", () => { it("returns [] for values conforming to the schema", async () => { const comp = await openComposition(BASE_HTML); expect(comp.validateVariableValues({ title: "x", count: 5, dark: true })).toEqual([]); }); it("flags undeclared keys, type mismatches, and enum violations", async () => { const comp = await openComposition(BASE_HTML); const issues = comp.validateVariableValues({ ghost: "boo", count: "five", layout: "diagonal", }); const kinds = issues.map((i) => `${i.kind}:${i.variableId}`).sort(); expect(kinds).toEqual(["enum-out-of-range:layout", "type-mismatch:count", "undeclared:ghost"]); }); it("stays consistent after setVariableValue edits the default", async () => { const comp = await openComposition(BASE_HTML); comp.setVariableValue("title", "Edited"); expect(comp.getVariableValues().title).toBe("Edited"); expect(comp.getVariableDeclarations().find((d) => d.id === "title")?.default).toBe("Edited"); expect(comp.validateVariableValues({ title: "still-a-string" })).toEqual([]); }); });