import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "./create-template-state"; import { toolcraftReducer } from "./reducer"; import { createState } from "./reducer-test-utils"; describe("toolcraftReducer canvas and panels", () => { it("updates canvas size and records history", () => { const size = { width: 1200, height: 900, unit: "px" } as const; const state = toolcraftReducer(createState(), { size, type: "canvas.setSize", }); expect(state.canvas.size).toEqual(size); expect(state.history.undo.at(-1)?.label).toBe("Resize canvas"); }); it("preserves app-chosen canvas size changes below the app shell minimum", () => { const state = toolcraftReducer(createState(), { size: { width: 640, height: 900, unit: "px" }, type: "canvas.setSize", }); expect(state.canvas.size).toEqual({ width: 640, height: 900, unit: "px" }); expect(state.history.undo.at(-1)?.after).toEqual({ "canvas.size": { width: 640, height: 900, unit: "px" }, }); }); it("routes canvas size control targets through canvas runtime state", () => { const app = defineToolcraft({ canvas: { enabled: true, size: { width: 1200, height: 768, unit: "px" }, }, panels: { controls: { sections: [ { controls: { width: { defaultValue: 1200, target: "canvas.size.width", type: "text", }, }, }, ], title: "Controls", }, }, }); const changed = toolcraftReducer(createToolcraftState(app), { target: "canvas.size.width", type: "controls.setValue", value: "640", }); const reset = toolcraftReducer(changed, { type: "controls.reset" }); const undone = toolcraftReducer(changed, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); expect(changed.canvas.size.width).toBe(640); expect(changed.canvas.size.height).toBe(768); expect(changed.values["canvas.aspectRatio"]).toEqual({ height: 6, mode: "custom", value: "5:6", width: 5, }); expect(changed.values["canvas.size.width"]).toBe(640); expect(changed.values["canvas.size.height"]).toBe(768); expect(changed.history.undo.at(-1)?.label).toBe("canvas.size.width"); expect(reset.canvas.size.width).toBe(1200); expect(reset.canvas.size.height).toBe(768); expect(reset.values["canvas.size.width"]).toBe(1200); expect(reset.values["canvas.size.height"]).toBe(768); expect(reset.history.undo.at(-1)?.label).toBe("Reset controls"); expect(undone.canvas.size.width).toBe(1200); expect(undone.canvas.size.height).toBe(768); expect(redone.canvas.size.width).toBe(640); expect(redone.canvas.size.height).toBe(768); }); it("routes canvas aspect ratio presets through canvas runtime state", () => { const state = createState(); const changed = toolcraftReducer(state, { target: "canvas.aspectRatio", type: "controls.setValue", value: { height: 9, mode: "preset", value: "16:9", width: 16, }, }); expect(changed.canvas.size).toEqual({ height: 1080, unit: "px", width: 1920 }); expect(changed.values["canvas.aspectRatio"]).toEqual({ height: 9, mode: "preset", value: "16:9", width: 16, }); expect(changed.values["canvas.size.width"]).toBe(1920); expect(changed.values["canvas.size.height"]).toBe(1080); expect(changed.history.undo.at(-1)?.label).toBe("canvas.aspectRatio"); }); it("turns manual canvas size edits into a custom aspect ratio", () => { const state = toolcraftReducer(createState(), { target: "canvas.aspectRatio", type: "controls.setValue", value: { height: 9, mode: "preset", value: "16:9", width: 16, }, }); const changed = toolcraftReducer(state, { target: "canvas.size.height", type: "controls.setValue", value: "720", }); expect(changed.canvas.size).toEqual({ height: 720, unit: "px", width: 1920 }); expect(changed.values["canvas.aspectRatio"]).toEqual({ height: 3, mode: "custom", value: "8:3", width: 8, }); expect(changed.values["canvas.size.height"]).toBe(720); expect(changed.values["canvas.size.width"]).toBe(1920); }); it("keeps repeated canvas size values as no-op without changing aspect ratio mode", () => { const app = defineToolcraft({ canvas: { enabled: true, size: { height: 1080, unit: "px", width: 1920 }, sizing: { mode: "editable-output" }, }, panels: { controls: { sections: [], title: "Controls", }, }, }); const state = createToolcraftState(app); const unchanged = toolcraftReducer(state, { target: "canvas.size.width", type: "controls.setValue", value: "1920", }); expect(unchanged).toBe(state); expect(unchanged.values["canvas.aspectRatio"]).toEqual({ height: 9, mode: "preset", value: "16:9", width: 16, }); expect(unchanged.history.undo).toEqual([]); }); it("updates canvas offset without recording history", () => { const state = createState(); const next = toolcraftReducer(state, { offset: { x: 12, y: -8 }, type: "canvas.setOffset", }); expect(next.canvas.offset).toEqual({ x: 12, y: -8 }); expect(next.history.undo).toHaveLength(0); }); it("pans canvas by a delta", () => { const moved = toolcraftReducer(createState(), { offset: { x: 12, y: -8 }, type: "canvas.setOffset", }); const next = toolcraftReducer(moved, { delta: { x: 3, y: 10 }, type: "canvas.panBy", }); expect(next.canvas.offset).toEqual({ x: 15, y: 2 }); }); it("undoes canvas size changes", () => { const changed = toolcraftReducer(createState(), { size: { width: 1200, height: 900, unit: "px" }, type: "canvas.setSize", }); const state = toolcraftReducer(changed, { type: "history.undo" }); expect(state.canvas.size).toEqual({ width: 1024, height: 768, unit: "px" }); expect(state.values).not.toHaveProperty("canvas.size"); }); it("redoes canvas size changes", () => { const changed = toolcraftReducer(createState(), { size: { width: 1200, height: 900, unit: "px" }, type: "canvas.setSize", }); const undone = toolcraftReducer(changed, { type: "history.undo" }); const state = toolcraftReducer(undone, { type: "history.redo" }); expect(state.canvas.size).toEqual({ width: 1200, height: 900, unit: "px" }); expect(state.values).not.toHaveProperty("canvas.size"); }); it("zooms in, out, and resets", () => { let state = createState(); state = toolcraftReducer(state, { type: "canvas.zoomOut" }); state = toolcraftReducer(state, { type: "canvas.zoomOut" }); state = toolcraftReducer(state, { type: "canvas.zoomIn" }); expect(state.canvas.zoom).toBe(90); state = toolcraftReducer(state, { type: "canvas.zoomReset" }); expect(state.canvas.zoom).toBe(100); }); it("sets viewport zoom and offset for gesture zoom", () => { const state = toolcraftReducer(createState(), { offset: { x: -100, y: 24 }, type: "canvas.setViewport", zoom: 999, }); expect(state.canvas.offset).toEqual({ x: -100, y: 24 }); expect(state.canvas.zoom).toBe(400); }); it("updates panel offsets without changing control values", () => { const state = createState(); const next = toolcraftReducer(state, { offset: { x: 24, y: -12 }, panelId: "controls", type: "panels.setOffset", }); expect(next.panels.controls.offset).toEqual({ x: 24, y: -12 }); expect(next.values).toBe(state.values); }); it("keeps repeated legacy panel placement commands as no-ops", () => { const state = createState(); const sameOffset = toolcraftReducer(state, { offset: { x: 0, y: 0 }, panelId: "controls", type: "panels.setOffset", }); const sameReset = toolcraftReducer(state, { panelId: "controls", type: "panels.resetOffset", }); expect(sameOffset).toBe(state); expect(sameReset).toBe(state); }); it("updates complete panel workspace state without adding history", () => { const state = createState(); const next = toolcraftReducer(state, { panelId: "controls", patch: { collapsed: true, hidden: true, offset: { x: 28, y: -14 }, snapEdge: "left", }, type: "panels.update", }); expect(next.panels.controls).toMatchObject({ collapsed: true, hidden: true, offset: { x: 28, y: -14 }, snapEdge: "left", }); expect(next.history).toBe(state.history); expect(next.values).toBe(state.values); }); it("stores section collapse by stable section id and ignores unknown ids", () => { const state = createState(); const sectionId = state.schema.panels.controls?.sections[0]?.id; expect(sectionId).toBeTruthy(); const collapsed = toolcraftReducer(state, { collapsed: true, sectionId: sectionId!, type: "panels.setSectionCollapsed", }); const ignored = toolcraftReducer(collapsed, { collapsed: true, sectionId: "missing-section", type: "panels.setSectionCollapsed", }); expect(collapsed.panels.controls.collapsedSections).toEqual({ [sectionId!]: true, }); expect(collapsed.history).toBe(state.history); expect(ignored).toBe(collapsed); }); it("stores timeline extended presentation outside control values", () => { const state = createState(); const extended = toolcraftReducer(state, { target: "panels.timeline.extended", type: "controls.setValue", value: true, }); expect(extended.panels.timeline.extended).toBe(true); expect(extended.values).toBe(state.values); expect(extended.history).toBe(state.history); const compact = toolcraftReducer(extended, { target: "panels.timeline.extended", type: "controls.setValue", value: false, }); expect(compact.panels.timeline.extended).toBe(false); expect(compact.values).toBe(state.values); }); it("keeps legacy timeline panel visibility outside control values", () => { const state = createState(); const hidden = toolcraftReducer(state, { target: "panels.timeline.visible", type: "controls.setValue", value: false, }); expect(hidden.panels.timeline.hidden).toBe(true); expect(hidden.values).toBe(state.values); expect(hidden.history).toBe(state.history); const shown = toolcraftReducer(hidden, { hidden: false, panelId: "timeline", type: "panels.setHidden", }); expect(shown.panels.timeline.hidden).toBe(false); expect(shown.values).toBe(state.values); }); it("resets a panel offset to its default position", () => { const moved = toolcraftReducer(createState(), { offset: { x: 40, y: 80 }, panelId: "controls", type: "panels.setOffset", }); const next = toolcraftReducer(moved, { panelId: "controls", type: "panels.resetOffset", }); expect(next.panels.controls.offset).toEqual({ x: 0, y: 0 }); }); });