skip to content

Your Playwright dragTo() leaves an issue-board card in place. How do you diagnose it and hand-build the drag?

level: seniorimportance: must knowfreq 46%

answer

  1. Four steps, only one move
  2. The board never sees dragover
  3. Repeat the hover on the target
  4. Aim with source and target positions
  5. More interpolated moves along the path

basics

~20 s

dragTo moves to the source, presses, moves to the target and releases. A board built on HTML5 drag and drop needs at least two mouse moves before dragover fires, so rebuild the gesture and hover the target twice.

solid answer

~40 s

`source.dragTo(target)` performs four steps: move to the source element, `mousedown`, move to the target, `mouseup`. That is enough for a pointer-driven board, but a widget built on the HTML5 drag-and-drop API reorders in a `drop` handler, and browsers need **at least two mouse moves** before `dragover` fires reliably. One `dragTo()` issues a single move, so the drop zone never activates and the card is picked up and dropped with nothing accepting it. Drive the gesture yourself instead: `source.hover()`, `page.mouse.down()`, `target.hover()`, `target.hover()` again, then `page.mouse.up()`. If the drop only registers on a narrow strip, aim with `sourcePosition` and `targetPosition`; if the widget samples the path, raise `steps` so more interpolated `mousemove` events are sent.

code

typescript · 11 lines
typescript
// Issue-board drag that an HTML5 drop zone will actually accept.
const card = page.getByRole('listitem', { name: 'Fix login redirect' });
const done = page.getByRole('region', { name: 'Done' });

await card.hover();
await page.mouse.down();
await done.hover();
await done.hover(); // second move -- this is what raises dragover
await page.mouse.up();

await expect(done.getByRole('listitem', { name: 'Fix login redirect' })).toBeVisible();

go deeper

for a junior

Know that dragTo drags one locator onto another. If the card does not move, say so and investigate rather than adding a sleep and hoping the next run is kinder.

for a middle

Explain the four steps dragTo performs and why one move is not enough for a board that listens for dragover. Name the manual sequence and say exactly where the second hover goes.

for a senior

Diagnose from the events the widget needs: read its listeners, confirm whether dragover ever fires, then choose between a positioned dragTo, more interpolated steps, and the hand-built sequence.

for a principal

Drag is the most expensive gesture a suite owns. Decide how many drag paths are worth covering end to end and where a direct state change is the honest, cheaper substitute for a simulated one.

## What dragTo actually performs `source.dragTo(target)` is a pointer gesture, not a drag-and-drop API call. Playwright performs four steps: 1. move the virtual pointer to the source element 2. `mousedown` 3. move the pointer to the target element or the given target position 4. `mouseup` That is the entire sequence. Playwright does not fabricate `dragstart`, `dragover` or `drop` events -- it moves a mouse and presses a button, and it is the browser and the page that decide what that means. ## Why an HTML5 board ignores it An issue board built on the HTML5 drag-and-drop API does not reorder on `mouseup`. It reorders in a `drop` handler, and the browser only raises `drop` on an element that has been accepting the drag through `dragover`. That is where the failure lives: **browsers need at least two mouse moves before `dragover` fires reliably across engines.** A single `dragTo()` issues one move to the destination, the drop zone never activates, and the card is picked up and dropped with the column never having seen a valid target. The test passes its actions and fails its assertion, or silently leaves the card where it was. This is also why the failure looks browser-dependent and gets misfiled as flakiness. The gesture is genuinely incomplete; different engines are just differently forgiving about it. ## The hand-built drag The fix is to drive the lower-level verbs yourself and issue the second move explicitly: 1. `await source.hover()` -- pointer arrives over the card 2. `await page.mouse.down()` -- press and hold 3. `await target.hover()` -- first move onto the drop zone 4. `await target.hover()` -- second move, and this is the one that raises `dragover` 5. `await page.mouse.up()` -- release, and the board's `drop` handler runs The repeated `hover()` is the entire trick, and a second `page.mouse.move(x, y)` does the same job. What does **not** work is a fixed wait before the release: the browser is waiting for an event, not for time, so a sleep sends nothing and the run just fails more slowly. ## Aiming and shaping the drag When the gesture is right but the geometry is wrong, `dragTo` has options for it: | option | what it controls | |---|---| | `sourcePosition` | the grab point, as an `{ x, y }` offset from the source element's padding box | | `targetPosition` | the drop point, as an `{ x, y }` offset from the target element's padding box | | `steps` | how many interpolated `mousemove` events are sent between the press and the release | `sourcePosition` matters when only a handle is draggable rather than the whole card. `targetPosition` matters when a column accepts a drop only on a narrow insertion strip, or when dropping in the dead centre of a tall column is ambiguous between two positions. `steps` matters when the widget samples the pointer path -- a board that renders a live insertion indicator as you move needs intermediate positions to compute one, and a single jump to the destination gives it nothing to work with. ## Diagnosing before rewriting Work from the events the widget needs rather than from the symptom: - read the drop zone's listeners; if it binds `dragover` and `drop`, you are in the HTML5 case and you need the second move - if it binds `pointermove` and `pointerup` instead, plain `dragTo()` is the right verb and the problem is geometry -- reach for the position options - confirm which of the two it is before changing anything, because the two fixes look nothing alike - check whether the card is picked up at all; if the source never receives `mousedown` where you expect, the grab handle is the issue, not the drop - treat a passing drag with a wrong final order as a separate bug: the gesture worked and the app placed the card badly One more distinction worth keeping straight: dragging a card between columns is this pointer gesture, whereas dropping a file or clipboard payload **onto** the page from outside it is a different verb -- `locator.drop()` -- which synthesises `dragenter`, `dragover` and `drop` with a constructed `DataTransfer`. It looks similar and solves a different problem. ## What to take away `dragTo` is a mouse gesture spelled conveniently, so it succeeds exactly when a mouse gesture is what the page listens for. The hand-built version exists because one class of widget needs a shape the convenience method does not produce, and the missing ingredient is always the same: a second mouse move over the target before the button comes up.

  • When is the built-in dragTo() enough, and which of its options do you reach for first?
    It is enough whenever the board reorders on plain pointer events rather than the HTML5 drag API. Reach first for `sourcePosition` and `targetPosition` when the grab handle or the drop strip is a small part of the element, and for `steps` when the widget samples the pointer path between press and release.
  • Why is repeating locator.hover() better than inserting a fixed wait before the mouse-up?
    Because the missing ingredient is a second `mousemove`, not elapsed time. A wait sends no events at all, so `dragover` still never fires and the run just fails more slowly. Repeating the hover, or issuing a second `page.mouse.move()`, produces the move the browser is waiting for.
  • How do you tell which kind of drop zone you are dealing with before rewriting the test?
    Read the drop zone's listeners. If it binds `dragover` and `drop`, you are in the HTML5 case and you need the second move. If it binds `pointermove` and `pointerup`, plain `dragTo()` is the right verb and any failure is geometry, which the position options fix.

saying these in an interview costs you the question

  • Adds a fixed sleep instead of a second mouse move
  • Thinks dragTo dispatches dragstart and drop directly
  • Blames the browser rather than the missing move
  • Retries the same dragTo call hoping it sticks
  • Assumes any point inside the column is a valid drop