Resource library

QA How-To

Playwright drag and drop: Examples and Best Practices

Explore Playwright drag and drop examples for kanban boards, sortable lists, handles, canvas, files, DataTransfer, iframes, and CI stability in modern suites.

26 min read | 3,767 words

TL;DR

Choose dragTo for visible elements, page.mouse for coordinate-driven canvases, and Locator.drop for external files or MIME data. Every example should prove the exact new order or status and confirm the old state is gone.

Key Takeaways

  • Match the automation method to the component's pointer, HTML5, canvas, or external-drop contract.
  • Assert preconditions so a no-op cannot satisfy a postcondition that was already true.
  • Verify target arrival, source removal, exact order, and persistence according to risk.
  • Use relative target positions for sortable insertion and live bounding boxes for canvas movement.
  • Use Locator.drop for file and MIME payloads, including validation and sanitization cases.
  • Test disabled UI and backend authorization at the correct layers.
  • Stabilize CI with unique data, explicit viewports, web-first assertions, and traces.

Playwright drag and drop examples should match the interaction technology and verify more than a pointer movement. A sortable list, kanban board, canvas, file drop zone, and custom DataTransfer widget need different test mechanics. The shared standard is simple: make the source and target unambiguous, perform the closest user-level action available, and assert a durable domain result.

This pattern catalog uses current 2026 Playwright Test APIs, including Locator.dragTo() with steps and relative positions, page.mouse for precise geometry, and Locator.drop() for external files or MIME data. The examples are independent so teams can adopt only the patterns their product needs.

A drag test is valuable when it can explain a failure. Assertions for target membership, old-location removal, exact order, status feedback, network persistence, and reload state give far better evidence than a screenshot after mouseup.

TL;DR

UI pattern Playwright pattern Main assertion
Kanban card card.dragTo(column, { steps }) New status and no duplicate
Sortable list item.dragTo(item, { targetPosition }) Exact list order
Handle-only card handle.dragTo(target) Parent item moved
Canvas object page.mouse sequence Model value or coordinates
File drop zone zone.drop({ files }) Parsed file and upload state
URL drop zone zone.drop({ data }) Created link and href
Custom HTML5 payload shared DataTransfer dispatch Payload-specific outcome
Forbidden move dragTo plus rejection assertion State remains unchanged

1. A Baseline for Playwright Drag and Drop Examples

Use deterministic test data and a clear assertion chain. The baseline below moves one card and proves its source, destination, status, and identity.

import { test, expect } from '@playwright/test';

test('moves TASK-42 from Todo to Done', async ({ page }) => {
  await page.goto('/boards/release');

  const todo = page.getByTestId('column-todo');
  const done = page.getByTestId('column-done');
  const task = todo.getByTestId('task-TASK-42');

  await expect(task).toContainText('Publish release notes');
  await expect(done.getByTestId('task-TASK-42')).toHaveCount(0);

  await task.dragTo(done, { steps: 8 });

  await expect(done.getByTestId('task-TASK-42')).toBeVisible();
  await expect(todo.getByTestId('task-TASK-42')).toHaveCount(0);
  await expect(page.getByRole('status'))
    .toHaveText('Publish release notes moved to Done');
});

Use the item ID for stable identity and visible text for user meaning. This combination catches a defect that moves the wrong record while rendering the expected label somewhere else.

The precondition that the target does not already contain TASK-42 is important. Without it, the postcondition could pass even if the drag did nothing. Deterministic setup should guarantee that state, and the assertion makes the assumption visible.

Do not take a screenshot and use it as the only oracle. Screenshots are useful evidence, but DOM state and backend persistence are easier to diagnose and less sensitive to rendering noise. Visual comparison can supplement state assertions when placement itself is contractual.

If your locators frequently resolve to multiple elements, fix the locator model using Playwright strict mode violation guidance before tuning the drag.

2. Example: Move a Kanban Card Between Columns

A realistic board often saves through an API and announces the result. Arm the response wait before the gesture, match the exact record, then verify persistence after a reload.

import { test, expect } from '@playwright/test';

