If Testing Library's user-event is the default for interactions, when is fireEvent still the right tool in a component test?
answer
- events with no gesture behind them
- the environment fires it, not the user
- media, animation, scroll, load, custom
- testing the handler, not the reachability
basics
~20 sUse 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.
solid answer
~50 suser-event only models things a user can physically do, so anything outside that vocabulary has no equivalent and `fireEvent` is the correct tool rather than a fallback. That covers events fired by the environment or by other code: `scroll` on a container, media events such as `play` or `ended`, `animationend` and `transitionend`, `load` and `error` on an image, `storage` or `online` on the window, and custom events dispatched by a widget you do not control. It also covers cases where you need to control event properties directly — supplying coordinates, or a `dataTransfer` payload for drag-and-drop, which user-event has no API for. The line I hold is that if a user could perform it with a mouse, keyboard or touch, it goes through user-event; if the event arrives from the environment, it goes through fireEvent — and I say in the test name which claim I am making.
go deeper
Know that user-event covers what a person does and fireEvent covers events the environment produces, such as an image failing to load or a media element ending.
Name the concrete categories — media, animation, scroll, load and error, custom events — and explain why jsdom will never fire them on its own.
Articulate the cost: a dispatched event proves handling, not occurrence. Choose deliberately per test and make the test name reflect the narrower claim.
Set and defend the boundary across a codebase: which flows earn a real-browser test because dispatch cannot establish reachability, and how review catches fireEvent creeping back into gesture paths.
## The vocabulary limit user-event exposes what a person can do: click, double-click, hover, type, press keys, tab, select options, upload a file, copy and paste. That vocabulary is the point — it is what makes an interaction test meaningful. But plenty of events a component must handle are not produced by a gesture at that level of abstraction, and for those, `fireEvent` is not a compromise, it is the right layer. ## The legitimate categories **Environment events.** The browser emits `load` and `error` on images and scripts, `online`/`offline` and `storage` on the window, `visibilitychange` on the document. Nothing a user does in the page produces these directly. If a component swaps in a placeholder when an image fails, the only way to reach that branch in a jsdom test is `fireEvent.error(img)`. **Media and animation events.** `play`, `pause`, `ended`, `timeupdate`, `animationend`, `transitionend`. jsdom neither plays media nor runs animations, so these will never fire on their own — a toast that removes itself on `animationend` is untestable without dispatching one. **Scrolling.** Scroll position in jsdom is not driven by any device; there is no layout to scroll. Testing that a sticky header appears past a threshold means setting `scrollTop` and dispatching `fireEvent.scroll`. **Custom and third-party events.** A charting or editor library may emit its own `CustomEvent`. You dispatch it directly: `fireEvent(node, new CustomEvent('chart:select', { detail: { id: 7 } }))`. **Precise event properties.** When the assertion depends on a specific `clientX`/`clientY`, or on a `dataTransfer` payload for drag-and-drop, you need to construct the event yourself. user-event has no drag-and-drop API at all, so a drag test in jsdom is necessarily a sequence of hand-built events — which is also a strong hint that drag-and-drop is better verified in a real browser. ## What you are giving up each time Be explicit about the price, because that is the senior half of the answer. A dispatched event proves only that **the component reacts to that event**. It does not prove the event can occur, that it can occur at the right moment, or that anything in the app will ever produce it. That is an acceptable claim for `animationend` — the browser genuinely produces it and you are testing the reaction. It is a worthless claim for `fireEvent.click` on a button, because whether the user can produce the click is the interesting part. So the honest formulation is: use `fireEvent` where the event's *occurrence* is not in question and its *handling* is what you are testing. Where the occurrence is the thing at risk, you need either an interaction-level API or a real browser. ## The anti-patterns to name - **Reaching for fireEvent because a user-event call is inconvenient.** Typing feels slow, hovering seems to need extra setup, so someone dispatches `mouseOver` directly. The resulting test passes against a component whose hover handling is broken. - **Using fireEvent to "get past" a thrown pointer-events error.** The library threw because the element is unreachable. Swapping in a dispatch removes the message, not the bug. - **Faking a gesture as a chain of dispatched events.** Reconstructing pointerdown/mousedown/focus/pointerup/click by hand reinvents user-event badly and will drift from what browsers actually do. ## Mixing the two in one test Mixing is normal and fine. A realistic video player test might use `user.click(playButton)` for the gesture and `fireEvent.ended(video)` to drive the media element to the end of playback, because the first is a user action and the second is an environment event. What matters is that each choice is deliberate and the test name reflects the claim: "shows the replay button when playback ends" is honest; "user can replay the video" would not be, because nothing in that test proves the ending is reachable. ## How to close the answer Give the rule in one line — gesture goes through user-event, environment goes through fireEvent — then name the cost of the second: you are testing the handler, not the reachability. That framing shows you are choosing a level of confidence rather than a function.
- How would you test that a toast removes itself when its exit animation finishes?Dispatch the animation completion event at the toast node, since jsdom runs no animations and the event will never occur on its own, then assert the toast is gone from the DOM. The claim is narrow but honest: given the animation ends, the component cleans up. Whether the animation actually runs is a browser-level question.
- A colleague replaces a failing user-event click with fireEvent.click to make the suite green. What do you say in review?That the failure was information. user-event refuses to click elements a user could not reach, so the red test was reporting a real reachability problem or a genuinely disabled control. Dispatching the event removes the message and keeps the defect, and now the suite actively defends the broken state.
- Is hand-rolling a pointer sequence with fireEvent ever justified?Almost never for gestures user-event covers — you would be reimplementing it with worse fidelity and no maintenance behind it. The legitimate exception is an event shape the library does not model, such as drag-and-drop with a dataTransfer payload, and even then it is worth asking whether that flow deserves a real-browser test instead.
saying these in an interview costs you the question
- Uses fireEvent for clicks because it is faster to write
- Says fireEvent is legacy and should never appear
- Hand-rolls a pointer sequence instead of using the interaction API
- Swaps in fireEvent to silence a pointer-events error
- Claims a dispatched event proves the user can trigger it