ESC

Type to search the knowledge base.

user-event vs fireEvent

Prefer user-event for realistic interactions — how it differs from fireEvent, setup patterns, and when fireEvent is still fine.

beginner3 min read
  • 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 click and 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

  1. Mixing fireEvent.change with controlled React inputs incorrectly.
  2. Not awaiting.
  3. Clicking before element is enabled — use findBy first.
  4. 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.

Further reading

Related guides