test('persists a kanban status change', async ({ page }) => {
  await page.goto('/boards/release');

  const card = page.getByTestId('task-TASK-42');
  const review = page.getByTestId('column-review');

  const update = page.waitForResponse(response =>
    response.url().endsWith('/api/tasks/TASK-42') &&
    response.request().method() === 'PATCH',
  );

  await card.dragTo(review, {
    steps: 10,
    targetPosition: { x: 40, y: 80 },
  });

  const response = await update;
  expect(response.ok()).toBe(true);

  await expect(review.getByTestId('task-TASK-42')).toBeVisible();
  await expect(page.getByRole('status')).toHaveText('Task saved');

  await page.reload();
  await expect(review.getByTestId('task-TASK-42')).toBeVisible();
});

The targetPosition selects a stable area inside the column, not a screen coordinate. If the column inserts based on vertical position, choose a region that corresponds to the intended order and document that meaning.

Avoid matching every PATCH response. Background preference updates or analytics calls could satisfy a broad predicate. Include the record endpoint and method, and inspect the body if the same endpoint serves different operations:

const requestBody = response.request().postDataJSON();
expect(requestBody).toEqual(
  expect.objectContaining({ status: 'review' }),
);

Reload only in the persistence-focused test. Faster interaction tests can assert the UI and status without repeating the backend check for every permutation.

To understand dragTo options and API selection before applying this pattern, read how to use Playwright drag and drop.

3. Example: Reorder a Sortable List at an Exact Position

Sortable lists often infer insertion from the pointer's vertical location within another item. Dropping near the top means insert before, while dropping near the bottom means insert after. Use a relative targetPosition and assert the complete order.

import { test, expect } from '@playwright/test';

test('moves Security before Performance', async ({ page }) => {
  await page.goto('/test-plan/sections');

  const list = page.getByTestId('plan-sections');
  const security = list.getByRole('listitem', { name: 'Security' });
  const performance = list.getByRole('listitem', { name: 'Performance' });

  const targetBox = await performance.boundingBox();
  if (!targetBox) {
    throw new Error('Performance row has no bounding box');
  }

  await security.dragTo(performance, {
    targetPosition: {
      x: targetBox.width / 2,
      y: 2,
    },
    steps: 12,
  });

  await expect(list.getByRole('listitem')).toHaveText([
    'Functional',
    'Security',
    'Performance',
    'Accessibility',
  ]);
});

targetPosition is relative to the target's padding box, so only width is needed to select its horizontal center. y: 2 intentionally chooses the upper edge. If the component has padding or a dedicated insertion marker, choose coordinates according to that behavior.

The entire ordered array is a strong oracle. Merely checking that Security is visible does not prove its index. For very long lists, assert a small neighborhood around the moved item and a stable order model rather than rendering thousands of rows.

When virtualization is present, the destination may not be in the DOM until scrolled. Scroll the target into view through a search, list control, or locator.scrollIntoViewIfNeeded before dragging. Avoid locator.all on a dynamic virtual list because it returns the currently matched elements immediately and can produce incomplete results.

4. Example: Drag From a Dedicated Handle

Many accessible sortable components reserve dragging for a handle. The card itself supports selection, links, or text interaction, while the handle starts movement. Locate the handle within its parent and drag that element.

import { test, expect } from '@playwright/test';

test('uses the drag handle without opening the card', async ({ page }) => {
  await page.goto('/backlog');

  const card = page.getByTestId('story-STORY-7');
  const handle = card.getByRole('button', { name: 'Drag STORY-7' });
  const sprint = page.getByTestId('sprint-current');

  await handle.dragTo(sprint, { steps: 8 });

  await expect(sprint.getByTestId('story-STORY-7')).toBeVisible();
  await expect(page).toHaveURL('/backlog');
  await expect(page.getByRole('dialog')).toHaveCount(0);
});

The URL and dialog assertions prove the card's normal click behavior was not accidentally triggered. This matters when an automation attempt drags the wrong element and releases it as a click.

