import { describe, expect, it } from "vitest"; import { toolcraftCanvasZoomMax, toolcraftCanvasZoomMin, } from "./canvas-zoom"; import { createState } from "./reducer-test-utils"; import { createToolcraftExternalStore } from "./toolcraft-external-store"; import type { ToolcraftCommand, ToolcraftPoint } from "./types"; function applyViewportOverlay( store: ReturnType, offset: ToolcraftPoint, zoom: number, ): void { store.dispatchTransient({ offset, type: "canvas.setViewport", zoom, }); } describe("createToolcraftExternalStore durable hot-field intent", () => { it.each([ { commandOffset: { x: 30, y: 15 }, label: "different from", }, { commandOffset: { x: 24, y: -12 }, label: "equal to", }, ])("setOffset $label the effective offset preserves transient zoom", ({ commandOffset }) => { const store = createToolcraftExternalStore(createState()); applyViewportOverlay(store, { x: 24, y: -12 }, 150); store.dispatch({ offset: commandOffset, type: "canvas.setOffset" }); expect(store.getCommittedState().canvas).toMatchObject({ offset: commandOffset, zoom: 100, }); expect(store.getState().canvas).toMatchObject({ offset: commandOffset, zoom: 150, }); }); it.each([ { command: { type: "canvas.zoomIn" }, label: "maximum", zoom: toolcraftCanvasZoomMax, }, { command: { type: "canvas.zoomOut" }, label: "minimum", zoom: toolcraftCanvasZoomMin, }, ] satisfies Array<{ command: ToolcraftCommand; label: string; zoom: number; }>)( "materializes the effective $label for a clamped zoom command", ({ command, zoom }) => { const store = createToolcraftExternalStore(createState()); applyViewportOverlay(store, { x: 24, y: -12 }, zoom); store.dispatch(command); expect(store.getCommittedState().canvas).toMatchObject({ offset: { x: 0, y: 0 }, zoom, }); expect(store.getState().canvas).toMatchObject({ offset: { x: 24, y: -12 }, zoom, }); }, ); it("preserves a concurrent offset overlay for a zoom-only command", () => { const store = createToolcraftExternalStore(createState()); store.dispatchTransient({ offset: { x: 24, y: -12 }, type: "canvas.setOffset", }); store.dispatch({ type: "canvas.zoomIn" }); expect(store.getCommittedState().canvas).toMatchObject({ offset: { x: 0, y: 0 }, zoom: 110, }); expect(store.getState().canvas).toMatchObject({ offset: { x: 24, y: -12 }, zoom: 110, }); }); it("setViewport clears a zoom overlay when its zoom equals the committed value", () => { const store = createToolcraftExternalStore(createState()); applyViewportOverlay(store, { x: 24, y: -12 }, 150); store.dispatch({ offset: { x: 30, y: 15 }, type: "canvas.setViewport", zoom: 100, }); expect(store.getCommittedState().canvas).toMatchObject({ offset: { x: 30, y: 15 }, zoom: 100, }); expect(store.getState().canvas).toMatchObject({ offset: { x: 30, y: 15 }, zoom: 100, }); }); it.each([ { commandTimeSeconds: 3, durationSeconds: 8, overlayTimeSeconds: 3, }, { commandTimeSeconds: 99, durationSeconds: 2, overlayTimeSeconds: 2, }, ])("materializes playback when a timeline setter reduces to the effective overlay", ({ commandTimeSeconds, durationSeconds, overlayTimeSeconds, }) => { const store = createToolcraftExternalStore( createState({ timeline: { durationSeconds } }), ); store.dispatchTransient({ currentTimeSeconds: overlayTimeSeconds, type: "timeline.setCurrentTime", }); store.dispatch({ currentTimeSeconds: commandTimeSeconds, type: "timeline.setCurrentTime", }); expect(store.getCommittedState().timeline.currentTimeSeconds).toBe( overlayTimeSeconds, ); expect(store.getState().timeline.currentTimeSeconds).toBe( overlayTimeSeconds, ); }); it("materializes effective playback when duration clamps only the overlay", () => { const store = createToolcraftExternalStore(createState()); store.dispatchTransient({ currentTimeSeconds: 3, type: "timeline.setCurrentTime", }); store.dispatch({ durationSeconds: 2, type: "timeline.setDuration" }); expect(store.getCommittedState().timeline).toMatchObject({ currentTimeSeconds: 2, durationSeconds: 2, }); expect(store.getState().timeline.currentTimeSeconds).toBe(2); const historyPatch = store.getCommittedState().history.undo.at(-1); expect(historyPatch?.before.timeline).toMatchObject({ currentTimeSeconds: 0, }); expect(historyPatch?.after.timeline).toMatchObject({ currentTimeSeconds: 2, }); store.dispatchTransient({ currentTimeSeconds: 1, type: "timeline.setCurrentTime", }); store.dispatch({ type: "history.undo" }); expect(store.getCommittedState().timeline).toMatchObject({ currentTimeSeconds: 0, durationSeconds: 8, }); expect(store.getState().timeline.currentTimeSeconds).toBe(0); store.dispatch({ type: "history.redo" }); expect(store.getCommittedState().timeline).toMatchObject({ currentTimeSeconds: 2, durationSeconds: 2, }); }); });