# Drag and Drop Recipes > **When to use**: You need to test drag-and-drop interactions -- sortable lists, kanban boards, file drop zones, or any element that can be repositioned by dragging. --- ## Recipe 1: Native HTML5 Drag and Drop ### Complete Example **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('drags an item from one container to another', async ({ page }) => { await page.goto('/drag-demo'); const sourceItem = page.getByText('Draggable Item'); const dropZone = page.locator('#drop-zone'); // Verify initial state await expect(sourceItem).toBeVisible(); await expect(dropZone).not.toContainText('Draggable Item'); // Perform drag and drop await sourceItem.dragTo(dropZone); // Verify the item moved to the drop zone await expect(dropZone).toContainText('Draggable Item'); }); test('drags between two drop zones using locators', async ({ page }) => { await page.goto('/drag-demo'); const item = page.locator('[data-testid="item-1"]'); const zoneA = page.locator('[data-testid="zone-a"]'); const zoneB = page.locator('[data-testid="zone-b"]'); // Item starts in zone A await expect(zoneA).toContainText('Item 1'); // Drag to zone B await item.dragTo(zoneB); // Item is now in zone B, not zone A await expect(zoneB).toContainText('Item 1'); await expect(zoneA).not.toContainText('Item 1'); // Drag back to zone A await zoneB.getByText('Item 1').dragTo(zoneA); await expect(zoneA).toContainText('Item 1'); await expect(zoneB).not.toContainText('Item 1'); }); test('verifies drag visual feedback', async ({ page }) => { await page.goto('/drag-demo'); const sourceItem = page.getByText('Draggable Item'); const dropZone = page.locator('#drop-zone'); // Start drag manually to check intermediate states await sourceItem.hover(); await page.mouse.down(); // Move toward the drop zone const dropBox = await dropZone.boundingBox(); await page.mouse.move(dropBox!.x + dropBox!.width / 2, dropBox!.y + dropBox!.height / 2); // Verify the drop zone shows a visual indicator while hovering await expect(dropZone).toHaveClass(/drag-over|highlight/); // Complete the drop await page.mouse.up(); // Verify drop zone returns to normal styling await expect(dropZone).not.toHaveClass(/drag-over|highlight/); await expect(dropZone).toContainText('Draggable Item'); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('drags an item from one container to another', async ({ page }) => { await page.goto('/drag-demo'); const sourceItem = page.getByText('Draggable Item'); const dropZone = page.locator('#drop-zone'); await expect(sourceItem).toBeVisible(); await expect(dropZone).not.toContainText('Draggable Item'); await sourceItem.dragTo(dropZone); await expect(dropZone).toContainText('Draggable Item'); }); test('drags between two drop zones', async ({ page }) => { await page.goto('/drag-demo'); const item = page.locator('[data-testid="item-1"]'); const zoneA = page.locator('[data-testid="zone-a"]'); const zoneB = page.locator('[data-testid="zone-b"]'); await expect(zoneA).toContainText('Item 1'); await item.dragTo(zoneB); await expect(zoneB).toContainText('Item 1'); await expect(zoneA).not.toContainText('Item 1'); }); ``` --- ## Recipe 2: Sortable Lists (Reordering Items) ### Complete Example **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('reorders items in a sortable list', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); // Verify initial order const initialItems = await list.getByRole('listitem').allTextContents(); expect(initialItems[0]).toContain('Task A'); expect(initialItems[1]).toContain('Task B'); expect(initialItems[2]).toContain('Task C'); // Drag Task C to the top (above Task A) const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' }); const taskA = list.getByRole('listitem').filter({ hasText: 'Task A' }); await taskC.dragTo(taskA); // Verify new order const reorderedItems = await list.getByRole('listitem').allTextContents(); expect(reorderedItems[0]).toContain('Task C'); expect(reorderedItems[1]).toContain('Task A'); expect(reorderedItems[2]).toContain('Task B'); }); test('reorders using drag handle', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); // Use the drag handle (grip icon) instead of the whole item const dragHandle = list .getByRole('listitem') .filter({ hasText: 'Task C' }) .getByRole('button', { name: /drag|reorder|grip/i }); const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' }); await dragHandle.dragTo(targetItem); const reorderedItems = await list.getByRole('listitem').allTextContents(); expect(reorderedItems[0]).toContain('Task C'); }); test('reorder persists after page reload', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' }); const taskA = list.getByRole('listitem').filter({ hasText: 'Task A' }); await taskC.dragTo(taskA); // Wait for the save API call to complete await page.waitForResponse((response) => response.url().includes('/api/tasks/reorder') && response.status() === 200 ); // Reload and verify persistence await page.reload(); const items = await list.getByRole('listitem').allTextContents(); expect(items[0]).toContain('Task C'); expect(items[1]).toContain('Task A'); expect(items[2]).toContain('Task B'); }); test('reorders with precise mouse movements for libraries that need it', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); const sourceItem = list.getByRole('listitem').filter({ hasText: 'Task C' }); const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' }); const sourceBox = await sourceItem.boundingBox(); const targetBox = await targetItem.boundingBox(); // Some drag libraries (react-beautiful-dnd, dnd-kit) require specific mouse event sequences await sourceItem.hover(); await page.mouse.down(); // Move in small steps -- some libraries require incremental movement to register const steps = 10; for (let i = 1; i <= steps; i++) { await page.mouse.move( sourceBox!.x + sourceBox!.width / 2, sourceBox!.y + (targetBox!.y - sourceBox!.y) * (i / steps), { steps: 1 } ); } await page.mouse.up(); const items = await list.getByRole('listitem').allTextContents(); expect(items[0]).toContain('Task C'); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('reorders items in a sortable list', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); const initialItems = await list.getByRole('listitem').allTextContents(); expect(initialItems[0]).toContain('Task A'); expect(initialItems[1]).toContain('Task B'); expect(initialItems[2]).toContain('Task C'); const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' }); const taskA = list.getByRole('listitem').filter({ hasText: 'Task A' }); await taskC.dragTo(taskA); const reorderedItems = await list.getByRole('listitem').allTextContents(); expect(reorderedItems[0]).toContain('Task C'); expect(reorderedItems[1]).toContain('Task A'); expect(reorderedItems[2]).toContain('Task B'); }); test('reorders with precise mouse movements', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); const sourceItem = list.getByRole('listitem').filter({ hasText: 'Task C' }); const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' }); const sourceBox = await sourceItem.boundingBox(); const targetBox = await targetItem.boundingBox(); await sourceItem.hover(); await page.mouse.down(); const steps = 10; for (let i = 1; i <= steps; i++) { await page.mouse.move( sourceBox.x + sourceBox.width / 2, sourceBox.y + (targetBox.y - sourceBox.y) * (i / steps), { steps: 1 } ); } await page.mouse.up(); const items = await list.getByRole('listitem').allTextContents(); expect(items[0]).toContain('Task C'); }); ``` --- ## Recipe 3: Kanban Board (Moving Between Columns) ### Complete Example **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('moves a card from Todo to In Progress', async ({ page }) => { await page.goto('/board'); const todoColumn = page.locator('[data-column="todo"]'); const inProgressColumn = page.locator('[data-column="in-progress"]'); // Verify initial state const card = todoColumn.getByText('Fix login bug'); await expect(card).toBeVisible(); const todoCountBefore = await todoColumn.getByRole('article').count(); const inProgressCountBefore = await inProgressColumn.getByRole('article').count(); // Drag the card to In Progress await card.dragTo(inProgressColumn); // Verify the card moved await expect(inProgressColumn.getByText('Fix login bug')).toBeVisible(); await expect(todoColumn.getByText('Fix login bug')).not.toBeVisible(); // Verify counts updated await expect(todoColumn.getByRole('article')).toHaveCount(todoCountBefore - 1); await expect(inProgressColumn.getByRole('article')).toHaveCount(inProgressCountBefore + 1); // Verify column header count badge updated await expect(todoColumn.getByText(`(${todoCountBefore - 1})`)).toBeVisible(); await expect(inProgressColumn.getByText(`(${inProgressCountBefore + 1})`)).toBeVisible(); }); test('moves a card through all stages', async ({ page }) => { await page.goto('/board'); const columns = { todo: page.locator('[data-column="todo"]'), inProgress: page.locator('[data-column="in-progress"]'), review: page.locator('[data-column="review"]'), done: page.locator('[data-column="done"]'), }; // Todo -> In Progress await columns.todo.getByText('Fix login bug').dragTo(columns.inProgress); await expect(columns.inProgress.getByText('Fix login bug')).toBeVisible(); // In Progress -> Review await columns.inProgress.getByText('Fix login bug').dragTo(columns.review); await expect(columns.review.getByText('Fix login bug')).toBeVisible(); // Review -> Done await columns.review.getByText('Fix login bug').dragTo(columns.done); await expect(columns.done.getByText('Fix login bug')).toBeVisible(); // Verify the card is only in the Done column await expect(columns.todo.getByText('Fix login bug')).not.toBeVisible(); await expect(columns.inProgress.getByText('Fix login bug')).not.toBeVisible(); await expect(columns.review.getByText('Fix login bug')).not.toBeVisible(); }); test('reorders cards within the same column', async ({ page }) => { await page.goto('/board'); const todoColumn = page.locator('[data-column="todo"]'); const cardA = todoColumn.getByRole('article').filter({ hasText: 'Task A' }); const cardC = todoColumn.getByRole('article').filter({ hasText: 'Task C' }); // Drag Task C above Task A within the same column await cardC.dragTo(cardA); // Verify new order within the column const cards = await todoColumn.getByRole('article').allTextContents(); expect(cards.indexOf('Task C')).toBeLessThan(cards.indexOf('Task A')); }); test('kanban board state persists via API', async ({ page }) => { await page.goto('/board'); const todoColumn = page.locator('[data-column="todo"]'); const inProgressColumn = page.locator('[data-column="in-progress"]'); // Wait for the PATCH/PUT to complete after the drag const responsePromise = page.waitForResponse( (r) => r.url().includes('/api/cards') && r.request().method() === 'PATCH' ); await todoColumn.getByText('Fix login bug').dragTo(inProgressColumn); const response = await responsePromise; expect(response.status()).toBe(200); const body = await response.json(); expect(body.column).toBe('in-progress'); // Reload to confirm persistence await page.reload(); await expect(inProgressColumn.getByText('Fix login bug')).toBeVisible(); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('moves a card from Todo to In Progress', async ({ page }) => { await page.goto('/board'); const todoColumn = page.locator('[data-column="todo"]'); const inProgressColumn = page.locator('[data-column="in-progress"]'); const card = todoColumn.getByText('Fix login bug'); await expect(card).toBeVisible(); await card.dragTo(inProgressColumn); await expect(inProgressColumn.getByText('Fix login bug')).toBeVisible(); await expect(todoColumn.getByText('Fix login bug')).not.toBeVisible(); }); test('moves a card through all stages', async ({ page }) => { await page.goto('/board'); const columns = { todo: page.locator('[data-column="todo"]'), inProgress: page.locator('[data-column="in-progress"]'), review: page.locator('[data-column="review"]'), done: page.locator('[data-column="done"]'), }; await columns.todo.getByText('Fix login bug').dragTo(columns.inProgress); await expect(columns.inProgress.getByText('Fix login bug')).toBeVisible(); await columns.inProgress.getByText('Fix login bug').dragTo(columns.review); await expect(columns.review.getByText('Fix login bug')).toBeVisible(); await columns.review.getByText('Fix login bug').dragTo(columns.done); await expect(columns.done.getByText('Fix login bug')).toBeVisible(); }); ``` --- ## Recipe 4: File Drop Zone ### Complete Example **TypeScript** ```typescript import { test, expect } from '@playwright/test'; import path from 'path'; test('uploads a file by dropping it on the drop zone', async ({ page }) => { await page.goto('/upload'); const dropZone = page.locator('[data-testid="file-drop-zone"]'); // Verify initial state await expect(dropZone).toContainText('Drag files here'); // Create a DataTransfer-like event using the file chooser approach // Since Playwright cannot simulate native DnD file events from the OS, // we use the underlying input[type=file] that drop zones rely on const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/sample-document.pdf')); // Verify file appears in the upload list await expect(page.getByText('sample-document.pdf')).toBeVisible(); await expect(page.getByText(/\d+ KB/)).toBeVisible(); }); test('simulates drag-over visual feedback via JavaScript dispatch', async ({ page }) => { await page.goto('/upload'); const dropZone = page.locator('[data-testid="file-drop-zone"]'); // Dispatch dragenter/dragover events to test visual feedback await dropZone.dispatchEvent('dragenter', { dataTransfer: { types: ['Files'] }, }); // Verify the drop zone shows the active/hover state await expect(dropZone).toHaveClass(/drag-active|drop-highlight/); await expect(dropZone).toContainText(/drop.*here|release.*upload/i); // Dispatch dragleave to reset await dropZone.dispatchEvent('dragleave'); await expect(dropZone).not.toHaveClass(/drag-active|drop-highlight/); }); test('rejects invalid file types in drop zone', async ({ page }) => { await page.goto('/upload'); const fileInput = page.locator('input[type="file"]'); // Try to upload a file type that is not allowed await fileInput.setInputFiles({ name: 'malware.exe', mimeType: 'application/x-msdownload', buffer: Buffer.from('fake-content'), }); await expect(page.getByRole('alert')).toContainText(/not allowed|invalid file type/i); await expect(page.getByText('malware.exe')).not.toBeVisible(); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); const path = require('path'); test('uploads a file by dropping it on the drop zone', async ({ page }) => { await page.goto('/upload'); const dropZone = page.locator('[data-testid="file-drop-zone"]'); await expect(dropZone).toContainText('Drag files here'); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/sample-document.pdf')); await expect(page.getByText('sample-document.pdf')).toBeVisible(); }); test('rejects invalid file types in drop zone', async ({ page }) => { await page.goto('/upload'); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles({ name: 'malware.exe', mimeType: 'application/x-msdownload', buffer: Buffer.from('fake-content'), }); await expect(page.getByRole('alert')).toContainText(/not allowed|invalid file type/i); }); ``` --- ## Recipe 5: Drag with Custom Preview / Ghost Image ### Complete Example **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('shows custom drag preview during drag operation', async ({ page }) => { await page.goto('/board'); const card = page.locator('[data-testid="card-1"]'); const targetColumn = page.locator('[data-column="in-progress"]'); const cardBox = await card.boundingBox(); const targetBox = await targetColumn.boundingBox(); // Start dragging await card.hover(); await page.mouse.down(); // Move partially toward the target const midX = (cardBox!.x + targetBox!.x) / 2; const midY = (cardBox!.y + targetBox!.y) / 2; await page.mouse.move(midX, midY, { steps: 5 }); // Take a screenshot to visually verify the drag preview // The custom preview element should be visible during drag await expect(page.locator('.drag-preview')).toBeVisible(); // Verify the original card shows a placeholder/ghost await expect(card).toHaveClass(/dragging|placeholder/); // Complete the drag await page.mouse.move( targetBox!.x + targetBox!.width / 2, targetBox!.y + targetBox!.height / 2, { steps: 5 } ); await page.mouse.up(); // Preview should disappear after drop await expect(page.locator('.drag-preview')).not.toBeVisible(); }); test('drag preview shows item count for multi-select drag', async ({ page }) => { await page.goto('/board'); // Select multiple cards await page.locator('[data-testid="card-1"]').click(); await page.locator('[data-testid="card-2"]').click({ modifiers: ['Shift'] }); await page.locator('[data-testid="card-3"]').click({ modifiers: ['Shift'] }); // Start dragging one of the selected cards const card = page.locator('[data-testid="card-1"]'); const targetColumn = page.locator('[data-column="done"]'); await card.hover(); await page.mouse.down(); const targetBox = await targetColumn.boundingBox(); await page.mouse.move(targetBox!.x + 50, targetBox!.y + 50, { steps: 5 }); // Verify the preview shows the count of selected items await expect(page.locator('.drag-preview')).toContainText('3 items'); await page.mouse.up(); // All three cards should be in the target column await expect(targetColumn.locator('[data-testid="card-1"]')).toBeVisible(); await expect(targetColumn.locator('[data-testid="card-2"]')).toBeVisible(); await expect(targetColumn.locator('[data-testid="card-3"]')).toBeVisible(); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('shows custom drag preview during drag operation', async ({ page }) => { await page.goto('/board'); const card = page.locator('[data-testid="card-1"]'); const targetColumn = page.locator('[data-column="in-progress"]'); const cardBox = await card.boundingBox(); const targetBox = await targetColumn.boundingBox(); await card.hover(); await page.mouse.down(); const midX = (cardBox.x + targetBox.x) / 2; const midY = (cardBox.y + targetBox.y) / 2; await page.mouse.move(midX, midY, { steps: 5 }); await expect(page.locator('.drag-preview')).toBeVisible(); await expect(card).toHaveClass(/dragging|placeholder/); await page.mouse.move( targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { steps: 5 } ); await page.mouse.up(); await expect(page.locator('.drag-preview')).not.toBeVisible(); }); ``` --- ## Recipe 6: Testing Drag Position and Coordinates ### Complete Example **TypeScript** ```typescript import { test, expect } from '@playwright/test'; test('drags an element to a specific coordinate on a canvas', async ({ page }) => { await page.goto('/canvas-editor'); const canvas = page.locator('#design-canvas'); const element = page.locator('[data-testid="shape-1"]'); // Get the initial position const initialBox = await element.boundingBox(); expect(initialBox).toBeTruthy(); // Define target position (absolute coordinates within the canvas) const canvasBox = await canvas.boundingBox(); const targetX = canvasBox!.x + 300; const targetY = canvasBox!.y + 200; // Drag to specific coordinates await element.hover(); await page.mouse.down(); await page.mouse.move(targetX, targetY, { steps: 10 }); await page.mouse.up(); // Verify element moved to approximately the right position const newBox = await element.boundingBox(); expect(newBox!.x).toBeCloseTo(targetX - newBox!.width / 2, -1); expect(newBox!.y).toBeCloseTo(targetY - newBox!.height / 2, -1); }); test('snaps element to grid when dropped', async ({ page }) => { await page.goto('/canvas-editor'); const element = page.locator('[data-testid="shape-1"]'); const canvas = page.locator('#design-canvas'); const canvasBox = await canvas.boundingBox(); // Drag to a position that is not on the grid await element.hover(); await page.mouse.down(); await page.mouse.move(canvasBox!.x + 147, canvasBox!.y + 83, { steps: 10 }); await page.mouse.up(); // With a 20px grid, position should snap to nearest grid point const snappedBox = await element.boundingBox(); expect(snappedBox!.x % 20).toBeCloseTo(0, 0); expect(snappedBox!.y % 20).toBeCloseTo(0, 0); }); test('constrains drag within boundaries', async ({ page }) => { await page.goto('/canvas-editor'); const element = page.locator('[data-testid="constrained-element"]'); const container = page.locator('#constraint-box'); const containerBox = await container.boundingBox(); // Try to drag far outside the container await element.hover(); await page.mouse.down(); await page.mouse.move(containerBox!.x + containerBox!.width + 500, containerBox!.y - 200, { steps: 10, }); await page.mouse.up(); // Element should be clamped within the container const elementBox = await element.boundingBox(); expect(elementBox!.x).toBeGreaterThanOrEqual(containerBox!.x); expect(elementBox!.y).toBeGreaterThanOrEqual(containerBox!.y); expect(elementBox!.x + elementBox!.width).toBeLessThanOrEqual( containerBox!.x + containerBox!.width ); expect(elementBox!.y + elementBox!.height).toBeLessThanOrEqual( containerBox!.y + containerBox!.height ); }); test('resizes an element by dragging its handle', async ({ page }) => { await page.goto('/canvas-editor'); const element = page.locator('[data-testid="shape-1"]'); await element.click(); // Select to show resize handles const resizeHandle = element.locator('.resize-handle-se'); // south-east corner const handleBox = await resizeHandle.boundingBox(); const initialBox = await element.boundingBox(); // Drag the resize handle to make the element larger await resizeHandle.hover(); await page.mouse.down(); await page.mouse.move(handleBox!.x + 100, handleBox!.y + 80, { steps: 5 }); await page.mouse.up(); const newBox = await element.boundingBox(); expect(newBox!.width).toBeCloseTo(initialBox!.width + 100, -1); expect(newBox!.height).toBeCloseTo(initialBox!.height + 80, -1); }); ``` **JavaScript** ```javascript const { test, expect } = require('@playwright/test'); test('drags an element to a specific coordinate on a canvas', async ({ page }) => { await page.goto('/canvas-editor'); const canvas = page.locator('#design-canvas'); const element = page.locator('[data-testid="shape-1"]'); const canvasBox = await canvas.boundingBox(); const targetX = canvasBox.x + 300; const targetY = canvasBox.y + 200; await element.hover(); await page.mouse.down(); await page.mouse.move(targetX, targetY, { steps: 10 }); await page.mouse.up(); const newBox = await element.boundingBox(); expect(newBox.x).toBeCloseTo(targetX - newBox.width / 2, -1); expect(newBox.y).toBeCloseTo(targetY - newBox.height / 2, -1); }); test('constrains drag within boundaries', async ({ page }) => { await page.goto('/canvas-editor'); const element = page.locator('[data-testid="constrained-element"]'); const container = page.locator('#constraint-box'); const containerBox = await container.boundingBox(); await element.hover(); await page.mouse.down(); await page.mouse.move(containerBox.x + containerBox.width + 500, containerBox.y - 200, { steps: 10, }); await page.mouse.up(); const elementBox = await element.boundingBox(); expect(elementBox.x).toBeGreaterThanOrEqual(containerBox.x); expect(elementBox.y).toBeGreaterThanOrEqual(containerBox.y); }); ``` --- ## Variations ### Drag and Drop with Keyboard Accessibility ```typescript test('reorders items using keyboard', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); const taskC = list.getByRole('listitem').filter({ hasText: 'Task C' }); // Focus the item await taskC.focus(); // Use keyboard shortcut to pick up the item await page.keyboard.press('Space'); // Move up twice await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp'); // Drop the item await page.keyboard.press('Space'); const items = await list.getByRole('listitem').allTextContents(); expect(items[0]).toContain('Task C'); }); ``` ### Drag and Drop Between iframes ```typescript test('drags between main page and iframe', async ({ page }) => { await page.goto('/editor'); const sourceItem = page.getByText('Widget A'); const iframe = page.frameLocator('#preview-frame'); const dropTarget = iframe.locator('#content-area'); // Cross-frame drag requires coordinates since dragTo does not work across frames const sourceBox = await sourceItem.boundingBox(); const iframeElement = page.locator('#preview-frame'); const iframeBox = await iframeElement.boundingBox(); // Calculate target position within the iframe const targetX = iframeBox!.x + 100; const targetY = iframeBox!.y + 100; await sourceItem.hover(); await page.mouse.down(); await page.mouse.move(targetX, targetY, { steps: 20 }); await page.mouse.up(); await expect(iframe.getByText('Widget A')).toBeVisible(); }); ``` ### Touch-Based Drag on Mobile ```typescript test('drags items on mobile via touch events', async ({ page }) => { await page.goto('/tasks'); const list = page.getByRole('list', { name: 'Task list' }); const sourceItem = list.getByRole('listitem').filter({ hasText: 'Task C' }); const targetItem = list.getByRole('listitem').filter({ hasText: 'Task A' }); const sourceBox = await sourceItem.boundingBox(); const targetBox = await targetItem.boundingBox(); // Simulate long-press then drag using touch await page.touchscreen.tap(sourceBox!.x + sourceBox!.width / 2, sourceBox!.y + sourceBox!.height / 2); // Dispatch touchstart, touchmove, touchend for libraries that use touch events await sourceItem.dispatchEvent('touchstart', { touches: [{ clientX: sourceBox!.x + 10, clientY: sourceBox!.y + 10 }], }); // Move in steps for (let i = 1; i <= 5; i++) { const y = sourceBox!.y + (targetBox!.y - sourceBox!.y) * (i / 5); await sourceItem.dispatchEvent('touchmove', { touches: [{ clientX: sourceBox!.x + 10, clientY: y }], }); } await sourceItem.dispatchEvent('touchend'); const items = await list.getByRole('listitem').allTextContents(); expect(items[0]).toContain('Task C'); }); ``` --- ## Tips 1. **Use `dragTo()` first, then fall back to manual mouse events**. Playwright's built-in `dragTo()` handles most native HTML5 drag and drop. Only use `page.mouse.down()` / `move()` / `up()` sequences for custom drag libraries (react-beautiful-dnd, dnd-kit, SortableJS) that need specific event sequences. 2. **Add intermediate mouse move steps for drag libraries**. Libraries like `react-beautiful-dnd` require multiple `mousemove` events with small increments to detect a drag. Use `{ steps: 10 }` or a manual loop. A single jump from source to target often fails silently. 3. **Always assert the final state, not just the drop event**. After a drag-and-drop, verify that the DOM actually reflects the change -- item order, column contents, position coordinates. Visual feedback during drag is nice to test but the final persisted state matters most. 4. **Use `boundingBox()` for coordinate-based assertions**. When testing canvas editors, grid layouts, or position-sensitive drops, capture the bounding box after the operation and compare against expected coordinates. Use `toBeCloseTo()` for tolerance. 5. **Test undo after drag operations**. If your app supports Ctrl+Z to undo a reorder or move, test that the drag operation is reversible. This catches state management bugs that only appear on undo. --- ## Related - [Playwright Drag and Drop Docs](https://playwright.dev/docs/input#drag-and-drop) - `recipes/file-upload-download.md` -- File drop zones specifically - `foundations/actions.md` -- Mouse and keyboard interaction basics - `patterns/page-objects.md` -- Encapsulate complex drag flows