What's the difference between user-event and fireEvent, and why is user-event usually the right choice?
Understanding that real interactions are sequences of events, and why simulating a single event under-tests behavior.
fireEvent dispatches a SINGLE DOM event exactly as you specify — fireEvent.click(el) fires one click, fireEvent.change(input, {target:{value:'x'}}) sets the value and fires one change. user-event simulates a REAL USER INTERACTION, which is usually a whole SEQUENCE of events plus browser-realistic behavior. When a user clicks, the browser fires pointerdown, mousedown, focus, pointerup, mouseup, and click; when they type 'ab', it fires keydown/keypress/input/keyup per character, respects focus, and won't 'type' into a disabled or readonly field. user-event reproduces those sequences, so it catches bugs fireEvent misses: a handler that relies on focus firing, a keydown listener, an input that should ignore typing when disabled, or logic that runs on the intermediate events. Because it's realistic, user-event is asynchronous (v14+ returns promises; you await it) and you typically set it up with userEvent.setup() at the top of the test. The guidance is: reach for user-event by default because 'the more your tests resemble how software is used, the more confidence they give you'; drop to fireEvent only for the rare low-level case user-event doesn't model well (certain scroll, custom, or media events, or when you need to fire one specific event in isolation). A concrete gotcha: fireEvent.change directly sets a value without the keystroke sequence, so a component that formats input on each keystroke, or blocks certain characters, can look correct under fireEvent and be broken for real users — user-event.type would expose it. So the difference isn't cosmetic: it's the gap between 'this event handler ran' and 'a person using this actually gets the right result'.
Simulating clicks/typing in component tests; debugging a test that passes but the feature is broken for users.
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/react';
test('typing runs the real event sequence', async () => {
const user = userEvent.setup();
render(<Search />);
const box = screen.getByRole('searchbox');
await user.type(box, 'hi'); // keydown/keypress/input/keyup per char, respects focus
expect(box).toHaveValue('hi');
});
// fireEvent fires ONE event and skips the sequence:
// fireEvent.change(box, { target: { value: 'hi' } });
// -> can pass while per-keystroke formatting/validation is broken for users.