skip to content

In Playwright, how does locator.dispatchEvent('click') differ from locator.click(), and when is it wrong?

level: middleimportance: should knowfreq 43%

answer

  1. One drives a pointer, one does not
  2. Visibility stops mattering
  3. Nothing about it is trusted
  4. eventInit fills the event's own fields
  5. An escape hatch, not a shortcut

basics

~20 s

locator.click drives a real pointer to the element and sends mousedown and mouseup. dispatchEvent builds a synthetic event in the page and fires it straight at the node, whatever its visibility, with no pointer, no coordinates and isTrusted false.

solid answer

~40 s

`locator.click()` is a real gesture: Playwright moves its virtual mouse onto the element and sends the whole `mousemove`, `mousedown`, `mouseup`, `click` sequence, so hover styles fire and the topmost element at that point is what gets hit. `locator.dispatchEvent(type, eventInit?)` skips all of it -- it constructs an event of the given type in the page, initialises it from `eventInit`, and calls `dispatchEvent` on the matched node. The event bubbles and is `composed` and `cancelable` by default, but `isTrusted` is `false` and the element's visibility is irrelevant, so a button under a modal still receives it. That makes it an escape hatch for raising events the input pipeline cannot produce -- a `dragstart` carrying a `DataTransfer` handle, say -- and a poor way to make a stubborn click pass.

code

typescript · 7 lines
typescript
// Escape hatch: raise a drag pair the pointer pipeline cannot build on its own.
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
const card = page.getByRole('listitem', { name: 'Fix login redirect' });
const done = page.getByRole('region', { name: 'Done' });

await card.dispatchEvent('dragstart', { dataTransfer });
await done.dispatchEvent('drop', { dataTransfer });

go deeper

for a junior

Reach for click by default. Know that dispatchEvent exists and that it fires an event directly at the element rather than moving a mouse, so it is not what a user actually does.

for a middle

Explain the mechanics: the event is built in the page from the type and eventInit, bubbles and is cancelable by default, and lands on the element whatever its visibility state, with isTrusted false.

for a senior

Show when the escape hatch is legitimate, such as raising an event the input stack cannot generate, and why swapping it in to silence a failing click hides the very defect that click was catching.

for a principal

Own the rule. Dispatched events buy green tests at the cost of coverage, so decide where they are permitted, require a comment naming the reason, and keep a real-pointer path for the flows that matter.

## Two different machines Playwright gives you two ways to make a page think a click happened, and they run on completely different machinery. `locator.click()` drives the **input pipeline**. Playwright moves its virtual pointer to a point on the resolved element and sends the whole `mousemove`, `mousedown`, `mouseup`, `click` sequence at real coordinates. Hover styles fire on the way in. Whatever element is topmost at that point is what receives the press, so an overlay that covers the button intercepts the click exactly as it would for a user. The events carry a button, coordinates, and `isTrusted` set to `true`. `locator.dispatchEvent(type, eventInit?)` runs **inside the page**. Playwright constructs an event object of the given type, initialises it from `eventInit`, and calls `dispatchEvent` on the matched node. No pointer moves. No coordinates are involved. The element's visibility state is irrelevant -- the documented behaviour is that the event is dispatched regardless -- so a button underneath a modal receives its `click` event and its handler runs. ## What the dispatched event looks like The event Playwright builds is `composed`, `cancelable` and bubbling by default, so it propagates up the tree and a listener on an ancestor still sees it. The one thing it cannot be is trusted: an event created and dispatched from script has `isTrusted` set to `false`, and the browser will not perform the default user-activation-gated behaviours that a real click unlocks. | | `locator.click()` | `locator.dispatchEvent('click')` | |---|---|---| | pointer moves | yes | no | | coordinates | real, from the element box | none | | element under an overlay | the overlay is hit | the element is hit | | visibility matters | yes | no | | hover and pointer events on the way | fired | not fired | | `isTrusted` | `true` | `false` | | you can shape the event | only through the click options | freely, through `eventInit` | ## eventInit and live objects `eventInit` supplies the event-specific initialisation properties, and which ones are meaningful depends entirely on the type: `key` for a `KeyboardEvent`, `button` and `clientX` for a `MouseEvent`, `deltaY` for a `WheelEvent`, `dataTransfer` for a `DragEvent`. Plain values are serialised into the page, which is fine for numbers and strings but useless for a live browser object. For those you pass a handle instead. The canonical case is a drag event that has to carry a real `DataTransfer`: ```typescript const dataTransfer = await page.evaluateHandle(() => new DataTransfer()); await source.dispatchEvent('dragstart', { dataTransfer }); await target.dispatchEvent('drop', { dataTransfer }); ``` This is the shape of use that `dispatchEvent` is genuinely for: raising an event the pointer pipeline cannot construct on its own, with a payload only the page can create. ## Where it is the right tool, and where it is not Legitimate uses: - raising an event with a payload, such as the `dragstart`/`drop` pair above - exercising a handler for an event the browser will not generate on demand at all, such as a device orientation event - deliberately simulating a click "by any means possible" in a throwaway setup step whose purpose is to reach a state, not to test the click The trap is the fourth use, and it is the one interviewers are probing for: swapping `dispatchEvent('click')` in because `click()` failed. When a click on the issue tracker's "Resolve" button times out because a toast covers it, the failure **is the finding** -- a real user cannot reach that button either. Dispatching the event straight at the node turns a genuine product defect into a green test, and it removes the whole point of driving a real pointer. The right fixes are to wait for the toast to clear, dismiss it, or file the overlap as a bug. The second trap is subtler: because nothing hovers on the way in, a widget that only becomes interactive on `pointerenter` never gets set up, so a dispatched `click` can run against a half-initialised component and produce a passing test for behaviour that never happens in a browser. ## How to talk about it The compact answer is that `click()` asks the browser to click the element and `dispatchEvent()` tells the element that it was clicked. The first can be wrong about your intent because the page is genuinely in the way; the second is never wrong about your intent and, for exactly that reason, never tells you the page was in the way.

  • What does the eventInit argument let you control, and what has to be passed as a handle?
    `eventInit` supplies the event-specific initialisation properties -- `key` for a `KeyboardEvent`, `button` and `clientX` for a `MouseEvent`, `deltaY` for a `WheelEvent`. Plain values are serialised into the page, so a live browser object such as a `DataTransfer` must be passed as a handle from `page.evaluateHandle()` instead.
  • A click fails because a toast covers the button. Why is dispatchEvent the wrong fix?
    Because the failure is the finding. A real user cannot reach a button under a toast either, so dispatching the event straight at the node converts a genuine defect into a passing test. Wait for the toast to clear or dismiss it, and keep the pointer path honest.

saying these in an interview costs you the question

  • Uses dispatchEvent to make a covered button pass
  • Thinks the dispatched event is indistinguishable from a real one
  • Believes dispatchEvent waits for the element to be visible
  • Assumes dispatched events do not bubble
  • Expects hover styles to fire before a dispatched click