How to Test Drag-and-Drop and Sortable Lists
Short answer
Drag-and-drop UIs fail when library-specific handlers ignore synthetic events, order is not persisted, or permissions differ by column—not when pixels move. Prefer probe Assert on persisted order after dragTo or library test hooks; fall back to Playwright ai.act for complex DnD; never rely on DOM order alone without save.
Part of Testing Guides by UI patterns.
Who this is for
Teams shipping Kanban boards, sortable tables, file upload dropzones, tree reordering, or dashboard tile layouts. Typical stacks: @dnd-kit, react-beautiful-dnd, SortableJS, HTML5 DnD, custom pointer handlers.
Why testing drag-and-drop matters
Order and placement drive workflow and authorization:
- Wrong workflow state — ticket moves to "Done" without review; SLA reports skew.
- Permission bypass — user drags item into admin-only column; UI animates but server rejects—or worse, accepts.
- Lost ordering — refresh reverts sort; users reorder daily and lose trust.
- Mobile gaps — touch long-press paths untested while desktop dragTo passes.
- Accessibility — keyboard reorder buttons missing; only pointer paths exist in prod.
Assert persisted order via probe after explicit save or debounced persist—not intermediate animation frames.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| HTML5 DnD | dragTo insufficient | No drop event | Library-specific dataTransfer |
| dnd-kit | Pointer sensor only | Headless miss | page.mouse sequence or test hook |
| Cross-column | Different droppable ids | Drop rejected silently | Probe column_id on item |
| Sortable table | Virtual rows | Source not in DOM | Scroll into view first |
| Dropzone upload | Overlay intercepts | Click hits wrong layer | setInputFiles bypass for upload |
| Cancel drag | Escape mid-drag | Partial state | Probe unchanged order |
| Optimistic UI | Reverts on 409 | Test passes then fails | Poll probe after save |
| Nested lists | Parent vs child drop | Wrong container | Probe parent_id |
| Touch | Long press delay | Desktop-only spec | Touchscreen project or API reorder |
| Permissions | Read-only column | UI allows drag | Probe 403 on persist |
| Multi-select drag | Batch move | Partial move | Probe all ids moved |
| Auto-scroll | Drag near edge | Flaky coordinates | Fixed container height in test |
Playwright dragTo baseline
const source = page.getByTestId('card-101');
const target = page.getByTestId('column-done');
await source.scrollIntoViewIfNeeded();
await source.dragTo(target);
await page.getByRole('button', { name: 'Save board' }).click();
const order = await request.get(`/api/test/board-order?runId=${runId}`).then(r => r.json());
expect(order.column.done).toEqual(['101', '102', '103']);
Always probe order—DOM order before save is not proof.
Library-specific: dnd-kit / pointer sensors
When dragTo fails, use stepped mouse events:
const box = await source.boundingBox();
const targetBox = await target.boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(targetBox.x + 10, targetBox.y + 10, { steps: 10 });
await page.mouse.up();
Some teams expose test-only reorder API in Arrange/Act:
await request.post('/api/test/reorder', { data: { runId, itemId: '101', column: 'done', index: 0 } });
await page.reload();
// Assert UI reflects probe order
Dropzone file upload
For file dropzones, setInputFiles on hidden input is more reliable than simulating file drag:
await page.setInputFiles('input[type=file]', 'fixtures/report.pdf');
const probe = await request.get(`/api/test/uploads?runId=${runId}`);
expect((await probe.json()).files).toContainEqual(expect.objectContaining({ name: 'report.pdf' }));
See file uploads guide for MIME and size validation overlap.
Permission negative path
await source.dragTo(page.getByTestId('column-admin-only'));
await expect(page.getByRole('alert')).toContainText(/permission/i);
const probe = await request.get(`/api/test/item/101`);
expect((await probe.json()).column).toBe('in-progress'); // unchanged
CI checklist
- Seed list with stable string ids (not array index)
- Scroll virtualized sources into view before drag
- Probe persisted order after save or debounced PATCH
- One negative permission spec per restricted column
- Touch project or API reorder fallback for mobile-critical apps
- Avoid coordinate-only tests without probe
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Assert DOM order only | Optimistic UI lies | Probe after save |
| Single dragTo attempt | Library ignores event | Mouse steps or test hook |
| Pixel-perfect screenshots | Theme changes flake | Order probe |
| Skip permission columns | Security bug | Negative probe |
| Drag without scroll in virtual list | Source not found | scrollIntoViewIfNeeded |
| Test every permutation | n! explosion | Critical paths + probe |
Example scenario
Situation: Recruiter drags candidate card from Interview to Hired column.
Expected outcome: Card persists in Hired; pipeline stage updated; audit log entry created.
Why UI-only automation breaks: Card animates to Hired but probe still shows stage=interview—reports wrong.
- Arrange: Seed candidate id=c-55 at stage interview for runId.
- Act: dragTo Hired column; confirm dialog if present.
- Assert: Probe stage=hired; board-order includes c-55 in hired; optional audit probe for stage_change.
TestChimp workflow: Track kanban_column transitions in prod; expand tests when new columns ship via /testchimp evolve.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Data grids — row reorder and bulk move
- Canvas visual AI — spatial drag on canvases
- File uploads — dropzone overlap
- Audit compliance — stage change trails
External references
Frequently asked questions
Playwright dragTo vs ai.act for drag-and-drop?
Try locator.dragTo(target) first with scrollIntoViewIfNeeded. For complex libraries, use stepped mouse events, library test hooks, or ai.act("move card X to Done column") then probe persisted order.
How do I assert order without flaky DOM checks?
After save or debounced PATCH, GET a test probe route returning ordered ids per column. DOM order before network completes is unreliable.
How do I test drag into permission-restricted columns?
Attempt drag, assert UI error if shown, then probe item unchanged in source column. Never trust animation alone.
File dropzone vs sortable list?
Dropzones often accept setInputFiles on hidden input reliably. Sortable lists need drag simulation or test reorder API.
How do virtualized lists affect drag tests?
Scroll source row into view before drag; destination column may also need scroll. Probe order regardless of visible DOM rows.
Should mobile touch drag be a separate project?
If mobile is primary, enable Playwright touch and long-press paths—or cover mobile via API reorder plus one manual ExploreChimp session converted to SmartTest.
How does TestChimp help Kanban maintenance?
Record complex board flows in manual session, convert to SmartTests with stable data-id locators, link // @Scenario: to pipeline requirements in markdown plans.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.