A handle implemented as a button should also have an accessible name. Testing by role makes an unlabeled handle fail early, which provides useful accessibility feedback. If the component uses a non-interactive span with pointer listeners, a test ID may be necessary, but the product should still provide a keyboard movement path.

For reliable accessible locator patterns, see Playwright getByRole examples.

Do not use force simply because the handle is small. dragTo chooses an actionable point and accepts sourcePosition when the component requires an exact subregion. A hidden handle that appears on hover can be hovered first, then asserted visible before the drag.

5. Example: Test a Canvas Drag With Model-Based Assertions

Canvas contents are not normal DOM nodes, so a locator cannot identify the drawn object. Use the canvas bounding box and coordinates defined by a seeded fixture. Assert the application's exposed model, accessible mirror, or properties panel after moving.

import { test, expect } from '@playwright/test';

test('moves a diagram node on a canvas', async ({ page }) => {
  await page.goto('/diagram?fixture=single-node');

  const canvas = page.getByTestId('diagram-canvas');
  await expect(canvas).toBeVisible();

  const box = await canvas.boundingBox();
  if (!box) {
    throw new Error('Canvas has no bounding box');
  }

  const start = {
    x: box.x + 100,
    y: box.y + 100,
  };
  const end = {
    x: box.x + 260,
    y: box.y + 180,
  };

  await page.mouse.move(start.x, start.y);
  await page.mouse.down();

  try {
    await page.mouse.move(end.x, end.y, { steps: 16 });
  } finally {
    await page.mouse.up();
  }

  await expect(page.getByTestId('selected-node-x')).toHaveText('260');
  await expect(page.getByTestId('selected-node-y')).toHaveText('180');
});

The fixture guarantees a node starts at canvas-local coordinate 100,100. The test calculates viewport coordinates by adding the live canvas origin. A stable viewport and device scale configuration keep the geometry predictable.

The try/finally ensures mouseup occurs if the move throws. Do not place assertions while the mouse button is held.

A properties panel is a better oracle than pixel color. It exposes the application model and yields clear failure messages. If no model is observable, add a test-only seeded route or an accessible representation rather than relying entirely on image recognition.

For a freehand drawing tool, send a series of path points and assert the created object's metadata. That is a pointer-path test, not an element drag, but the same coordinate principles apply.

6. Example: Drop One or More Files From Memory

Current Playwright provides Locator.drop for external file drag-and-drop. In-memory payloads remove fixture-path problems and make contents explicit.

import { test, expect } from '@playwright/test';

test('drops two valid evidence files', async ({ page }) => {
  await page.goto('/defects/BUG-42/evidence');

  const zone = page.getByTestId('evidence-drop-zone');

  await zone.drop({
    files: [
      {
        name: 'console.txt',
        mimeType: 'text/plain',
        buffer: Buffer.from('TypeError: value is undefined\n'),
      },
      {
        name: 'request.json',
        mimeType: 'application/json',
        buffer: Buffer.from(
          JSON.stringify({ method: 'POST', status: 500 }),
        ),
      },
    ],
  });

  await expect(page.getByRole('list', { name: 'Pending evidence' })
    .getByRole('listitem'))
    .toHaveText(['console.txt', 'request.json']);

  await expect(page.getByRole('status'))
    .toHaveText('2 files ready to upload');
});

This proves the drop zone receives both files and displays them in order. Add upload completion assertions in a separate test if the product automatically sends files to a server.

Test rejection with a disallowed MIME type or size defined by the application:

await zone.drop({
  files: {
    name: 'script.exe',
    mimeType: 'application/octet-stream',
    buffer: Buffer.from([0, 1, 2, 3]),
  },
});

await expect(page.getByRole('alert'))
  .toHaveText('Executable files are not allowed');
await expect(page.getByRole('listitem', { name: 'script.exe' }))
  .toHaveCount(0);

Keep the invalid fixture small. The test targets validation logic, not network capacity. For maximum-size boundaries, generate a precisely sized Buffer based on the documented limit without committing a large binary file.

