skip to content

user-event vs fireEvent

You will learn why user-event is preferred over fireEvent: it replays the full sequence of events a real interaction produces instead of dispatching a single synthetic one. Interviewers ask this to check whether your tests would actually catch a bug in a disabled button or a keyboard handler.

on this pageshow

explore

questions

5

In Testing Library, what is the difference between fireEvent.click(button) and userEvent.click(button), and why is user-event the recommended default?

level: juniorimportance: must knowfreq 76%

answer

  1. one dispatch versus one interaction
  2. what the browser does before click
  3. pointer, mouse, focus, then click
  4. unreachable elements are refused, not clicked

basics

~20 s

fireEvent dispatches one synthetic DOM event. user-event replays the whole sequence a real interaction produces — pointer, mouse, focus and click events — and refuses to interact with elements a user could not touch, so it catches bugs fireEvent silently passes.

solid answer

~40 s

`fireEvent.click(el)` is a thin wrapper over `dispatchEvent`: exactly one `click` event lands on the element, and nothing else. A real click is a whole cascade — the browser fires pointer events, mouse events, moves focus, and only then fires `click`, and it does none of that if the element is disabled or has `pointer-events: none`. `userEvent.click(el)` reproduces that cascade and performs those checks, so the component is exercised the way a user exercises it. That matters because plenty of UI logic hangs off the parts fireEvent skips: `onMouseDown` handlers, focus and blur validation, dropdowns that close on outside pointer-down. I default to user-event for anything a human does with a mouse, keyboard or touch, and keep fireEvent for events a user cannot produce directly.

code

javascript · 18 lines
javascript
import { fireEvent } from '@testing-library/dom'
import userEvent from '@testing-library/user-event'

const button = document.createElement('button')
button.textContent = 'Save'
document.body.append(button)

const seen = []
for (const type of ['pointerdown', 'mousedown', 'focus', 'pointerup', 'mouseup', 'click']) {
  button.addEventListener(type, (event) => seen.push(event.type))
}

fireEvent.click(button)
console.log(seen) // ['click']

seen.length = 0
await userEvent.setup().click(button)
console.log(seen) // ['pointerdown','mousedown','focus','pointerup','mouseup','click']

go deeper

for a junior

Be able to say plainly that fireEvent dispatches a single event while user-event performs a whole interaction, and that user-event is the default choice for clicking, typing and tabbing.

for a middle

Explain the actual event sequence a click produces and name concrete handlers that fireEvent would miss, such as mousedown-based dropdown dismissal or blur-based validation.

for a senior

Show judgment about test value: argue which of the two supports the claim you want to make about the feature, and where fireEvent remains correct for events no gesture produces.

for a principal

Own the convention. Decide how a codebase enforces interaction-level tests — lint rules against fireEvent for gestures, shared test utilities, and the migration cost of an existing fireEvent-heavy suite.

