import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../../schema/define-toolcraft"; import { createToolcraftState } from "../../state/create-template-state"; import { toolcraftReducer } from "../../state/reducer"; import type { ToolcraftCommand, ToolcraftState } from "../../state/types"; import { applyToolcraftSettingsPayload, createToolcraftSettingsPayload, parseToolcraftSettingsPayload, } from "./settings-transfer"; function createSettingsSchema() { return defineToolcraft({ canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } }, panels: { controls: { sections: [ { controls: { fill: { defaultValue: "#112233", label: "Fill", target: "appearance.fill", type: "color", }, prompt: { defaultValue: "Initial prompt", label: "Prompt", target: "generation.prompt", type: "text", }, opacity: { defaultValue: 75, label: "Opacity", max: 100, min: 0, target: "style.opacity", type: "slider", }, }, title: "Generation", }, ], title: "Settings Test", }, timeline: { mode: "playback" }, }, settingsTransfer: { additionalValueTargets: ["composition.layout"], appId: "Settings Test", enabled: true, }, }); } function reduceWithCommands( initialState: ToolcraftState, run: (dispatch: (command: ToolcraftCommand) => void) => void, ): ToolcraftState { let state = initialState; run((command) => { state = toolcraftReducer(state, command); }); return state; } describe("settings transfer", () => { it("creates and parses a versioned app-specific payload", () => { const schema = createSettingsSchema(); const state = createToolcraftState(schema, { values: { "composition.layout": { elements: [{ id: "title", x: 120, y: 80 }], }, "generation.prompt": "Exported prompt", "panels.timeline.extended": true, "panels.timeline.visible": false, "style.opacity": 24, "unknown.target": "Ignored", }, }); const payload = createToolcraftSettingsPayload(state); expect(payload).toMatchObject({ appId: "settings-test", canvas: { mode: "finite", size: { height: 720, unit: "px", width: 1280 }, }, source: "toolcraft-settings", values: { "composition.layout": { elements: [{ id: "title", x: 120, y: 80 }], }, "generation.prompt": "Exported prompt", "style.opacity": 24, }, version: 2, }); expect(payload.values).not.toHaveProperty("panels.timeline.extended"); expect(payload.values).not.toHaveProperty("panels.timeline.visible"); expect(payload.values).not.toHaveProperty("unknown.target"); expect(parseToolcraftSettingsPayload(schema, payload)).toEqual(payload); expect( parseToolcraftSettingsPayload( schema, { ...payload, appId: "other-app" }, ), ).toBeNull(); }); it("applies imported values, canvas size, and timeline through runtime commands", () => { const schema = createSettingsSchema(); const initialState = createToolcraftState(schema, { timeline: { currentTimeSeconds: 0, durationSeconds: 8, expanded: false, isLooping: true, isPlaying: true, }, values: { "generation.prompt": "Initial prompt", "style.opacity": 75, }, }); const payload = { ...createToolcraftSettingsPayload(initialState), canvas: { mode: "finite" as const, size: { height: 900, unit: "px" as const, width: 1600 }, }, timeline: { currentTimeSeconds: 3, durationSeconds: 12, expanded: true, isLooping: false, isPlaying: false as const, }, values: { "composition.layout": { elements: [{ id: "title", x: 420, y: 280 }], }, "generation.prompt": "Imported prompt", "unknown.target": "Ignored", }, }; const state = reduceWithCommands(initialState, (dispatch) => { applyToolcraftSettingsPayload({ dispatch, state: initialState }, payload); }); expect(state.values["generation.prompt"]).toBe("Imported prompt"); expect(state.values["composition.layout"]).toEqual({ elements: [{ id: "title", x: 420, y: 280 }], }); expect(state.values["unknown.target"]).toBeUndefined(); expect(state.values["canvas.aspectRatio"]).toEqual({ height: 9, mode: "custom", value: "16:9", width: 16, }); expect(state.canvas.size).toEqual({ height: 900, unit: "px", width: 1600 }); expect(state.timeline).toMatchObject({ currentTimeSeconds: 3, durationSeconds: 12, expanded: true, isLooping: false, isPlaying: false, }); expect(state.history.undo).toEqual( expect.arrayContaining([ expect.objectContaining({ group: "settings.import", label: "Import settings", }), ]), ); }); it("normalizes v1 as finite and atomically imports v2 while already infinite", () => { const schema = createSettingsSchema(); const finite = createToolcraftState(schema); const infinite = toolcraftReducer(finite, { target: "canvas.infinity", type: "controls.setValue", value: true, }); const legacy = { ...createToolcraftSettingsPayload(finite), canvas: { size: { height: 600, unit: "px" as const, width: 800 } }, values: { "canvas.aspectRatio": { height: 3, mode: "preset" as const, value: "4:3", width: 4, }, }, version: 1 as const, }; const parsedLegacy = parseToolcraftSettingsPayload(schema, legacy); expect(parsedLegacy?.canvas).toMatchObject({ mode: "finite", size: { height: 600, unit: "px", width: 800 }, }); const imported = reduceWithCommands(infinite, (dispatch) => { applyToolcraftSettingsPayload( { dispatch, state: infinite }, { ...createToolcraftSettingsPayload(infinite), canvas: { mode: "infinite", size: { height: 900, unit: "px", width: 1600 }, }, }, ); }); expect(imported.canvas).toMatchObject({ mode: "infinite", size: { height: 900, unit: "px", width: 1600 }, }); expect(imported.values["canvas.size.width"]).toBe(1600); expect(imported.values["canvas.size.height"]).toBe(900); expect(imported.history.undo).toEqual( expect.arrayContaining([ expect.objectContaining({ group: "settings.import", label: "Import settings", }), ]), ); }); it("uses schema defaults for invalid imported control values", () => { const schema = createSettingsSchema(); const initialState = createToolcraftState(schema, { values: { "style.opacity": 24 }, }); const payload = { ...createToolcraftSettingsPayload(initialState), values: { "style.opacity": 200 }, }; const state = reduceWithCommands(initialState, (dispatch) => { applyToolcraftSettingsPayload({ dispatch, state: initialState }, payload); }); expect(state.values["style.opacity"]).toBe(75); }); it("canonicalizes legacy Color payloads during settings import", () => { const schema = createSettingsSchema(); const initialState = createToolcraftState(schema); const payload = { ...createToolcraftSettingsPayload(initialState), values: { "appearance.fill": { hex: "#abc" } }, }; const state = reduceWithCommands(initialState, (dispatch) => { applyToolcraftSettingsPayload({ dispatch, state: initialState }, payload); }); expect(state.values["appearance.fill"]).toBe("#AABBCC"); }); });