Locator.drop throws when the target rejects the drop protocol because its dragover listener does not call preventDefault. An application that accepts the event but rejects the file type should normally complete drop() and show its own validation message.

7. Example: Drop Text, HTML, or a URL

Drop targets can consume MIME-typed clipboard-style data. Supply data as a map of MIME type to string. This is useful for link collections, rich editors, and workflow designers.

import { test, expect } from '@playwright/test';

test('creates a bookmark from dropped URL data', async ({ page }) => {
  await page.goto('/bookmarks');

  const zone = page.getByTestId('bookmark-drop-zone');

  await zone.drop({
    data: {
      'text/plain': 'Playwright',
      'text/uri-list': 'https://playwright.dev/',
    },
  });

  const bookmark = page.getByRole('link', { name: 'Playwright' });
  await expect(bookmark).toHaveAttribute('href', 'https://playwright.dev/');
  await expect(page.getByRole('status')).toHaveText('Bookmark added');
});

The product defines which MIME type wins when several are present. Test that precedence only if it is contractual. A rich editor may prefer text/html, while a URL target may prefer text/uri-list.

For HTML, use a harmless fixture and assert sanitization:

await page.getByTestId('rich-drop-zone').drop({
  data: {
    'text/html': '<strong>Important</strong><script>window.pwned=true</script>',
    'text/plain': 'Important',
  },
});

await expect(page.getByTestId('rich-drop-zone').locator('strong'))
  .toHaveText('Important');
expect(await page.evaluate(() => (window as any).pwned)).toBeUndefined();

The example accesses a deliberately nonexistent test marker to prove the script did not execute. Production security coverage should also verify dangerous attributes, URL schemes, and server-side sanitization according to the threat model.

External MIME drops do not have a visible source. If the user drags an existing bookmark within the page, use dragTo instead.

8. Example: Pass a Custom DataTransfer Between Events

Some legacy widgets require a proprietary key set during dragstart. Use one DataTransfer JSHandle through all dispatched events and dispose it afterward.

import { test, expect } from '@playwright/test';

test('drops a palette component on the form builder', async ({ page }) => {
  await page.goto('/form-builder');

  const source = page.getByTestId('palette-date-field');
  const target = page.getByTestId('form-canvas');

  const transfer = await page.evaluateHandle(() => {
    const value = new DataTransfer();
    value.effectAllowed = 'copy';
    value.setData(
      'application/x-form-component',
      JSON.stringify({ type: 'date', version: 1 }),
    );
    return value;
  });

  try {
    await source.dispatchEvent('dragstart', { dataTransfer: transfer });
    await target.dispatchEvent('dragenter', { dataTransfer: transfer });
    await target.dispatchEvent('dragover', { dataTransfer: transfer });
    await target.dispatchEvent('drop', { dataTransfer: transfer });
    await source.dispatchEvent('dragend', { dataTransfer: transfer });
  } finally {
    await transfer.dispose();
  }

  await expect(target.getByLabel('Date')).toBeVisible();
  await expect(page.getByRole('status')).toHaveText('Date field added');
});

This test verifies the custom serialized payload. It does not verify that a human can physically drag the palette item because dispatchEvent bypasses real pointer input and actionability. Pair it with at least one dragTo test for the supported user path.

Do not invent a custom MIME type in the test. Read it from the component's documented contract or source. If that implementation detail changes frequently, prefer public behavior through dragTo and assert the created field.

DataTransfer construction must happen in the page context because it is a browser object. page.evaluateHandle returns a JSHandle that can be passed as an event property.

9. Example: Verify a Forbidden Drop Leaves State Unchanged

Negative tests should prove both the rejection message and the absence of mutation. Suppose only administrators can move a locked release card.

import { test, expect } from '@playwright/test';

test('prevents a viewer from moving a locked release', async ({ page }) => {
  await page.goto('/releases/locked-board');

  const locked = page.getByTestId('release-R-9');
  const source = page.getByTestId('column-approved');
  const target = page.getByTestId('column-deployed');

  await expect(locked).toHaveAttribute('aria-disabled', 'true');

  await locked.dragTo(target, { force: true, steps: 6 });

  await expect(page.getByRole('alert'))
    .toHaveText('You do not have permission to deploy this release');
  await expect(source.getByTestId('release-R-9')).toBeVisible();
  await expect(target.getByTestId('release-R-9')).toHaveCount(0);
});

force is justified here only if the requirement is server or application defense against a synthetic forbidden gesture. It intentionally bypasses disabled actionability to exercise the rejection layer. A separate UI test should prove that ordinary users cannot initiate the gesture.

If the disabled element has no event path at all, use an API-level authorization test for backend enforcement and assert the UI's disabled state. Do not contort a browser test into reaching impossible UI behavior.

Other valuable negative cases include incompatible column type, maximum item count, dependency rules, stale record version, offline save, and a target that disappears during the gesture. Each test should state which layer owns the rejection.

10. Cross-Frame Playwright Drag and Drop Examples

A source and target inside the same iframe can be located through FrameLocator and used with normal locator APIs. The following external file example avoids any cross-frame pointer ambiguity because the target owns the entire drop.

import { test, expect } from '@playwright/test';

test('drops a file into an embedded uploader', async ({ page }) => {
  await page.goto('/integrations/uploader');

  const frame = page.frameLocator('iframe[title="Evidence uploader"]');
  const zone = frame.getByTestId('upload-drop-zone');

  await zone.drop({
    files: {
      name: 'evidence.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from('Reproduction completed'),
    },
  });

  await expect(frame.getByText('evidence.txt')).toBeVisible();
});

For an element drag entirely within one iframe, source.dragTo(target) works with locators resolved in that frame. Keep source and target in the same interaction context. Cross-frame element dragging is browser- and product-sensitive because coordinates, event ownership, and security boundaries differ. Prefer an explicit application workflow if the product supports moving data between frames.

Playwright locators pierce open shadow roots by default for normal CSS and user-facing locator strategies. Closed shadow roots are not accessible to automation through DOM locators. Test through the component's public UI or an integration seam rather than patching internals.

Frame and shadow examples need the same outcome assertions as top-level interactions. A successful input call is never enough.

11. Build a Table-Driven Drag Coverage Matrix

Avoid duplicating an entire test when only source, target, and expected status change. Use typed cases while preserving descriptive test names and unique seeded records.

import { test, expect } from '@playwright/test';

const moves = [
  {
    task: 'TASK-10',
    from: 'todo',
    to: 'progress',
    status: 'IN_PROGRESS',
  },
  {
    task: 'TASK-11',
    from: 'progress',
    to: 'review',
    status: 'IN_REVIEW',
  },
] as const;

for (const move of moves) {
  test(
    move.task + ': ' + move.from + ' -> ' + move.to,
    async ({ page }) => {
      await page.goto('/boards/transition-fixture');

      const source = page.getByTestId('column-' + move.from);
      const target = page.getByTestId('column-' + move.to);
      const task = source.getByTestId('task-' + move.task);

      await task.dragTo(target, { steps: 8 });

      await expect(target.getByTestId('task-' + move.task))
        .toBeVisible();
      await expect(page.getByTestId('status-' + move.task))
        .toHaveText(move.status);
    },
  );
}

Keep invalid transitions in a separate table because their oracle is different. Over-parameterization makes failures hard to read and can hide setup differences. A handful of representative transitions often provides better signal than every combinatorial pair at the browser level.

Use lower-level unit or API tests for a full transition matrix, then reserve browser drag tests for critical user paths, permission boundaries, and component integration. This creates fast coverage without losing confidence in the gesture.

If general action timeouts affect these cases, follow the Playwright timeout debugging guide before raising suite-wide limits.

12. Diagnose and Stabilize Drag Examples in CI

Trace Viewer displays mouse action points and is the fastest first diagnostic:

npx playwright test tests/drag-examples.spec.ts --trace=on
npx playwright show-trace test-results/path-to-trace/trace.zip

Classify failures rather than applying one universal workaround:

