UI Interaction Testing Guide
How to Test Drag and Drop with Playwright: Sortable Lists, Kanban Boards, and DnD Libraries
A scenario-by-scenario walkthrough of testing drag and drop interactions with Playwright. Native HTML5 drag, mouse event sequences for dnd-kit and react-beautiful-dnd, keyboard-based accessible drag, Kanban board column transfers, sortable list reordering, and the timing pitfalls that make drag tests flaky.
“According to the 2025 State of JS survey, 62% of web applications with list or board UIs use a JavaScript drag and drop library, making automated drag testing one of the most common yet under-covered E2E scenarios.”
Drag and Drop Event Sequence
1. Why Drag and Drop Testing Is Harder Than It Looks
A drag and drop interaction seems simple on the surface: pick up an element, move it somewhere else, release it. Under the hood, the browser fires a precise sequence of pointer events (pointerdown, pointermove, pointerup) that must arrive in the correct order, at the correct coordinates, with enough intermediate move steps to trigger the drag threshold. Most drag and drop libraries ignore the native HTML5 Drag and Drop API entirely because of its well-documented inconsistencies across browsers. Instead, they build custom drag engines on top of pointer events, which means locator.dragTo() often does not work with them.
The difficulty compounds across five dimensions. First, every DnD library has a different activation threshold: dnd-kit requires a minimum distance before a drag is recognized, react-beautiful-dnd listens for a specific pointer hold duration, and SortableJS uses its own delay/distance configuration. A test that skips directly from source to target coordinates without intermediate steps will never trigger the drag. Second, animations play a critical role. After dropping, most libraries animate the item into its final position over 200 to 500 milliseconds. If your assertion fires before the animation completes, the DOM order may not yet reflect the new state. Third, coordinate calculation is fragile. A scrolled container, a sticky header, or a CSS transform can all shift the actual pixel position of elements away from what element.boundingBox() reports.
Fourth, keyboard-based drag (for accessibility compliance) uses an entirely different code path in most libraries. dnd-kit activates keyboard drag on Space or Enter, then uses arrow keys for movement and Space again to drop. react-beautiful-dnd uses a similar but not identical pattern. If you only test mouse drag, you miss the keyboard implementation entirely. Fifth, Kanban boards introduce cross-container drag, where an item leaves one sortable list and enters another. The source container must remove the item, the target container must accept it, and the visual placeholder must appear in the correct insertion position, all while the pointer is in motion.
Why DnD Tests Break
Activation Threshold
Library ignores short moves
Coordinate Mismatch
Scroll offset, transforms
Animation Delay
DOM not settled at assertion
Library Differences
Each library, different API
Keyboard vs Mouse
Separate code paths
2. Setting Up a Reliable Drag and Drop Test Environment
Before writing any drag test, configure your Playwright project to handle the timing and viewport requirements that drag interactions demand. Drag tests are more sensitive to viewport size than typical E2E tests because coordinate calculations depend on how much of the page is visible and whether containers are scrolled.
Create a shared utility module for the mouse-based drag helper that you will reuse across all non-native drag scenarios. This helper encapsulates the three-phase mouse sequence (down, move with steps, up) and adds a configurable pause after the drop to let animations settle.
Pre-flight checklist before writing drag tests
- Fixed viewport configured in playwright.config.ts
- Trace recording enabled for debugging failed drags
- Shared drag helper utility created with configurable steps
- Identify which DnD library your app uses (check package.json)
- Disable CSS transitions in test mode if possible (speeds up assertions)
- Ensure test containers are not scrolled on initial load
Native HTML5 Drag with locator.dragTo()
Straightforward3. Scenario: Native HTML5 Drag with locator.dragTo()
Goal
Test a simple drag and drop interaction that uses the native HTML5 Drag and Drop API. This works for elements with draggable="true" and containers listening for dragover and drop events. Playwright provides a built-in locator.dragTo() method specifically designed for this API.
Preconditions
The application uses the native HTML5 draggable attribute and standard drag events. No third-party DnD library is involved. Elements use event.dataTransfer.setData() to pass data during the drag.
Playwright Implementation
What to Assert Beyond the UI
Check that dataTransferdata was received correctly by the target. Verify that the source container's item count decreased by one. If your app persists the new order to a backend, intercept the API call with page.route() and validate the request body contains the updated position.
Native HTML5 drag: Playwright vs Assrt
const sourceItem = page.locator('[data-testid="drag-item-1"]');
const dropZone = page.locator('[data-testid="drop-zone"]');
await sourceItem.dragTo(dropZone);
await expect(dropZone).toContainText('Item 1');
await expect(sourceContainer).not.toContainText('Item 1');Mouse Event Sequence for dnd-kit
Complex4. Scenario: Mouse Event Sequence for dnd-kit
Goal
Test a sortable list powered by dnd-kit, the most popular React DnD library with over 30,000 GitHub stars. dnd-kit does not use the native HTML5 Drag and Drop API. Instead, it listens for pointer events, so locator.dragTo() will not trigger a drag. You must simulate the full mouse event sequence manually.
Preconditions
The app uses @dnd-kit/core and @dnd-kit/sortable. The default PointerSensor is active with its default activation constraint of 8 pixels of distance before a drag is recognized. Items are rendered with a data-testid attribute for reliable selection.
Playwright Implementation
The steps: 20parameter is critical. dnd-kit's PointerSensor requires the pointer to travel at least 8 pixels before activating a drag. Playwright interpolates mouse positions across the given number of steps, so with 20 steps over a distance of, say, 200 pixels, each step moves 10 pixels. The first step alone exceeds the activation threshold. If you reduce steps to 1 or 2, the pointer jumps directly to the target without generating enough intermediate pointermove events, and dnd-kit never recognizes the gesture as a drag.
What to Assert Beyond the UI
Intercept the PATCH or PUT request that saves the new sort order and validate the payload contains the correct sequence of item IDs. If the app uses optimistic updates, verify the order immediately after the drop. Then simulate a page reload and confirm the order persists, proving the backend received the update.
dnd-kit Drag Activation Flow
mouse.down()
Start pointer tracking
mouse.move() x N
Exceed 8px threshold
Drag Activates
onDragStart fires
Placeholder Appears
Visual drop indicator
mouse.up()
onDragEnd fires
Animation Settles
DOM reorders (200ms)
react-beautiful-dnd Sortable List
Complex5. Scenario: react-beautiful-dnd Sortable List
Goal
Test a sortable list built with react-beautiful-dnd (rbd). Despite being in maintenance mode, rbd remains widely used in production. Its drag activation works differently from dnd-kit: rbd uses a "pre-drag" phase where the pointer must be held down for a brief moment before the drag activates. A quick mousedown/mouseup sequence is treated as a click, not a drag.
Preconditions
The app uses react-beautiful-dnd with its default mouse sensor. Items render with data-rbd-draggable-id attributes that rbd injects automatically. The list container has a data-rbd-droppable-id attribute.
Playwright Implementation
The key difference from dnd-kit is the pre-drag phase. After mouse.down(), rbd waits for either a small movement threshold or a time delay before committing to a drag. The initial 5-pixel move combined with the 150ms timeout ensures rbd transitions from the "pending" state into an active drag. Without this, rbd treats the interaction as a potential click and cancels the drag on the large mouse movement.
What to Assert Beyond the UI
Verify that data-rbd-draggable-id attributes remain correct after reordering. If the app fires a callback like onDragEnd that triggers a state update, inspect the resulting network request. Reload the page and confirm the order persisted.
react-beautiful-dnd reorder: Playwright vs Assrt
const sourceBox = await source.boundingBox();
const targetBox = await target.boundingBox();
const sx = sourceBox.x + sourceBox.width / 2;
const sy = sourceBox.y + sourceBox.height / 2;
const tx = targetBox.x + targetBox.width / 2;
const ty = targetBox.y + targetBox.height / 2;
await page.mouse.move(sx, sy);
await page.mouse.down();
await page.mouse.move(sx, sy + 5, { steps: 5 });
await page.waitForTimeout(150);
await page.mouse.move(tx, ty + 5, { steps: 20 });
await page.waitForTimeout(200);
await page.mouse.up();
await page.waitForTimeout(300);
await expect(items.nth(0)).toContainText('Review PR');
await expect(items.nth(2)).toContainText('Design mockups');Kanban Board Column Transfer
Complex6. Scenario: Kanban Board Column Transfer
Goal
Test dragging a card from one Kanban column to another. This is a cross-container drag: the card must leave the "To Do" column and land in the "In Progress" column at a specific position. Cross-container drag is harder than within-list reordering because the item must be removed from one sortable context and inserted into another, often with different scroll positions and container heights.
Preconditions
The Kanban board has three columns: "To Do", "In Progress", and "Done". Each column is a separate droppable container. The board uses dnd-kit with SortableContext per column and a DndContext wrapping all columns.
Playwright Implementation
Cross-container drag demands more mouse move steps (25 to 30) because the pointer must travel a longer horizontal distance. The settle time is also longer (400ms) because the source column collapses its empty space while the target column expands to accommodate the new card. Both animations must complete before your assertion checks the DOM. If you target an empty column, drag to the column container element rather than trying to find a card that does not exist yet.
What to Assert Beyond the UI
Validate the PATCH request body to confirm the card's status and position fields were updated. Verify both column card counts. If the board supports undo, trigger Ctrl+Z and assert the card returns to its original column.
Keyboard-Based Accessible Drag
Moderate7. Scenario: Keyboard-Based Accessible Drag
Goal
Test the keyboard-based drag interaction that accessibility standards require. Both dnd-kit and react-beautiful-dnd support keyboard drag out of the box: the user focuses a draggable item, presses Space to pick it up, uses arrow keys to move it, and presses Space again to drop it. This code path is separate from the mouse-based drag and must be tested independently.
Preconditions
The sortable list uses dnd-kit with the KeyboardSensor configured alongside the PointerSensor. Each draggable item has a drag handle with role="button" and tabIndex="0" for keyboard focus. An aria-live region announces drag state changes to screen readers.
Playwright Implementation
What to Assert Beyond the UI
Verify the aria-live announcements fire at each stage: pickup, movement, drop, and cancel. These announcements are what screen reader users depend on. Check that focus returns to the dropped item after the drag completes. If using rbd, verify that the data-rbd-drag-handle-draggable-id attribute is correctly set on the focused handle.
Keyboard drag: Playwright vs Assrt
const dragHandle = page.locator(
'[data-testid="sortable-item-a"] [data-testid="drag-handle"]'
);
await dragHandle.focus();
await page.keyboard.press('Space');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Space');
await page.waitForTimeout(300);
await expect(items.nth(0)).toContainText('Item B');
await expect(items.nth(2)).toContainText('Item A');SortableJS with Animation Timing
Moderate8. Scenario: SortableJS with Animation Timing
Goal
Test a sortable list powered by SortableJS, the framework-agnostic drag and drop library. SortableJS is commonly used with Vue (via vuedraggable), Angular, and vanilla JavaScript projects. Unlike dnd-kit, SortableJS has a configurable delay option and an animation property (in milliseconds) that controls how long the reorder animation plays after a drop. Your test must account for this animation duration.
Preconditions
SortableJS is configured with animation: 300 and delay: 0. The list is rendered as a <ul> with <li> children. Each item has a .sortable-item class and a data-id attribute.
Playwright Implementation
SortableJS has an important implementation detail that affects testing: it physically reorders DOM nodes rather than using CSS transforms. This means after the animation completes, querying .sortable-item:nth-child(N) returns the element in its new position. However, during the animation, the DOM is already reordered but the visual position has not caught up. If you screenshot during the animation, you will see items mid-transition. Always wait for the full animation duration plus a buffer before taking screenshots or running visual regression checks.
What to Assert Beyond the UI
Capture the onEnd event payload from SortableJS to verify oldIndex and newIndex values. If the app serializes the new order and sends it to an API, intercept that request. Verify the data-id sequence matches your expectation after the animation completes.
9. Common Pitfalls That Break Drag and Drop Tests
Drag and drop tests are among the flakiest in any E2E suite. Most failures trace back to one of these recurring issues, drawn from real bug reports on the Playwright, dnd-kit, and react-beautiful-dnd GitHub repositories.
Pitfalls that break drag tests (avoid these)
- Using locator.dragTo() with non-native DnD libraries. It dispatches HTML5 drag events, not pointer events. Libraries like dnd-kit and rbd ignore them entirely.
- Setting steps: 1 in mouse.move(). The pointer jumps from source to target without intermediate positions, so the library never recognizes the gesture as a drag.
- Asserting immediately after mouse.up() without waiting for animation. The DOM is mid-transition and nth-child queries return stale positions.
- Calculating coordinates from a scrolled container without accounting for scroll offset. Use element.boundingBox() which returns viewport-relative coordinates, not document-relative ones.
- Forgetting to test keyboard drag separately. Keyboard uses a completely different activation and movement mechanism. A mouse-only test gives false confidence.
- Running drag tests in parallel with shared state. If two tests modify the same list simultaneously, sort order assertions become non-deterministic.
- Hardcoding pixel coordinates instead of computing them from boundingBox(). A CSS change, new header, or different font rendering shifts everything.
- Not using a fixed viewport. Different viewport sizes cause different element positions, making coordinate-based drag tests fail across environments.
The most insidious failure mode is the "silent no-drag": your test runs without errors, the mouse events fire, but the library never activated the drag because the movement threshold was not met. The element stays in its original position, and your order assertion fails with a confusing message about the wrong text in the wrong position. The fix is always the same: increase the steps parameter to ensure enough intermediate pointermove events are generated.
Another common trap is testing with a mobile viewport. Touch-based drag uses different events (touchstart, touchmove, touchend) that require Playwright's page.touchscreen API instead of page.mouse. dnd-kit provides a separate TouchSensor for this, and rbd has its own touch sensor implementation. You cannot reuse mouse drag helpers for mobile viewport tests.
10. Writing Drag and Drop Tests in Plain English with Assrt
The Kanban board column transfer scenario from Section 6 required over 30 lines of Playwright code: computing bounding boxes, executing a three-phase mouse sequence with 25 steps, waiting for animations, and asserting card counts in both columns. With Assrt, the same test reads as a plain English description of what you want to happen. Assrt's AI-native engine handles the coordinate math, activation thresholds, animation waits, and library-specific event sequences automatically.
Assrt detects which DnD library your application uses and applies the correct drag strategy automatically. For dnd-kit, it uses pointer event sequences with appropriate step counts. For react-beautiful-dnd, it adds the pre-drag pause. For native HTML5 drag, it uses the built-in drag events. For keyboard drag, it uses the Space/Arrow/Space pattern. You never need to specify the mechanical details.
The plain English format also makes drag tests more maintainable. When you switch from react-beautiful-dnd to dnd-kit (a common migration since rbd entered maintenance mode), your Playwright tests break because the mouse event timing and activation logic are different. Your Assrt tests continue to pass without changes, because they describe the intent ("drag Item A below Item C") rather than the mechanism.
Kanban column transfer: Playwright vs Assrt
const card = page.locator('[data-testid="card-setup-ci"]');
const targetCard = inProgressColumn
.locator('[data-testid^="card-"]').first();
const sourceBox = await card.boundingBox();
const targetBox = await targetCard.boundingBox();
const sx = sourceBox.x + sourceBox.width / 2;
const sy = sourceBox.y + sourceBox.height / 2;
const tx = targetBox.x + targetBox.width / 2;
const ty = targetBox.y + targetBox.height / 2;
await page.mouse.move(sx, sy);
await page.mouse.down();
await page.mouse.move(tx, ty, { steps: 25 });
await page.mouse.up();
await page.waitForTimeout(400);
await expect(inProgressColumn).toContainText('Setup CI');
await expect(todoColumn).not.toContainText('Setup CI');
await expect(todoCards).toHaveCount(2);
await expect(inProgressCards).toHaveCount(3);Related Guides
How to Test Airtable Form Embed
Step-by-step guide to testing embedded Airtable forms with Playwright. Covers iframe...
How to Test Copy Button
Step-by-step guide to testing code block copy buttons with Playwright. Clipboard API...
How to Test Combobox Multiselect
A practical guide to testing combobox multiselect components with Playwright. Covers...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.