import { describe, expect, test } from "bun:test"; import type { Dispatch } from "@commands/dispatcher"; import { commandIds } from "@commands/ids"; import type { CommandPayloads } from "@commands/payloads"; import type { PointerInputEvent } from "./pointer"; import { createViewportPanInputController } from "./viewport-pan"; const ignoredState = undefined as never; describe("viewport pan store integration", () => { test("space key input updates actual tool mode", () => { const store = createInputStore(); const controller = createController(store); expect(controller.keyDown(keyEvent("Space"))).toBe(true); expect(store.state.tools.interactionMode).toEqual({ type: "temporary-pan", previousTool: "select" }); expect(controller.keyUp(keyEvent("Space"))).toBe(true); expect(store.state.tools.interactionMode).toEqual({ type: "tool", tool: "select" }); }); test("left-drag only pans while temporary pan is active", () => { const store = createInputStore(); const controller = createController(store); expect(controller.pointerDown(pointerEvent({ buttons: 1, position: { x: 0, y: 0 } }))).toBe(false); expect(controller.pointerMove(pointerEvent({ buttons: 1, position: { x: 10, y: 0 } }))).toBe(false); expect(store.state.viewport.center).toEqual({ x: 0, y: 0 }); store.dispatch(commandIds.toolEnterTemporaryPan, undefined); expect(controller.pointerDown(pointerEvent({ buttons: 1, position: { x: 0, y: 0 } }))).toBe(true); expect(controller.pointerMove(pointerEvent({ buttons: 1, position: { x: 10, y: 0 } }))).toBe(true); expect(store.state.viewport.center).toEqual({ x: -10, y: 0 }); }); test("left-drag pans while pan tool is active", () => { const store = createInputStore(); const controller = createController(store); store.dispatch(commandIds.toolSetActive, { tool: "pan" }); expect(controller.pointerDown(pointerEvent({ buttons: 1, position: { x: 0, y: 0 } }))).toBe(true); expect(controller.pointerMove(pointerEvent({ buttons: 1, position: { x: 4, y: -2 } }))).toBe(true); expect(store.state.viewport.center).toEqual({ x: -4, y: 2 }); }); }); type InputToolId = CommandPayloads[typeof commandIds.toolSetActive]["tool"]; type InputInteractionMode = | { type: "tool"; tool: InputToolId } | { type: "temporary-pan"; previousTool: InputToolId }; type InputStoreState = { viewport: { center: { x: number; y: number }; zoom: number }; tools: { activeTool: InputToolId; interactionMode: InputInteractionMode }; }; type InputStore = ReturnType; function createInputStore() { const state: InputStoreState = { viewport: { center: { x: 0, y: 0 }, zoom: 1 }, tools: { activeTool: "select", interactionMode: { type: "tool", tool: "select" }, }, }; const store = { state, dispatch: ((commandId, payload) => { switch (commandId) { case commandIds.toolSetActive: { const next = payload as CommandPayloads[typeof commandIds.toolSetActive]; store.state.tools.activeTool = next.tool; store.state.tools.interactionMode = { type: "tool", tool: next.tool }; break; } case commandIds.toolEnterTemporaryPan: store.state.tools.interactionMode = { type: "temporary-pan", previousTool: store.state.tools.activeTool }; break; case commandIds.toolExitTemporaryPan: { const mode = store.state.tools.interactionMode; if (mode.type === "temporary-pan") { store.state.tools.activeTool = mode.previousTool; store.state.tools.interactionMode = { type: "tool", tool: mode.previousTool }; } break; } case commandIds.viewportPan: { const next = payload as CommandPayloads[typeof commandIds.viewportPan]; store.state.viewport.center = { x: store.state.viewport.center.x + next.delta.x, y: store.state.viewport.center.y + next.delta.y, }; break; } } return ignoredState; }) as Dispatch, }; return store; } function createController(store: InputStore) { return createViewportPanInputController({ globalKeyConsumer: () => false, globalPointerConsumer: () => false, getCurrentZoom: () => store.state.viewport.zoom, isPanMode: () => { const mode = store.state.tools.interactionMode; return mode.type === "temporary-pan" || (mode.type === "tool" && mode.tool === "pan"); }, dispatch: store.dispatch, }); } function keyEvent(code: string) { return { key: code === "Space" ? " " : code, code, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false }; } function pointerEvent(overrides: Partial): PointerInputEvent { return { pointerId: 1, pointerType: "mouse", position: { x: 0, y: 0 }, buttons: 0, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...overrides, }; }