import { describe, expect, test, vi } from 'vitest'; import { createRuntimeDragInputController, createRuntimeInputPointFromNormalized, resolveRuntimeInputGridCell, } from './index'; describe('runtime drag input controller', () => { test('pointer-like short press remains a tap', () => { const onTap = vi.fn(); const onDrop = vi.fn(); const controller = createRuntimeDragInputController({ dragThresholdPx: 12, onTap, onDrop, }); controller.press({ targetId: 'piece-1', inputId: 'pointer:1', deviceKind: 'pointer', point: { clientX: 10, clientY: 10 }, }); controller.move({ inputId: 'pointer:1', point: { clientX: 14, clientY: 14 }, }); controller.release({ inputId: 'pointer:1', point: { clientX: 14, clientY: 14 }, }); expect(onTap).toHaveBeenCalledTimes(1); expect(onTap).toHaveBeenCalledWith( expect.objectContaining({ targetId: 'piece-1', dragging: false }), ); expect(onDrop).not.toHaveBeenCalled(); }); test('device adapters can force continuous drag semantics', () => { const onDragStart = vi.fn(); const onDragMove = vi.fn(); const onDrop = vi.fn(); const controller = createRuntimeDragInputController({ dragThresholdPx: 100, onDragStart, onDragMove, onDrop, }); controller.press({ targetId: 'piece-1', inputId: 'mocap:hand', deviceKind: 'mocap', point: { clientX: 10, clientY: 10 }, }); controller.move({ inputId: 'mocap:hand', point: { clientX: 11, clientY: 11 }, forceDragging: true, }); controller.release({ inputId: 'mocap:hand', point: { clientX: 12, clientY: 12 }, }); expect(onDragStart).toHaveBeenCalledTimes(1); expect(onDragMove).toHaveBeenCalledTimes(1); expect(onDrop).toHaveBeenCalledTimes(1); expect(onDrop).toHaveBeenCalledWith( expect.objectContaining({ deviceKind: 'mocap', dragging: true }), ); }); test('device adapters can force drop on release without converting to tap', () => { const onTap = vi.fn(); const onDrop = vi.fn(); const controller = createRuntimeDragInputController({ dragThresholdPx: 100, onTap, onDrop, }); controller.press({ targetId: 'piece-1', inputId: 'mocap:hand', deviceKind: 'mocap', point: { clientX: 10, clientY: 10 }, }); controller.release({ inputId: 'mocap:hand', point: { clientX: 10, clientY: 10 }, forceDrop: true, }); expect(onDrop).toHaveBeenCalledTimes(1); expect(onDrop).toHaveBeenCalledWith( expect.objectContaining({ targetId: 'piece-1', forceDrop: true, dragging: false, }), ); expect(onTap).not.toHaveBeenCalled(); }); test('input-scoped cancel keeps unrelated active sessions alive', () => { const onCancel = vi.fn(); const onDrop = vi.fn(); const controller = createRuntimeDragInputController({ dragThresholdPx: 1, onCancel, onDrop, }); controller.press({ targetId: 'piece-1', inputId: 'pointer:1', deviceKind: 'pointer', point: { clientX: 10, clientY: 10 }, }); controller.cancel('mocap:hand'); controller.move({ inputId: 'pointer:1', point: { clientX: 20, clientY: 20 }, }); controller.release({ inputId: 'pointer:1', point: { clientX: 20, clientY: 20 }, }); expect(onCancel).not.toHaveBeenCalled(); expect(onDrop).toHaveBeenCalledTimes(1); expect(onDrop).toHaveBeenCalledWith( expect.objectContaining({ inputId: 'pointer:1', targetId: 'piece-1' }), ); }); }); describe('runtime input geometry', () => { test('normalised device coordinates map into client coordinates and grid cells', () => { const point = createRuntimeInputPointFromNormalized(0.75, 0.25, { left: 20, top: 10, width: 200, height: 100, }); expect(point).toEqual({ clientX: 170, clientY: 35, normalizedX: 0.75, normalizedY: 0.25, }); expect(resolveRuntimeInputGridCell(point, { rows: 4, cols: 4 })).toEqual({ row: 1, col: 3, }); }); });