user-event vs fireEvent
TL;DR
fireEvent dispatches a single DOM event synchronously. user-event simulates a full user interaction — typing a character fires keydown, keypress, input, keyup; focusing first, blurring on Tab, respecting disabled and aria-* state. Always prefer user-event — it catches real bugs (forgotten focus management, missed input events) that fireEvent lets through. Use fireEvent only when you need to test a specific event no user could realistically fire.
Interview Q&A
Q: One-line difference?
A: fireEvent.click(button) fires click. userEvent.click(button) fires pointerdown → mousedown → pointerup → mouseup → click, in order, with the right bubbles/cancelable settings, after checking the element is visible and clickable. user-event mimics real browser sequencing; fireEvent is the low-level “dispatch this one event.”
Q: API shape — they differ.
A:
// fireEvent — synchronous, single event
fireEvent.click(button);
fireEvent.change(input, { target: { value: "hello" } });
fireEvent.keyDown(input, { key: "Enter" });
// user-event — async, you setup() once per test
const user = userEvent.setup();
await user.click(button);
await user.type(input, "hello"); // types each char with full event sequence
await user.keyboard("{Enter}");
user-event v14+ requires userEvent.setup() once (returns an instance with the methods); all interactions are async and must be awaited.
Q: A bug fireEvent misses.
A: A form with onChange validation:
function Form() {
const [value, setValue] = useState("");
const [error, setError] = useState("");
return (
<form onSubmit={(e) => { e.preventDefault(); setError(value.length < 3 ? "too short" : ""); }}>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<button type="submit">Submit</button>
{error && <p role="alert">{error}</p>}
</form>
);
}
fireEvent.change with the target value works:
fireEvent.change(input, { target: { value: "hi" } });
// works — value updates
But if the component depends on a keydown handler (“submit on Enter”), fireEvent.change doesn’t dispatch keys — so a “submit on Enter while typing” test silently passes when the real behavior is broken. userEvent.type(input, "hi{Enter}") does dispatch the keys.
userEvent.click checks the element isn’t disabled or pointer-events: none — fireEvent.click will “click” a disabled button without error, hiding a bug.
Q: When use fireEvent?
A: Rarely:
- Testing a specific event no user can realistically dispatch (
scrollon a non-scrollable container, programmaticfocus/blurfor edge cases). - Performance —
user-eventis slower because it does more. - Legacy tests in a codebase where wholesale migration isn’t worth it.
For new code: default to user-event.
Q: Show me userEvent.setup + common interactions.
A:
import userEvent from "@testing-library/user-event";
it("fills and submits a form", async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), "ada@example.com");
await user.type(screen.getByLabelText(/password/i), "secret");
await user.click(screen.getByRole("checkbox", { name: /remember me/i }));
await user.selectOptions(screen.getByLabelText(/country/i), "us");
await user.tab(); // focus next field
await user.keyboard("{Enter}"); // submit via keyboard
expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
});
Common methods:
user.click(el),user.dblClick(el),user.tripleClick(el)user.type(el, "text")— types char-by-char; supports special{Enter},{Backspace},{Tab}, modifiers{Shift>}A{/Shift}user.keyboard("{Enter}")— keys without a targetuser.clear(el)— clear inputuser.tab({ shift: false })— focus next/previoususer.hover(el),user.unhover(el)user.selectOptions(select, values)user.upload(fileInput, file)— file inputs
Q: How do you test paste behavior?
A: user.paste("text") (no target — pastes into the currently focused element):
await user.click(input);
await user.paste("pasted content");
For clipboard-related code, you may need jsdom’s clipboard polyfill or happy-dom’s built-in support.
Q: How do you simulate keyboard shortcuts with modifiers?
A: Curly-brace syntax with > to hold, / to release:
await user.keyboard("{Control>}a{/Control}"); // Ctrl+A
await user.keyboard("{Meta>}{Shift>}P{/Shift}{/Meta}"); // Cmd+Shift+P
For testing global shortcut handlers, ensure focus is in the right place first (user.click(document.body) if needed).
Q: Async vs sync user-event?
A: Async (v14+) is the default and recommended:
const user = userEvent.setup();
await user.click(button);
The async version uses setTimeout(0) between events to mimic real browser timing — important for code that reads state in between events.
Legacy userEvent.click(button) (no setup) is synchronous; deprecated. Don’t write new tests with it.
Q: What’s userEvent.setup options for?
A: Tune the simulator:
const user = userEvent.setup({
delay: null, // no delay between events (faster, less realistic)
pointerEventsCheck: 0, // skip pointer-events check (escape hatch)
writeToClipboard: true, // write to test clipboard
advanceTimers: vi.advanceTimersByTime, // integrate with fake timers
});
Most tests use defaults. advanceTimers is useful when you’ve called vi.useFakeTimers() — let user-event advance them automatically so debounce/throttle behave naturally.
Gotchas / edge cases
user.typedoesn’t replace existing content — append. Useuser.clear()first or{selectall}modifier.disabledelements —user-eventskips clicks;fireEventdoesn’t.fireEvent.change(input, { target: { value: ... } })is the only way to set a value programmatically withfireEvent. Withuser-event, useuser.type(input, "x")oruser.clear()+user.type().actwarnings —user-eventalready wraps inact; usually no manualactneeded. If you see warnings, your async code probably needsawait-ing.- Slow tests with
user.type— typing 50 chars one-at-a-time isn’t free. Useuser.paste("long string")for bulk input. - Form submit by Enter — works in
user-eventwhen the form has a single text input or a submit button (HTML behavior). Multiple-input forms without a submit button: Enter does nothing, same as browsers. onChangefor React’s<input>— React’s syntheticonChangeis actually the DOMinputevent. Bothuser.typeandfireEvent.changework;user.typeis more thorough.
What a senior is expected to say
- “
user-eventfor almost everything — it simulates the full event sequence and respectsdisabled/aria-*.fireEventis the low-level escape hatch.” - “
userEvent.setup()once per test, async API,awaitevery interaction. v14+ pattern.” - “
user.typedispatches each key —keydown/keypress/input/keyupper char. Catches handlers that depend on the right event (‘submit on Enter’, ‘autocomplete on keyup’).” - “
fireEvent.clickon adisabledbutton silently succeeds —user.clickfails as a real browser would. The bug-catching difference.” - “Pair
user-event’sadvanceTimerswithvi.useFakeTimers()so debounce/throttle don’t hang in tests.”
Cross-references
- React Testing Library queries: 03_react_testing_library.md
- Async UI testing: 06_async_ui_testing.md
- Vitest setup: 02_vitest_vs_jest.md
Further reading
@testing-library/user-eventdocs: https://testing-library.com/docs/user-event/intro- Migration guide v13 → v14: https://testing-library.com/docs/user-event/intro#differences-from-fireevent
- Common keys reference: https://testing-library.com/docs/user-event/keyboard