## Two very different levels of abstraction `fireEvent` and `userEvent` are often presented as interchangeable ways to "click a button in a test". They are not. They sit at different levels: `fireEvent` operates on **events**, `user-event` operates on **interactions**. `fireEvent.click(element)` constructs a `MouseEvent` of type `click` and calls `element.dispatchEvent(...)` on it. That is the whole story. No other event is created, no state changes, no checks are made about whether that element could receive a click at all. It is the testing equivalent of reaching into the page and poking one listener. ## What a browser actually does when a user clicks When a person presses a mouse button over a button, the browser emits a sequence roughly like: ```text pointerover → pointerenter → pointermove → pointerdown → mousedown → (focus moves to the element) → pointerup → mouseup → click ``` Before any of that, the browser decides *whether the element is interactive at all*: it hit-tests the coordinates, honours `pointer-events`, and suppresses interaction with disabled form controls. `user-event` models this. `await user.click(button)` walks the same ordered sequence of events, moves focus as the browser would, and tracks pointer state between calls. It also refuses to proceed when the target's effective `pointer-events` is `none`, throwing instead of quietly dispatching. ```javascript const user = userEvent.setup() await user.click(saveButton) // a whole interaction fireEvent.click(saveButton) // one event object ``` ## Why the difference changes what your tests catch A test is only as good as the bugs it can fail on. Everything `fireEvent.click` skips is a place where a real defect can hide and the test will still be green: - **Handlers on the skipped events.** Menus and popovers commonly close on `pointerdown`/`mousedown` rather than `click`; drag affordances start on `mousedown`. `fireEvent.click` never fires those, so the code path is untested — or worse, a test that asserts "the menu closed" passes for the wrong reason. - **Focus.** A real click focuses the clicked control and blurs the previous one. Field-level validation that runs on blur, or a combobox that commits its value on blur, is simply not exercised by a dispatched `click`. - **Unreachable elements.** A control styled `pointer-events: none`, or one sitting under a modal overlay, cannot be clicked by a human. `fireEvent.click` clicks it anyway and the test reports a feature that does not work as working. - **Event properties.** A real `click` carries `detail`, button state and modifier flags that user-event maintains; a bare `fireEvent.click` leaves them at defaults unless you pass them explicitly. The same argument applies to typing, tabbing and selecting: `userEvent.type` produces key events per character, `userEvent.tab()` moves focus along the tab order, `userEvent.selectOptions` goes through the real control interaction. ## The API shape follows from this Because one interaction is many events with possible delays between them, user-event's API (from v14 onwards) is asynchronous and instance-based: you create an instance with `userEvent.setup()` and `await` each interaction. `fireEvent`, being one dispatch, stays synchronous. The asymmetry is not an inconvenience to work around — it reflects that you asked for something bigger. ## When fireEvent is still the right tool User-event only models things a user can do. Events that nothing in the user's hands produces directly — `scroll` on a container, media events such as `play` or `ended`, `animationend`, `load`/`error` on an image, a custom event emitted by a third-party widget — have no user-event equivalent, and `fireEvent` is the correct and only tool. What you should not do is reach for `fireEvent.click` or `fireEvent.change` as a shortcut for a gesture user-event can perform. ## The rule of thumb Ask what you are claiming. "This component responds to a click event" is a weak claim about your own wiring. "A user can click this and the right thing happens" is the claim the product cares about — and only the interaction-level API can support it. Default to user-event; drop to fireEvent deliberately, for events with no gesture behind them.

  • If both APIs end up firing a click event, why would a test that uses fireEvent ever fail to catch a bug?
    Because the bug often lives in what fireEvent skips. A dropdown that closes on `mousedown`, a field that validates on blur, or a control styled `pointer-events: none` all behave correctly under a bare dispatched `click` and incorrectly for a real user. The test asserts your wiring, not the user's experience.
  • Does user-event replace your assertions about handler calls with something better?
    It changes what the assertion is worth rather than replacing it. Asserting `onClick` was called after a user-event click means a user could have caused that call; after a fireEvent click it only means the listener exists. Where possible, assert on the resulting DOM — the text that appeared, the row that vanished — rather than on the spy.
  • Is user-event slower, and does that matter?
    It does more work per interaction and can add a configured delay between keystrokes, so it is measurably slower than a single dispatch. In practice the difference is milliseconds per interaction against a test suite dominated by rendering, and the extra fidelity is exactly what you are paying for. Optimise elsewhere before trading it away.

fireEvent is like reaching into the machine and tripping one switch; user-event is like handing the machine a mouse and letting it decide which switches that trips.

saying these in an interview costs you the question

  • Says fireEvent and userEvent are the same thing with different syntax
  • Thinks fireEvent.click fires the full mouse and pointer sequence
  • Believes fireEvent moves focus to the clicked element
  • Claims user-event is only preferred because it is newer
  • Uses fireEvent.click as a shortcut for every gesture in a suite

context

open as a page

Why does Testing Library's user-event require a userEvent.setup() call and return a promise from every interaction, instead of exposing plain synchronous helpers like fireEvent does?

level: middleimportance: should knowfreq 50%

basics

~20 s

Because one interaction is many ordered events with optional delays between them, which needs an asynchronous API, and because interactions share state — held modifier keys, pressed mouse buttons, the clipboard. setup() creates the instance that carries that state and your configuration.

open as a page

A test fills a search box with Testing Library's fireEvent.change(input, { target: { value: 'shoes' } }) and the assertion passes. Which real-user behaviours does that single line fail to exercise?

level: middleimportance: should knowfreq 55%

basics

~20 s

That line jumps the value from empty to 'shoes' in one synthetic change event. It produces no key events and no per-character input events, so keyboard handlers, per-keystroke logic, maxlength enforcement, caret-position formatting and focus behaviour all go untested.

open as a page

A Testing Library test clicks a button with fireEvent.click and passes, but in the browser that button does nothing because a CSS rule sets pointer-events: none on it. Why did the test still pass, and what would user-event do differently?

level: seniorimportance: should knowfreq 43%

basics

~20 s

fireEvent calls dispatchEvent directly, skipping everything the browser decides before an event exists — so it clicks elements no user could reach. user-event checks the element's effective pointer-events and throws instead of clicking, turning the silent pass into a failure.

open as a page

If Testing Library's user-event is the default for interactions, when is fireEvent still the right tool in a component test?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Use fireEvent for events no human gesture produces directly — scroll, media events, animationend and transitionend, load and error on an image, or a custom event from a third-party widget — and when you need precise control over an event's properties. Never as a shortcut for a click or keystroke.

open as a page