In React Native Testing Library 14, how do you type a search and press a filter chip with userEvent, and why await each call?
answer
- const user = userEvent.setup()
- user.type(input, text) on a host TextInput
- user.press(chip) on the pressable
- every call returns a Promise
- assert on what the screen shows
basics
~20 sCreate 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 sWith 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 linesimport { 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
Recall the pattern: await render, userEvent.setup, await user.type and user.press, then assert on the screen.
Explain what type emits per character, that it appends to existing text, and why each call resolves only after its events and re-renders.
Recognise missing awaits as the source of flaky or leaking tests, enforce them with lint rules, and separate interaction from waiting for async results.
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.