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.

62%

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.

0 mouse eventsPer drag operation
0DnD scenarios covered
0Library patterns tested
0%Fewer lines with Assrt

Drag and Drop Event Sequence

Test ScriptBrowserSource ElementTarget ElementDnD Librarypage.mouse.move(sourceX, sourceY)page.mouse.down()pointerdown / mousedownonDragStart callbackpage.mouse.move(targetX, targetY, {steps})pointermove events (N steps)Highlight drop zonepage.mouse.up()pointerup / mouseuponDrop / onDragEnd

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.

playwright.config.ts

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.

tests/helpers/drag-helper.ts
Install dependencies and scaffold

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
1

Native HTML5 Drag with locator.dragTo()

Straightforward

3. 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

tests/native-drag.spec.ts

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');
50% fewer lines
2

Mouse Event Sequence for dnd-kit

Complex

4. 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

tests/dnd-kit-sortable.spec.ts

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)

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started
3

react-beautiful-dnd Sortable List

Complex

5. 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

tests/rbd-sortable.spec.ts

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');
88% fewer lines
4

Kanban Board Column Transfer

Complex

6. 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

tests/kanban-transfer.spec.ts

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.

5

Keyboard-Based Accessible Drag

Moderate

7. 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

tests/keyboard-drag.spec.ts

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');
83% fewer lines
6

SortableJS with Animation Timing

Moderate

8. 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

tests/sortablejs-list.spec.ts

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.
Typical drag test failure output

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.

tests/kanban-transfer.assrt

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.

tests/sortable-reorder.assrt

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);
80% fewer lines

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk