Drag-and-drop reordering looks simple on the screen and turns into a reliability tax in the test suite. A user grabs a card, moves it somewhere else, and drops it. The app updates the list order. Done. In practice, the interaction can involve pointer capture, temporary ghost elements, CSS transforms, virtualization, auto-scroll, delayed layout updates, and libraries that simulate native HTML5 drag-and-drop in slightly different ways across browsers.

If you have ever tried to test drag-and-drop reordering and ended up with tests that pass locally but fail in CI, this article is for you. The goal here is not to show one magic locator or one perfect helper. The goal is to build a mental model for the failure modes, then use that model to design tests that are stable, meaningful, and maintainable.

What makes drag-and-drop reordering hard to test

A reorderable list is not just a click sequence. The UI often has to track an item while the pointer is held down, compute the intended drop target, animate placeholders, and update the DOM without confusing the browser or the test runner. That gives you several classes of bugs:

  • Pointer events are intercepted by overlays, handles, or floating preview layers.
  • The dragged item is replaced by a ghost or clone that changes the DOM mid-action.
  • The page auto-scrolls when you drag near the viewport edge.
  • The list re-renders during the drag, which invalidates element handles.
  • Animation timing introduces race conditions between mouse movement and assertion.
  • Browser-specific drag behavior differs between HTML5 drag events and pointer-driven custom libraries.

The hardest part is often not the drag itself, it is proving that the app has reached a stable post-reorder state after the UI finishes animating and re-rendering.

For QA teams, this is where tests become noisy. A test can be logically correct and still fail because it clicked one pixel off the handle, grabbed the ghost element instead of the real card, or asserted the order before the framework finished applying the state update.

First, identify which drag model your app uses

Before writing tests, confirm what the app actually implements. The test strategy depends on the drag model.

1. HTML5 drag and drop

Some apps use native drag events such as dragstart, dragover, and drop. These are common in older implementations and in some libraries. In browser automation, HTML5 DnD can be awkward because the default browser behavior is not the same as a user moving a pointer across the page.

2. Pointer events or mouse events

Modern components often use pointer or mouse events, such as pointerdown, pointermove, pointerup, or mousedown plus mousemove. These are common in board-style UIs, kanban columns, and mobile-friendly widgets. They can be more realistic in automation because you can drive the interaction with actual pointer movement.

3. Library abstraction

Many apps use libraries like SortableJS, dnd-kit, react-beautiful-dnd, or custom abstractions. The library decides whether the dragged item is cloned, hidden, transformed, or reparented. That implementation detail affects your locators and your assertions.

If you can inspect the event listeners or source code, do it. If not, use browser devtools to watch what happens to the DOM when drag starts. The difference between moving the original node, rendering a clone, or applying transforms is the difference between a stable test and a flaky one.

Test the outcome first, not the gesture

A common mistake is to make the test too obsessed with the drag motion. In most cases, you do not need to verify every intermediate pointer coordinate. The business value is usually the final order, plus maybe one or two important side effects.

Focus on outcomes such as:

  • the item appears in the expected position
  • the list order is persisted after refresh
  • the server receives the correct reorder payload
  • the UI reflects the saved order after a rerender
  • the moved item still has the expected label, status, or metadata

If you are testing a kanban board, you might care that a card moved from “To Do” to “In Progress” and that the status chip changed. You usually do not need to prove the exact pointer path, unless you are debugging a drag regression.

This distinction matters because gesture-heavy assertions create brittle tests. Outcome-based assertions survive refactors better.

Build stable locators for reorderable items

Drag-and-drop tests fail often because the locator strategy is too generic. Lists change order, elements duplicate during animation, and some components render multiple copies of the same card.

Use stable attributes when possible:

  • data-testid or data-qa
  • item IDs from backend data
  • accessible labels on drag handles
  • role-based locators when the UI supports them

Example in Playwright:

typescript

const card = page.getByTestId('task-card-42');
const handle = card.getByRole('button', { name: 'Drag task' });

If your app renders a ghost element that mirrors the original content, a text locator like page.getByText('Write docs') may match the wrong node. Prefer a scoped locator anchored to the real item container.

For reorderable tables or lists, structure matters:

<div data-testid="todo-item-42">
  <button aria-label="Drag" />
  <span>Write docs</span>
</div>

