skip to content

In React Native Testing Library 14, how do you type a search and press a filter chip with userEvent, and why await each call?

level: juniorimportance: must knowfreq 50%

answer

  1. const user = userEvent.setup()
  2. user.type(input, text) on a host TextInput
  3. user.press(chip) on the pressable
  4. every call returns a Promise
  5. assert on what the screen shows

basics

~20 s

Create an instance with userEvent.setup(), then await user.type(searchInput, 'dune') and await user.press(chip), and assert on the rendered result. Each call returns a Promise that settles after its events and React updates, so skipping await lets assertions run too early.

solid answer

~50 s

With React Native Testing Library 14 I call `const user = userEvent.setup()` at the start of the test, after `await render(<MovieSearch />)`. I find the input by its label and call `await user.type(input, 'dune')`, which focuses the `TextInput`, emits key, change and `onChangeText` events one character at a time, then blurs it. Then `await user.press(screen.getByRole('button', { name: 'Sci-Fi' }))` presses the filter chip with the full press sequence. Every `userEvent` method is async: it waits between events and runs each dispatch inside `act`, so the promise resolves only after the handlers and React's re-render have finished. Without `await`, the assertion can run before the typed text or the filter is applied, and the updates may land outside `act`. Finally I assert on what the user would see, for example that the matching titles are on screen.

code

tsx · 13 lines
tsx
import { render, screen, userEvent } from '@testing-library/react-native';
import { MovieSearch } from './MovieSearch';

test('filters movies by query and genre', async () => {
  await render(<MovieSearch />);
  const user = userEvent.setup();

  await user.type(screen.getByLabelText('Search movies'), 'dune');
  await user.press(screen.getByRole('button', { name: 'Sci-Fi' }));

  expect(screen.getByText('Dune: Part Two')).toBeOnTheScreen();
  expect(screen.queryByText('Paddington')).not.toBeOnTheScreen();
});

go deeper

for a junior

Recall the pattern: await render, userEvent.setup, await user.type and user.press, then assert on the screen.

for a middle

Explain what type emits per character, that it appends to existing text, and why each call resolves only after its events and re-renders.

for a senior

Recognise missing awaits as the source of flaky or leaking tests, enforce them with lint rules, and separate interaction from waiting for async results.

for a principal

Standardise interaction helpers and review rules so every team writes tests in the same user-centred shape and flakiness stays measurable.

## The shape of an interaction test A React Native Testing Library interaction test has four steps: 1. **Render** the screen: `await render(<MovieSearch />)`. 2. **Set up user events**: `const user = userEvent.setup()`, once per test. 3. **Interact**, awaiting each call: `type`, `press`, `clear`, `paste`, `longPress`, `scrollTo`. 4. **Assert** on the output the user sees, not on internal state. `userEvent` also exposes direct calls such as `userEvent.press(element)` for compatibility with older code; they create a fresh default instance on every call. Calling `setup()` once keeps options such as `delay` in one place. ## What type and press do **`user.type(input, 'dune')`** works only on a host `TextInput` and throws for anything else. It: - presses into the input and fires `focus`; - for each character fires `keyPress`, `change`, `changeText` and `selectionChange`; - finishes with `endEditing` and `blur`. The component's `onChangeText` is therefore called four times, with `d`, `du`, `dun` and `dune`, as it would be on a device. Text is **appended** to the current value, so an input already holding text needs `user.clear(input)` first. **`user.press(chip)`** replays the press sequence on the pressable: press in, press out, press, taking at least 130 ms. ## Why every call must be awaited Since v14, `render`, `fireEvent` and `act` are async, and `userEvent` methods always were. Each method: - waits between events, by default `delay: 0`, which still yields to the event loop so other asynchronous code can run between keystrokes; - dispatches each event inside an async `act`, flushing state updates and effects; - resolves only when the whole sequence has finished. | Code | What happens | |---|---| | `await user.type(input, 'dune')` | All four `onChangeText` calls and their re-renders complete before the next line | | `user.type(input, 'dune')` without `await` | The next line runs before most or all of the events; assertions see stale UI | | `user.press(chip)` without `await` | The filter may apply after the test ends, producing `act` warnings or a failure in a later test | A missing `await` is the most common cause of flaky RNTL interaction tests. Lint rules for floating promises catch it mechanically. ## Choosing elements to interact with - Find the search field by its label: `screen.getByLabelText('Search movies')`. - Find the chip by role and name: `screen.getByRole('button', { name: 'Sci-Fi' })`. - Keep test IDs as a fallback for elements with no user-facing identity. ## Asserting the result After the interactions, assert on the rendered outcome: a matching title is visible, a non-matching one is not, the chip shows as selected. If the search is debounced or fetches data, the result appears later; waiting for it is the job of the async `findBy*` queries, not of more `userEvent` calls.

  • Why is onChangeText called four times when typing 'dune'?
    `user.type` simulates typing one character at a time, emitting `keyPress`, `change`, `changeText` and `selectionChange` for each key. The handler receives `d`, `du`, `dun` and `dune`, which exercises the same per-keystroke logic as a device, such as debouncing or validation.
  • What does userEvent.setup({ delay }) change?
    It sets the pause between subsequent events, such as keystrokes. The default is 0, which still yields to the event loop between events so other asynchronous code can run. A larger delay makes typing slower in real time and is rarely needed.

saying these in an interview costs you the question

  • userEvent methods are synchronous, so await is optional.
  • user.type replaces the existing text in the TextInput.
  • user.type works on any element that displays text.
  • You must wrap user.press in act() yourself.
  • userEvent.setup() must be called before render.