Skip to content
React rc testing 4 min read

Simulating User Events

Tests are only as trustworthy as the interactions they simulate. Real users do not fire a single synthetic change event when they fill in a form — they focus an input, press keys one at a time, blur, and click. The @testing-library/user-event companion library reproduces those full interaction sequences, so your assertions reflect what a person would actually experience. This page shows how to type, click, and select with user-event, why it beats the low-level fireEvent, and how to combine it with async queries like findBy and waitFor.

Why user-event over fireEvent

React Testing Library ships fireEvent, which dispatches one DOM event at a time. Calling fireEvent.change(input, { target: { value: 'hi' } }) jumps straight to the final value without simulating keystrokes, focus, or key events. That can pass tests that a real user would fail — for example, a field that ignores onKeyDown or enforces a maxlength per character.

user-event instead dispatches the realistic chain of events a browser produces. Typing a single character triggers keydown, keypress, input, and keyup, plus focus management. The result is a test that exercises your component the way the platform does.

AspectfireEventuser-event
Events dispatchedOne synthetic eventFull realistic sequence
Focus / blurNot handledManaged automatically
TypingSets value directlyPer-character key events
Async by defaultNoYes (returns Promises)
Recommended forEdge-case low-level eventsAlmost all interaction tests

Prefer user-event for everything that mimics a human. Reach for fireEvent only when you need a single, low-level event that user-event does not model.

Setting up a user instance

Since v14, you create an instance with userEvent.setup() before rendering, ideally in the test body. Every method on the returned object is async, so always await it.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

test('lets a user sign in', async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  await user.type(screen.getByLabelText(/email/i), '[email protected]');
  await user.type(screen.getByLabelText(/password/i), 'hunter2');
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  expect(screen.getByText(/welcome, ada/i)).toBeInTheDocument();
});

Call setup() once per test. It configures a clean clipboard, pointer, and keyboard state, which keeps tests isolated from one another.

Typing into inputs

user.type sends each character through the keyboard pipeline. You can include special keys with curly-brace syntax — {Enter}, {Backspace}, {ArrowLeft}, and modifier holds like {Shift>}A{/Shift}.

test('submits the search on Enter', async () => {
  const user = userEvent.setup();
  const onSearch = vi.fn();
  render(<SearchBox onSearch={onSearch} />);

  const input = screen.getByRole('searchbox');
  await user.type(input, 'react testing{Enter}');

  expect(onSearch).toHaveBeenCalledWith('react testing');
});

To replace existing text, clear first:

await user.clear(input);
await user.type(input, 'new value');

Clicking, double-clicking, and hovering

Pointer interactions follow the same realistic model. user.click dispatches pointerdown, mousedown, focus, pointerup, mouseup, and click in order.

test('toggles details on click', async () => {
  const user = userEvent.setup();
  render(<Disclosure />);

  const toggle = screen.getByRole('button', { name: /show details/i });
  expect(screen.queryByText(/secret content/i)).not.toBeInTheDocument();

  await user.click(toggle);
  expect(screen.getByText(/secret content/i)).toBeInTheDocument();
});

Other pointer helpers include user.dblClick(el), user.hover(el), user.unhover(el), and user.tripleClick(el) — useful for testing tooltips and selection behavior.

Selecting options and checkboxes

For <select> elements use selectOptions; for checkboxes and radios use click.

test('filters by category', async () => {
  const user = userEvent.setup();
  render(<ProductFilter />);

  await user.selectOptions(
    screen.getByRole('combobox', { name: /category/i }),
    'books',
  );
  await user.click(screen.getByRole('checkbox', { name: /in stock/i }));

  expect(screen.getByRole('option', { name: 'Books' }).selected).toBe(true);
});

You can also keyboard-navigate with user.tab() to verify focus order and accessibility.

Async queries: findBy and waitFor

Interactions often kick off asynchronous work — a fetch, a debounce, a state update that renders later. Use findBy* queries, which return a Promise and retry until the element appears (or time out around one second by default).

test('shows results after submitting', async () => {
  const user = userEvent.setup();
  render(<UserSearch />);

  await user.type(screen.getByRole('searchbox'), 'ada');
  await user.click(screen.getByRole('button', { name: /search/i }));

  const result = await screen.findByText(/ada lovelace/i);
  expect(result).toBeInTheDocument();
});

When you need to assert on something other than a single element — for example, that a spinner has disappeared — wrap the assertion in waitFor, which polls until the callback stops throwing.

import { waitFor } from '@testing-library/react';

await waitFor(() => {
  expect(screen.queryByRole('status')).not.toBeInTheDocument();
});

Output:

 ✓ src/UserSearch.test.jsx (1)
   ✓ shows results after submitting

 Test Files  1 passed (1)
      Tests  1 passed (1)

Use findBy* to wait for something to appear and waitForElementToBeRemoved (or waitFor with queryBy*) to wait for it to vanish. Reserve raw waitFor for non-element assertions to keep failures readable.

Best Practices

  • Always await every user-event call — forgetting the await produces flaky, race-prone tests.
  • Create one userEvent.setup() per test, before render, to keep keyboard and pointer state isolated.
  • Query by accessible role and name (getByRole, getByLabelText) so tests resemble how users and assistive tech find elements.
  • Prefer findBy* over waitFor plus getBy* for awaiting elements; it is shorter and gives clearer error messages.
  • Use user-event for interaction tests and fall back to fireEvent only for rare low-level events it does not model.
  • Avoid arbitrary setTimeout waits; rely on Testing Library’s built-in retrying queries instead.
Last updated June 14, 2026
Was this helpful?