That kind of markup makes pointer events testing much less painful. It also helps assistive tech, which is a nice side benefit if your drag handle is keyboard-operable.

Prefer pointer-based dragging when the app is pointer-driven

If the app is driven by pointer or mouse events, use a real pointer sequence rather than trying to fake DOM drag events. In Playwright, that often means mouse.down, mouse.move, mouse.up with a measured path.

typescript

const source = page.getByTestId('todo-item-42');
const target = page.getByTestId('todo-item-99');

const sourceBox = await source.boundingBox();

const targetBox = await target.boundingBox();

if (!sourceBox || !targetBox) throw new Error(‘Missing bounding box’);

await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2);
await page.mouse.down();
await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { steps: 10 });
await page.mouse.up();

This is not perfect, but it models user behavior more closely than directly dispatching a drop event. It also surfaces pointer interception bugs that pure DOM event synthesis can hide.

That said, pointer-based dragging has its own issues:

  • the item may animate away during the drag
  • the target may move as the placeholder appears
  • the browser may scroll unexpectedly
  • the handle area may be too small for a stable movement start

If the test starts becoming coordinate arithmetic with lots of sleeps, step back and ask whether your UI can expose a better test hook.

Handle ghost elements explicitly

Ghost element bugs are a classic source of confusion. Some drag libraries render a floating copy of the item under the pointer, while the original item becomes hidden, semi-transparent, or moved into a placeholder state. In a test, that can lead to several surprises:

  • the text appears twice during the drag
  • the source element gets detached from the DOM
  • the same locator matches both the original and the clone
  • a screenshot captures the clone instead of the final destination state

The practical fix is to know what to wait for after the drop. A successful drag is not just the pointer release, it is the disappearance of the transient state.

Useful checks include:

  • wait for the ghost or preview element to disappear
  • wait for the placeholder to be removed
  • wait for the source and destination lists to settle
  • assert that the order in the DOM matches the visual order

Example:

typescript

await page.locator('[data-testid="drag-preview"]').waitFor({ state: 'detached' });
await expect(page.getByTestId('todo-list')).toContainText(['Write docs', 'Review PR', 'Ship release']);

If you do not have a dedicated test ID for the ghost, consider adding one. It is much easier than debugging an intermittent test that guesses when the animation is done.

Guard against scroll jank in E2E tests

Scroll jank in E2E tests shows up when the viewport moves during a drag, especially in tall lists or board columns. The test runner may think the target is visible, then the page auto-scrolls, and suddenly the target is somewhere else. This is especially common when dragging near the top or bottom edge of a scrollable container.

Common causes include:

  • auto-scroll triggered by proximity to the container edge
  • nested scroll containers competing for scroll control
  • layout shifts caused by placeholder insertion
  • sticky headers changing the available viewport
  • CSS transforms that affect visual position but not DOM position

To reduce this, test the list in a consistent viewport size and control the scroll context. If the component scrolls inside a container, bring the relevant section into view before starting the drag.

typescript

const board = page.getByTestId('board-column-in-progress');
await board.scrollIntoViewIfNeeded();

If your app uses virtualization, the item you want might not even exist in the DOM until you scroll. In that case, your test should explicitly scroll to the item or use the app’s filtering/search to reveal it before dragging.

Another practical trick is to separate the drag action from the scroll assertions. Verify that the item moved first, then separately verify that the viewport did not jump to an unexpected position if that behavior matters to the user.

Wait for the right thing

Waiting for arbitrary timeouts is one of the fastest ways to create flaky reorder tests. The right wait is usually tied to a state transition:

  • the API response that confirms the new order
  • the DOM attribute that reflects persistence
  • the disappearance of a placeholder
  • the final order rendered in the list
  • a status chip or toast that indicates success

For example, if the application sends a reorder request, use a network wait.

typescript

const reorder = page.waitForResponse(response =>
  response.url().includes('/api/reorder') && response.request().method() === 'POST'
);

await dragCard(page, ‘task-42’, ‘task-99’); await reorder;

Then assert the UI state after the request resolves. That makes your test much less dependent on animation timing.

If the app updates optimistically and then reconciles with the server, you may need two assertions, one for the immediate optimistic order and one after reload or refresh.

Verify persistence, not just the visual reorder

A reorder test that only checks the on-screen order can pass even when the application forgets to save the change. That is a partial test, not a complete one.

