import { describe, expect, it } from "vitest"; import { defineToolcraft } from "./define-toolcraft"; import { applyToolcraftAppDefaults, createToolcraftAppDefaults, parseToolcraftAppDefaults } from "./app-defaults"; import { createToolcraftState } from "../state/create-template-state"; import { createToolcraftExternalStore, toolcraftReducer } from "../composition/public-state"; const definition = { base: { identity: { id: "saved-parameters", title: "Saved parameters" }, canvas: { enabled: true, renderScale: true, size: { width: 1920, height: 1080, unit: "px" } }, panels: { controls: { title: "Parameters", sections: [{ id: "effect", title: "Effect", controls: { amount: { type: "slider", label: "Amount", target: "effect.amount", defaultValue: 10, min: 0, max: 100, applicability: { mode: "always" } }, color: { type: "colorOpacity", label: "Color", target: "effect.color", defaultValue: { hex: "#112233", opacity: 40 }, applicability: { mode: "always" } }, mode: { type: "select", label: "Mode", target: "effect.mode", defaultValue: "first", options: [{ label: "First", value: "first" }, { label: "Second", value: "second" }], applicability: { mode: "always" } }, hidden: { type: "slider", label: "Hidden amount", target: "effect.hidden", defaultValue: 15, min: 0, max: 100, applicability: { mode: "conditional", all: [{ target: "effect.mode", equals: "second" }] } }, }, }] } }, }, modules: [], } as const; const schema = defineToolcraft(definition); function changedState() { return createToolcraftState(schema, { values: { "effect.amount": 72, "effect.color": { hex: "#ABCDEF", opacity: 65 }, "effect.hidden": 88, "canvas.renderScale": 1, } }); } describe("source app defaults", () => { it("captures hidden and compound parameters, quality and workspace state", () => { const captured = createToolcraftAppDefaults(changedState()); expect(captured.state.values).toMatchObject({ "effect.amount": 72, "effect.hidden": 88, "effect.color": { hex: "#ABCDEF", opacity: 65 } }); expect(captured.state.values["canvas.renderScale"]).toBe(1); expect(captured).not.toHaveProperty("history"); expect(captured.state.timeline).toEqual(changedState().timeline); }); it("makes fresh values, reset values and control default markers agree", () => { const saved = createToolcraftAppDefaults(changedState()); const nextSchema = defineToolcraft({ base: definition.base, modules: definition.modules, defaults: saved }); const fresh = createToolcraftState(nextSchema); expect(fresh.values["effect.amount"]).toBe(72); expect(fresh.defaults["effect.hidden"]).toBe(88); expect(nextSchema.panels.controls?.sections.find((section) => section.id === "effect")?.controls.amount?.defaultValue).toBe(72); const edited = toolcraftReducer(fresh, { type: "controls.setValue", target: "effect.amount", value: 20 }); expect(toolcraftReducer(edited, { type: "controls.reset" }).values["effect.amount"]).toBe(72); expect(createToolcraftState(schema).values["effect.amount"]).toBe(10); }); it("lets a persisted workspace override starting values while Reset uses the new defaults", () => { const nextSchema = defineToolcraft({ base: definition.base, modules: definition.modules, defaults: createToolcraftAppDefaults(changedState()) }); const restored = createToolcraftState(nextSchema, { values: { "effect.amount": 31 } }); expect(restored.values["effect.amount"]).toBe(31); expect(toolcraftReducer(restored, { type: "controls.resetTargets", targets: ["effect.amount"] }).values["effect.amount"]).toBe(72); }); it("saves editable canvas geometry and restores it through Reset", () => { const resized = toolcraftReducer(changedState(), { type: "canvas.applySettings", mode: "finite", size: { width: 640, height: 480, unit: "px" } }); const nextSchema = defineToolcraft({ base: definition.base, modules: definition.modules, defaults: createToolcraftAppDefaults(resized) }); const fresh = createToolcraftState(nextSchema); expect(fresh.canvas.size).toEqual({ width: 640, height: 480, unit: "px" }); const other = toolcraftReducer(fresh, { type: "canvas.applySettings", mode: "finite", size: { width: 800, height: 800, unit: "px" } }); expect(toolcraftReducer(other, { type: "controls.reset" }).canvas.size).toEqual(fresh.canvas.size); }); it("rejects wrong apps, malformed controls and incomplete snapshots", () => { const saved = createToolcraftAppDefaults(changedState()); for (const invalid of [ { ...saved, appId: "another-app" }, { ...saved, theme: ["light"] }, { ...saved, state: { ...saved.state, values: { ...saved.state.values, "effect.amount": "not a number" } } }, { ...saved, state: { ...saved.state, canvas: { mode: "finite", size: { width: -1, height: 1, unit: "px" } } } }, { ...saved, state: { ...saved.state, timeline: { ...saved.state.timeline, keyframeGroups: [{ invalid: true }] } } }, ]) expect(() => parseToolcraftAppDefaults(schema, invalid)).toThrow(); expect(applyToolcraftAppDefaults(schema, null)).toBe(schema); }); it("restores every workspace slice and arbitrary runtime values, with an undoable full Reset", () => { const current = changedState(); current.values["selection.owner"] = "b"; current.canvas.offset = { x: 31, y: -54 }; current.canvas.zoom = 1.75; current.panels.controls = { ...current.panels.controls, offset: { x: 55, y: 23 }, scrollTop: 50, collapsedSections: { effect: true } }; current.layers = [{ id: "a", name: "First", visible: false }, { id: "b", name: "Second", visible: true }]; current.selectedLayerId = null; current.timeline = { ...current.timeline, currentTimeSeconds: 2, durationSeconds: 12, isPlaying: false, isLooping: false, keyframeGroups: [{ controlId: "effect.amount", label: "Amount", keyframes: [{ id: "k", controlId: "effect.amount", controlLabel: "Amount", valueLabel: "60", value: 60, timeSeconds: 2 }] }], selectedKeyframeId: "k" }; const saved = createToolcraftAppDefaults(current, [], "light"); const nextSchema = defineToolcraft({ base: definition.base, modules: definition.modules, defaults: saved }); const fresh = createToolcraftState(nextSchema); expect(createToolcraftAppDefaults(fresh, [], "light")).toEqual(saved); const edited = createToolcraftState(nextSchema, { canvas: { zoom: 3 }, layers: [], selectedLayerId: null, timeline: { keyframeGroups: [] }, values: { "selection.owner": "a" } }); const reset = toolcraftReducer(edited, { type: "controls.reset" }); expect(createToolcraftAppDefaults(reset, [], "light")).toEqual(saved); expect(toolcraftReducer(reset, { type: "history.undo" }).canvas).toEqual(edited.canvas); expect(toolcraftReducer(reset, { type: "history.undo" }).layers).toEqual([]); expect(reset.history.undo).toHaveLength(1); }); it("continues to read version 1 parameter defaults", () => { const previous = { version: 1, appId: schema.identity.id, values: { "effect.amount": 42 }, canvas: null }; expect(createToolcraftState(applyToolcraftAppDefaults(schema, previous)).values["effect.amount"]).toBe(42); }); it("resets and undoes live viewport and playhead changes through the external store", () => { const saved = createToolcraftAppDefaults(changedState()); const store = createToolcraftExternalStore(createToolcraftState(applyToolcraftAppDefaults(schema, saved))); store.dispatch({ type: "canvas.setViewport", offset: { x: 150, y: -90 }, zoom: 2.5 }); store.dispatch({ type: "timeline.setCurrentTime", currentTimeSeconds: 3 }); const before = store.getState(); expect(before.timeline.currentTimeSeconds).toBe(3); store.dispatch({ type: "controls.reset" }); expect(store.getState().canvas).toEqual(saved.state.canvas); expect(store.getState().timeline.currentTimeSeconds).toBe(saved.state.timeline.currentTimeSeconds); store.dispatch({ type: "history.undo" }); expect(store.getState().canvas).toEqual(before.canvas); expect(store.getState().timeline.currentTimeSeconds).toBe(before.timeline.currentTimeSeconds); store.dispatch({ type: "history.redo" }); expect(store.getState().canvas).toEqual(saved.state.canvas); }); it("rejects unserializable values instead of publishing an incomplete snapshot", () => { for (const value of [Number.NaN, Infinity, new Map(), new Blob(["file"]), () => 1, undefined, { nested: undefined }, [undefined], new Array(2)]) { const state = changedState(); state.values["custom.value"] = value; expect(() => createToolcraftAppDefaults(state)).toThrow("cannot be saved losslessly"); } }); });