import { describe, expect, test } from "bun:test"; import { commandIds } from "@commands/ids"; import type { ImageDocument } from "@core/document"; import type { PointerInputEvent } from "./pointer"; import { createTransformControlsInputController, hitTestArtboardTransformHandle, type TransformControlsEditorState } from "./transform-controls"; const ignoredState = undefined as never; const defaultViewport = { center: { x: 0, y: 0 }, zoom: 1, rotation: 0, size: { w: 200, h: 200 } }; const defaultTools = { activeTool: "select" as const, interactionMode: { type: "tool" as const, tool: "select" as const }, }; describe("transform controls input", () => { test("hit tests artboard handles and body", () => { const bounds = { x: -50, y: -50, w: 100, h: 100 }; expect(hitTestArtboardTransformHandle({ x: 50, y: 50 }, bounds, defaultViewport)).toBe("nw"); expect(hitTestArtboardTransformHandle({ x: 100, y: 100 }, bounds, defaultViewport)).toBe("body"); expect(hitTestArtboardTransformHandle({ x: 10, y: 10 }, bounds, defaultViewport)).toBeUndefined(); }); test("does not transform while temporary pan is active", () => { const state = createState({ selection: { artboardId: "a1", layerIds: [] }, tools: { activeTool: "select", interactionMode: { type: "temporary-pan", previousTool: "select" } }, }); const dispatched: unknown[] = []; const controller = createTransformControlsInputController({ getDocument: () => state.document, getEditor: () => state.editor, dispatch: (commandId, payload) => { dispatched.push({ commandId, payload }); return ignoredState; }, }); expect(controller.pointerDown(pointerEvent({ position: { x: 100, y: 100 }, buttons: 1 }))).toBe(false); expect(dispatched).toEqual([]); }); test("crop tool resizes from handles but does not move body", () => { const state = createState({ selection: { artboardId: "a1", layerIds: [] }, tools: { activeTool: "crop", interactionMode: { type: "tool", tool: "crop" } }, }); const dispatched: unknown[] = []; const controller = createTransformControlsInputController({ getDocument: () => state.document, getEditor: () => state.editor, dispatch: (commandId, payload) => { dispatched.push({ commandId, payload }); return ignoredState; }, }); expect(controller.pointerDown(pointerEvent({ position: { x: 100, y: 100 }, buttons: 1 }))).toBe(false); expect(controller.pointerDown(pointerEvent({ position: { x: 150, y: 150 }, buttons: 1 }))).toBe(true); }); test("dispatches transform lifecycle for selected artboard", () => { let state = createState({ selection: { artboardId: "a1", layerIds: [] } }); const dispatched: unknown[] = []; const controller = createTransformControlsInputController({ getDocument: () => state.document, getEditor: () => state.editor, dispatch: (commandId, payload) => { dispatched.push({ commandId, payload }); if (commandId === commandIds.transformBegin) state = { ...state, editor: { ...state.editor, transformSession: payload } }; return ignoredState; }, }); expect(controller.pointerDown(pointerEvent({ position: { x: 100, y: 100 }, buttons: 1 }))).toBe(true); expect(controller.pointerMove(pointerEvent({ position: { x: 110, y: 120 }, buttons: 1, shiftKey: true }))).toBe(true); expect(controller.pointerUp(pointerEvent({ position: { x: 110, y: 120 }, buttons: 0 }))).toBe(true); expect(dispatched.map((event) => (event as { commandId: string }).commandId)).toEqual([ commandIds.transformBegin, commandIds.transformUpdate, commandIds.transformEnd, ]); expect((dispatched[1] as { payload: { shiftKey: boolean } }).payload.shiftKey).toBe(true); }); }); function createState(editorOverrides: Partial = {}): { document: ImageDocument; editor: TransformControlsEditorState } { return { document: { id: "d1", name: "Test", version: 1, assets: [], artboards: [{ id: "a1", name: "Artboard", bounds: { x: -50, y: -50, w: 100, h: 100 }, backgroundColor: "transparent", visible: true, locked: false, layers: [] }], }, editor: { viewport: defaultViewport, selection: { layerIds: [] }, tools: defaultTools, ...editorOverrides, }, }; } 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, }; }