Symptom Investigation
Source actionability timeout Hidden handle, overlay, animation, duplicate locator
dragTo completes, no state change Wrong source, rejected target, insufficient move events
Wrong insertion index Target region and relative y coordinate
Only one browser fails Component support, dragover behavior, pointer event differences
CI only Viewport, fonts, data collision, slower persistence
File drop throws Target did not accept dragover
State changes then reverts Failed backend save or optimistic UI rollback
Manual mouse misses Stale bounding box, scroll, responsive layout

Use web-first assertions for readiness and saved state. Avoid page.waitForTimeout. Animation should expose a stable state, or the test can wait for the target's visible drop-active condition if the component renders one.

Set the same viewport locally and in CI for coordinate tests. Seed unique records per worker. Turn off only nonessential animations through an application test mode or reduced-motion emulation when that matches supported behavior, not by injecting arbitrary CSS that changes hit testing.

Do not compensate for a wrong target with very large steps. Steps changes event density, not element identity. Keep one trace from each browser-specific failure and compare where the pointer starts and ends.

Interview Questions and Answers

Q: How do you decide between dragTo, page.mouse, and drop?

I use dragTo for a visible source element moving to a target, page.mouse for coordinate or path-driven interfaces, and drop for external files or MIME data. I choose based on the application's event contract rather than trying APIs randomly. Then I assert a domain outcome.

Q: What should a kanban drag test assert?

It should assert the card's identity, arrival in the target, removal from the source, user feedback, and persistence when important. For a save test, I match the specific update response and reload. This detects cloning, wrong-record moves, and optimistic rollback.

Q: How do you test exact ordering in a sortable list?

I drop at a meaningful relative target position and assert the complete expected item order or a stable neighborhood. The coordinate is relative to the target padding box, so the test remains independent of screen position. I seed a deterministic list.

Q: Why might you drag a handle instead of its parent card?

The component may attach gesture listeners only to the handle, while clicks on the card navigate or select. Using the actual handle reproduces the user contract and prevents a drag attempt from becoming an unintended click. I locate it within the card to preserve identity.

Q: How do you test drag and drop on a canvas?

I seed a known canvas state, calculate viewport points from the live canvas bounding box, and send a mouse sequence with steps. I assert an exposed model value or properties panel, not only pixels. A stable viewport keeps geometry predictable.

Q: What is the right way to test a file drop zone?

Use Locator.drop with file paths or in-memory file objects. Assert accepted filenames, validation, pending state, and upload completion according to scope. Include rejected MIME type and size boundaries without committing huge fixtures.

Q: How would you test authorization on a forbidden drag?

I first prove the UI exposes a disabled or unavailable gesture. For defense in depth, I test backend authorization at the API layer. I use a forced browser gesture only when the application intentionally handles that synthetic path and I assert no state mutation.

Q: What makes drag tests flaky in parallel CI?

Shared board data, shared accounts, changing layouts, stale bounding boxes, and unobserved optimistic save failures are common causes. I isolate records per worker, use relative geometry, await specific state, and capture traces. Fixed sleeps and force usually hide the underlying issue.

Common Mistakes

  • Using one drag technique for every widget without identifying its event contract.
  • Verifying only that dragTo returned, with no business assertion.
  • Omitting a precondition, allowing a no-op to satisfy the postcondition.
  • Checking target arrival but not source removal, which misses duplication.
  • Dropping a sortable item at the target center when the component uses top and bottom insertion zones.
  • Dragging a parent card when only its handle is interactive.
  • Using stale bounding boxes after scrolling or layout changes.
  • Testing canvas output only with screenshots when a stable model is available.
  • Using setInputFiles for a drop-zone behavior that specifically requires drag events.
  • Treating Locator.drop as an element-to-element action.
  • Dispatching custom events without a real gesture test.
  • Forcing a disabled drag and calling it accessibility coverage.
  • Comparing a broad background response instead of the exact persistence request.
  • Reusing shared board records across parallel workers and retries.
  • Adding arbitrary sleeps for animations or backend saves.
  • Skipping browser projects without making the product support decision explicit.

Conclusion