A stronger flow is:

  1. drag the item
  2. confirm the visible order changes
  3. wait for the save action or success indicator
  4. refresh the page or navigate away and back
  5. confirm the order is still correct

This catches bugs in state management, API integration, and serialization.

If you have access to the backend, you can also assert the request payload. In many apps, the correct reorder is represented by a sequence, an index change, or a moved item ID. Check whatever the backend contract actually uses. If the contract is versioned or normalized, verify against that schema rather than guessing based on the UI.

Test edge cases, not just the happy path

Drag-and-drop features usually break around the edges, literally and figuratively.

Reordering to the top or bottom

The first and last positions can behave differently because the drop target area is small or because the placeholder logic changes.

Dragging across columns

Board-style interactions often update both the source list and the destination list. That can expose stale state in either container.

Dragging a filtered item

If a list can be filtered, the item order in the filtered view may differ from the full list. Make sure the app’s rules are clear.

Rapid repeated reorders

Users sometimes drag an item twice before the first save completes. Tests should check whether the app debounces or queues updates correctly.

Disabled or locked items

Some rows cannot be moved. The test should confirm that the handle is disabled, and that the app does not reorder when the user tries anyway.

Good drag-and-drop testing is less about proving the mouse can move and more about proving the app preserves business rules under awkward motion.

Make the UI easier to test

The best drag-and-drop tests often start with small product decisions.

Add a dedicated drag handle instead of making the whole card draggable. That gives automation a single reliable element to interact with. Expose accessible names. Keep test IDs stable. Avoid hidden overlays that block pointer input. If you use animations, keep them short and deterministic, or disable them in test mode.

A few patterns help a lot:

  • separate the drag handle from the clickable card content
  • add data-testid to cards, handles, placeholders, and ghost elements
  • expose order in the DOM, not only in rendered pixels
  • minimize layout shifts during drag
  • use a test mode that turns off long transitions

For teams that own the frontend, these are cheap changes compared to spending months chasing flaky reorder tests.

A small helper can make tests much clearer

When several suites need the same drag logic, wrap the drag in a helper rather than duplicating coordinates everywhere.

typescript

async function dragItem(page, sourceTestId, targetTestId) {
  const source = page.getByTestId(sourceTestId);
  const target = page.getByTestId(targetTestId);
  const sourceBox = await source.boundingBox();
  const targetBox = await target.boundingBox();
  if (!sourceBox || !targetBox) throw new Error('Could not locate drag targets');

await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2); await page.mouse.down(); await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { steps: 12 }); await page.mouse.up(); }

This is not just convenience. It centralizes the tricky bits, which means when the component changes you only update one place. If you are using a code-first framework, that kind of helper is often the difference between a test suite people trust and a suite people mute.

Where Endtest can fit

If your team is spending too much time maintaining brittle gesture helpers, Endtest can be a practical alternative for browser-based reorder flows. It uses agentic AI and low-code workflows to build editable, platform-native test steps, which can be useful when the hardest part is keeping the suite stable across UI changes rather than writing custom drag code.

That is not a replacement for understanding the interaction model, but it can be a good option when a browser automation workflow needs to stay maintainable without a lot of framework plumbing.

A pragmatic checklist for drag-and-drop E2E coverage

Before calling the feature done, sanity-check these items:

  • Can you locate the draggable item and the handle reliably?
  • Does the test use the right drag model for the app?
  • Does it wait for ghost elements or placeholders to disappear?
  • Does it verify final order after save or refresh?
  • Does it avoid fixed sleeps?
  • Does it work at the viewport size used in CI?
  • Does it cover top, middle, bottom, and cross-container moves?
  • Does it fail for disabled items the way the product should?
  • Can someone on the team understand the helper without opening the browser devtools every time?

If the answer to any of those is no, the test is probably not stable enough yet.

Final thoughts

To test drag-and-drop reordering well, treat it as a state transition problem, not a pointer choreography problem. The pointer is just the trigger. The real test is whether the app updates order correctly, survives transient ghost elements, handles scrolling without jank, and persists the new state after the UI settles.

That mindset usually leads to fewer flaky tests and better product coverage. It also forces the team to make the component more testable, which pays off every time the list implementation changes.

If you only remember one thing, make it this: the best drag-and-drop test is the one that checks the business outcome after the interface finishes doing its weird little animation dance.