feat: add shared runtime input device layer
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
161
src/services/input-devices/runtimeDragInputController.test.ts
Normal file
161
src/services/input-devices/runtimeDragInputController.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user