user-event vs fireEvent
Prefer user-event for realistic interactions — how it differs from fireEvent, setup patterns, and when fireEvent is still fine.
- testing
- user-event
@testing-library/user-event simulates full user interactions (pointer, keyboard, focus). fireEvent dispatches one DOM event you specify. For most UI tests, user-event catches bugs fireEvent misses: focus order, key sequences, double-click, typing into inputs.
Docs: user-event, fireEvent.
Quick contrast
// fireEvent — low level
fireEvent.change(input, { target: { value: 'abc' } });
fireEvent.click(button);
// user-event — higher level
const user = userEvent.setup();
await user.type(input, 'abc');
await user.click(button);
type fires keydown/keypress/keyup per character and updates value like a browser (with some differences still — read docs for your version).
Always setup() (v14+)
test('submits', async () => {
const user = userEvent.setup();
render(<Form />);
await user.click(screen.getByRole('button', { name: /save/i }));
});
Don’t use the legacy userEvent.click API without setup in modern versions.
What user-event covers better
| Interaction | Why user-event |
|---|---|
| Typing | Selection, modifiers |
| Tab focus | Real focus movement |
| selectOptions | Closer to users |
| upload | File inputs |
keyboard {Enter} |
Shortcut handlers |
await user.tab();
await user.keyboard('{Enter}');
When fireEvent is OK
- Firing a rare event user-event doesn’t model well yet.
- Simple smoke where you only need
clickand don’t care about focus. - Library unit tests of event handler props with synthetic events.
Still prefer user-event in app code.
Async nature
user-event methods return promises — always await. Forgetting await is a top flake source.
// Bug
user.click(btn);
expect(…); // race
// Fix
await user.click(btn);
Fake timers
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
Required when timers are mocked — fake timers for debounce.
Pointer vs click
await user.pointer({ keys: '[MouseLeft]', target: el });
Useful for drag sequences; most cases click is enough.
Footguns
- Mixing
fireEvent.changewith controlled React inputs incorrectly. - Not awaiting.
- Clicking before element is enabled — use
findByfirst. - Assuming user-event is 100% browser-identical — still jsdom limits.
Interview out-loud answer
“I use user-event.setup() for interactions because it models typing, clicking, and tabbing more realistically than fireEvent. I await every call. fireEvent is a low-level escape hatch, not the default.”
Clipboard and special keys
await user.click(input);
await user.keyboard('{Control>}a{/Control}{Backspace}');
await user.paste(); // when supported / mocked
Prefer documented user-event helpers over synthesizing partial keyup/keydown pairs with fireEvent unless you’re testing a low-level handler API.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Implementation notes
Wire this into real code on the next feature, not only a demo. Prefer the smallest change that encodes the rule — a shared helper, a lint rule, or a checklist item in the PR template. Revisit after a week of production traffic: if users or tests still hit the failure mode, the documentation (and the abstraction) are not sharp enough yet.
Related on this site
- Testing Library
- Testing Library queries priority
- Fake timers for debounce
- Flaky tests common causes
- Keyboard accessibility checklist
Further reading
Related guides
- Accessibility Testing in CIWire axe and lint rules into CI without false confidence — what automation catches, what it misses, and a practical pipeline.
- Component Testing StorybookUse Storybook as a component workshop and optional test runner — stories as living specs, interaction tests, and a11y checks.
- Contract Testing APIsKeep frontend and backend agreements honest — schema contracts, Pact-style consumer tests, and OpenAPI-driven checks without brittle E2E.
- Coverage Metrics PitfallsWhy 100% line coverage can still ship bugs — gaming metrics, useless tests, and what coverage is actually good for.
- End-to-End Testing TradeoffsWhen E2E tests earn their keep, why they flake, and how to keep a small critical-path suite instead of a slow second frontend.