The best Playwright drag and drop examples are technology-specific and outcome-driven. Use dragTo for visible elements, relative positions for sortable insertion, page.mouse for canvases, Locator.drop for external payloads, and custom DataTransfer dispatch only when the component truly requires it.

Build a small browser matrix around critical user flows, then cover transition logic and authorization more deeply at faster layers. Deterministic data, unique locators, relative geometry, persistence assertions, and traces turn drag tests from fragile demos into useful engineering signals.

Interview Questions and Answers

How do you select a Playwright drag-and-drop strategy?

I identify whether the product moves a visible DOM element, tracks a pointer path, draws on canvas, or receives external DataTransfer data. I choose dragTo, page.mouse, or drop accordingly. I validate domain state rather than trusting input completion.

What assertions would you use for a kanban move?

I verify a known card starts only in the source, then assert it appears in the target and disappears from the source. For important flows, I also assert the specific update payload, success feedback, and state after reload.

How do target positions help with sortable lists?

Sortable components often map the upper and lower parts of a row to before and after insertion. targetPosition lets the test choose that region relative to the target padding box. I assert exact order to prove the intended index.

Why is handle-level dragging important?

Only the handle may own gesture listeners, while the card body navigates or selects. Dragging the handle matches the real interaction and prevents false failures or accidental clicks. An accessible name on the handle also improves locator quality.

How do you make a canvas drag test maintainable?

I use a seeded model, live canvas bounds, local coordinates, a fixed viewport, and a model-backed oracle. I keep the path short and meaningful and guarantee mouseup in finally. I avoid unexplained absolute screen coordinates.

How would you test file-drop validation?

I use Locator.drop with small in-memory payloads for valid, invalid MIME, boundary size, and multiple-file cases. I assert the accepted list and precise rejection messages. I separate browser validation from backend upload and security tests when appropriate.

What are the limitations of dispatching DataTransfer events?

dispatchEvent bypasses real pointer movement and actionability, so it may pass when the UI is unusable. It is suitable for a narrow custom payload contract. I pair it with a real dragTo or manual mouse path and dispose the handle.

How do you diagnose a drag test that only fails in WebKit?

I inspect the trace, pointer path, event log, component browser support, and dragover acceptance. I compare the same viewport and data across projects and try meaningful stepped movement. I do not skip WebKit until the product support decision is explicit.

Frequently Asked Questions

What is the best Playwright drag and drop example for a kanban board?

Locate the card within its source column, call card.dragTo(targetColumn, { steps: 8 }), and assert it appears in the target and disappears from the source. Add a matched save response and reload for persistence coverage.

How do I reorder a sortable list with Playwright?

Drag the source item to a target item and use targetPosition near the top or bottom according to insertion behavior. Assert the exact resulting list order, not just source visibility.

How do I test a drag handle in Playwright?

Locate the named handle inside the parent item and use the handle as the dragTo source. Assert that the parent item moved and that its click or navigation behavior was not accidentally triggered.

How do I drag an object inside an HTML canvas?

Get the canvas bounding box, convert known canvas-local coordinates to viewport coordinates, and use page.mouse move, down, stepped move, and up. Assert the application's exposed coordinates or model state.

Can Playwright drop several files at once?

Yes. Call target.drop({ files: [...] }) with paths or in-memory file objects. Then assert all accepted file names, validation results, and upload state.

How do I provide custom DataTransfer data in Playwright?

For external data, use target.drop({ data: { mimeType: value } }). For a visible legacy source with a custom dragstart contract, create a DataTransfer through page.evaluateHandle and pass the same handle to dispatched events.

How do I test drag and drop inside an iframe?

Use page.frameLocator to locate source and target inside the frame, then apply normal locator actions. File drops work directly on the frame-owned target; keep element source and target in the same interaction context when possible.

How can I stop drag-and-drop tests from being flaky in CI?

Seed worker-specific data, set a deliberate viewport for coordinate tests, use relative positions, wait for observable saved state, and review traces. Avoid fixed delays, stale bounding boxes, and shared records.

Related Guides