Testing
How to test pointer and keyboard drags.
The engine is synthetic: it listens to pointer and keyboard events and never uses the browser’s HTML5 drag-and-drop. There is no dragstart to fire. A test drives a drag the way a user does, with pointerdown, pointermove and pointerup.
Pick the environment first
A drag resolves drop targets through layout and document.elementFromPoint, which jsdom does not provide.
- Use a browser test for behavior that requires a resolved target, including
onDrop,data-drag-over, drop indicators, auto-scroll, and arrow-key movement. - Use jsdom for activation thresholds, lifecycle events, cancellation reasons, source state, preview rendering,
disabled, andonBeforeDragStartcancellation.
Stubbing getBoundingClientRect in jsdom supplies geometry but not hit-testing. A drag can still start and end there, but it resolves no target and ends as an 'outside-release'. Use a browser test when the destination matters.
Start a pointer drag
A press is not a drag. Mouse and pen activate once the pointer has moved 5px from where it went down; touch activates after a 250ms press-hold, with a 5px tolerance so a list scroll still scrolls. A test that dispatches pointerdown then pointerup has simulated a click.
Three details matter:
- Dispatch moves on
document, not onwindow. This reaches the drag’s active pointer listeners. - Carry the same
pointerIdthrough. The engine tracks one gesture by id and ignores events from another pointer. - Keep
buttons: 1on every move. A move reportingbuttons: 0means the button came up without the engine seeing it. Omit it on the move that would cross the threshold and the press is abandoned before it commits, so no drag starts and no handler fires at all; drop it mid-drag and the drag cancels with the reason'missed-release'.
For touch, advance timers past the press-hold instead of moving:
Set pointerActivation={{ type: 'immediate' }} on the draggable under test when the threshold is not what you are testing: the drag then starts on pointerdown.
Let a frame pass
onDrag and the drag-over state it drives are throttled to one call per animation frame, so an assertion made right after a move reads the state from before it. Await a frame in between:
onDragStart and onDragEnd are not throttled and fire synchronously with the event that caused them.
Keyboard drags
Starting a keyboard drag needs no geometry, so the lifecycle can be tested anywhere. Where the drag goes is still resolved from the targets’ rects, so assert on the destination only in a browser test.
Escape and Tab both cancel and end the drag with canceled: true. The default focus behavior then tries the handle, source, and innermost drop target. The second handler argument distinguishes the keys: eventDetails.reason is 'escape-key' or 'tab-key'.
Assert on what a user can observe
Assert on the data attributes and the handler arguments: data-dragging on the source, data-drag-over and data-drag-over-innermost on the target, data-drag-preview on the preview element, the source, location and canceled fields on the first argument, and reason on the second.
Handlers take two arguments, so toHaveBeenCalledWith does not match when only the payload is listed. Assert on mock.calls[0][0] or pass a matcher for the second argument.
Clean up between tests
A drag left running leaks into the next test: the engine is global, the cursor stays pinned, and <html> stays scroll-locked. End every drag a test starts, with a pointerup or an Escape, including on the failure path. For a test that asserts mid-drag, release in an afterEach.