import { describe, expect, it } from "vitest"; import { imageExportModule } from "../modules/built-ins/image-export/image-export-module"; import { normalizeControlsPanelLayout } from "./controls-panel-normalization"; import { defineToolcraft } from "./define-toolcraft"; import type { ToolcraftProductDefinition } from "./product-base"; import type { ToolcraftControlsPanelSchema, ToolcraftControlSectionSchema, } from "./types"; const opacityControl = { applicability: { mode: "always" }, defaultValue: 75, max: 100, min: 0, target: "appearance.opacity", type: "slider", } as const; function definition( sections: readonly ToolcraftControlSectionSchema[], ): ToolcraftProductDefinition { return { base: { canvas: { enabled: true }, identity: { id: "section-identity", title: "Section identity" }, panels: { controls: { sections, title: "Controls" } }, }, modules: [], }; } function mixedPanel(): ToolcraftControlsPanelSchema { return { sections: [ { controls: { opacity: opacityControl, palette: { applicability: { mode: "always" }, target: "appearance.palette", type: "palette", }, }, id: "appearance", title: "Appearance", }, ], title: "Controls", }; } describe("Toolcraft control section identity normalization", () => { it("preserves section descriptions through definition resolution and normalization", () => { const description = "Controls the point population and popup shown for the active revenue tab."; const authored = definition([ { controls: { showPopup: { applicability: { mode: "always" }, target: "revenue.showPopup", type: "switch", }, }, description, id: "incremental-revenue", title: "Incremental Revenue", }, ]); const resolved = defineToolcraft(authored); const repeated = defineToolcraft(authored); const normalized = normalizeControlsPanelLayout(resolved.panels.controls!); for (const panel of [ resolved.panels.controls!, repeated.panels.controls!, normalized, ]) { expect( panel.sections.find(({ id }) => id === "incremental-revenue") ?.description, ).toBe(description); } }); it("preserves explicit always and conditional applicability through normalization", () => { const resolved = defineToolcraft( definition([ { controls: { enabled: { applicability: { mode: "always" }, target: "appearance.enabled", type: "switch", }, opacity: { ...opacityControl, applicability: { all: [{ equals: true, target: "appearance.enabled" }], mode: "conditional", }, }, }, id: "appearance", title: "Appearance", }, ]), ); const readApplicability = (panel: typeof resolved.panels.controls) => Object.fromEntries( panel?.sections .filter(({ id }) => id.startsWith("appearance")) .flatMap(({ controls }) => Object.entries(controls)) .map(([id, control]) => [id, control.applicability]) ?? [], ); expect(readApplicability(resolved.panels.controls)).toEqual({ enabled: { mode: "always", origin: "explicit" }, opacity: { all: [{ equals: true, target: "appearance.enabled" }], mode: "conditional", origin: "explicit", }, }); expect( readApplicability( normalizeControlsPanelLayout(resolved.panels.controls!), ), ).toEqual(readApplicability(resolved.panels.controls)); }); it("resolves one canonical set of product and module section ids on every definition", () => { const authored = { ...definition([ { controls: { prompt: { applicability: { mode: "always" }, defaultValue: "", target: "generation.prompt", type: "text", }, }, id: "generation", title: "Generation", }, ]), modules: [imageExportModule()], }; const firstIds = defineToolcraft(authored).panels.controls!.sections.map( ({ id }) => id, ); const secondIds = defineToolcraft(authored).panels.controls!.sections.map( ({ id }) => id, ); expect(secondIds).toEqual(firstIds); expect(firstIds.filter((id) => id === "generation")).toHaveLength(1); expect(firstIds.filter((id) => id === "runtime.setup")).toHaveLength(1); expect(firstIds.filter((id) => id === "runtime.image-export")).toHaveLength( 1, ); expect(firstIds.filter((id) => id === "runtime.export")).toHaveLength(1); }); it("derives deterministic child ids when mixed layout splits one authored section", () => { const firstIds = normalizeControlsPanelLayout(mixedPanel()).sections.map( ({ id }) => id, ); const secondIds = normalizeControlsPanelLayout(mixedPanel()).sections.map( ({ id }) => id, ); expect(firstIds).toHaveLength(2); expect(new Set(firstIds).size).toBe(2); expect(firstIds).toEqual(secondIds); expect(firstIds.every((id) => id.startsWith("appearance.part-"))).toBe( true, ); }); it("normalizes deterministic mixed-layout child ids idempotently", () => { const normalized = normalizeControlsPanelLayout(mixedPanel()); const normalizedAgain = normalizeControlsPanelLayout(normalized); const firstIds = normalized.sections.map(({ id }) => id); expect(firstIds).toHaveLength(2); expect(firstIds.every((id) => id.startsWith("appearance.part-"))).toBe( true, ); expect(normalizedAgain.sections.map(({ id }) => id)).toEqual(firstIds); }); it("preserves composed action-extracted and mixed-layout child ids", () => { const panel = mixedPanel(); const section = panel.sections[0]!; const normalized = normalizeControlsPanelLayout({ ...panel, sections: [ { ...section, controls: { ...section.controls, copy: { applicability: { mode: "always" }, actions: [{ label: "Copy", value: "copy" }], target: "appearance.actions", type: "panelActions", }, }, }, ], }); const normalizedAgain = normalizeControlsPanelLayout(normalized); const productIds = normalized.sections .map(({ id }) => id) .filter((id) => !id.startsWith("runtime.")); expect(productIds).toHaveLength(2); expect( productIds.every((id) => (id.match(/\.part-/gu) ?? []).length === 2), ).toBe(true); expect( normalizedAgain.sections .map(({ id }) => id) .filter((id) => !id.startsWith("runtime.")), ).toEqual(productIds); expect( normalizedAgain.sections.filter(({ id }) => id === "runtime.export"), ).toHaveLength(1); }); });