skip to content

In React Native Testing Library, which events does userEvent.type emit on a TextInput, and how do clear, paste and the type options change that?

level: middleimportance: should knowfreq 30%

answer

  1. focus, per-key events, endEditing, blur
  2. keyPress, change, changeText, selectionChange
  3. type appends, paste replaces
  4. maxLength and editable are honoured
  5. skipPress, skipBlur, submitEditing

basics

~20 s

userEvent.type fires focus, then keyPress, change, changeText and selectionChange per character, then endEditing and blur, appending text and honouring maxLength. clear empties the field, paste replaces it in one change, and skipPress, skipBlur and submitEditing adjust the edges.

solid answer

~40 s

React Native Testing Library's `user.type(input, text)` works only on a host `TextInput`. It presses in, fires `focus` and presses out, then for each character fires `keyPress`, `change`, `changeText` and `selectionChange`, plus `contentSizeChange` when `multiline` is set, and finishes with `endEditing` and `blur`. Text is appended to the current value; a key that would exceed `maxLength` fires only `keyPress`, and with `editable={false}` nothing fires. Options: `skipPress` drops press in and out, `skipBlur` leaves the field focused, `submitEditing: true` fires `submitEditing` before leaving. `{Enter}` and `{Backspace}` are special keys. `user.clear(input)` focuses, selects all, sends one Backspace and blurs. `user.paste(input, text)` focuses, selects all and replaces the value in a single `change` and `changeText`, without key presses.

code

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

test('submits the search from the keyboard', async () => {
  const onSearch = jest.fn();
  await render(<MovieSearch onSearch={onSearch} onQueryChange={jest.fn()} />);
  const user = userEvent.setup();
  const input = screen.getByLabelText('Search movies');

  await user.type(input, 'dune');
  await user.clear(input);
  await user.type(input, 'arrival', { submitEditing: true });

  expect(onSearch).toHaveBeenLastCalledWith('arrival'); // from onSubmitEditing
});

test('receives a pasted query in one change', async () => {
  const onQueryChange = jest.fn();
  await render(<MovieSearch onSearch={jest.fn()} onQueryChange={onQueryChange} />);
  const user = userEvent.setup();

  await user.paste(screen.getByLabelText('Search movies'), 'Blade Runner');

  expect(onQueryChange).toHaveBeenCalledTimes(1); // no per-key changes
  expect(onQueryChange).toHaveBeenCalledWith('Blade Runner');
});

go deeper

for a junior

Recall that type fires events per character and blurs at the end, that clear empties the field and that paste inserts text at once.

for a middle

Explain the full event order, appending versus replacing, maxLength and editable handling, and what each type option changes.

for a senior

Pick the helper that exercises the handler under test, such as submitEditing for keyboard submit or paste for whole-string input, and explain the fidelity gap to fireEvent.changeText.

for a principal

Decide how much keyboard fidelity component tests must provide versus device-level tests, and document helper conventions for forms across the codebase.

## Why the exact sequence matters A search field in a movie app often reacts to more than the final text: it validates per keystroke, debounces requests on `onChangeText`, submits on the keyboard's return key through `onSubmitEditing`, and clears suggestions on `onBlur`. A test that only sets the final value misses all of that. React Native Testing Library's `userEvent` text helpers reproduce the events React Native's `TextInput` emits, so those handlers run in the same order as on a device. All three helpers accept **only a host `TextInput`** and throw for other elements. All three do nothing when the input has `editable={false}`. ## type: the full sequence `await user.type(input, 'dune', options)` emits: 1. **Entering**: `pressIn`, `focus`, `pressOut` (the press events are skipped with `skipPress: true`); 2. **Per character**: `keyPress`, `change`, `changeText`, `selectionChange`, plus `contentSizeChange` for a `multiline` input; 3. **Leaving**: `submitEditing` when `submitEditing: true` is passed, then `endEditing` and `blur` (both skipped with `skipBlur: true`). Rules worth knowing: - text is **appended** to the current `value` or `defaultValue`; - **`maxLength` is honoured**: a key that would exceed it emits only `keyPress`, matching iOS behaviour; - `{Enter}` inserts a newline and `{Backspace}` deletes the last character; write `{{` for a literal brace; - the pause between events comes from `userEvent.setup({ delay })`, default `0`. ## clear and paste | Helper | Events | Effect on value | |---|---|---| | `user.clear(input)` | `focus`, `selectionChange` (select all), `keyPress` Backspace, `change`, `changeText`, `selectionChange`, `endEditing`, `blur` | Becomes empty | | `user.paste(input, 'blade runner')` | `focus`, `selectionChange` (select all), `change`, `changeText`, `selectionChange`, `contentSizeChange` if multiline, `endEditing`, `blur` | **Replaced** with the pasted text in one change, no key presses | | `user.type(input, 'blade runner')` | per-character events as above | **Appended**, one character at a time | `paste` is the right tool for a handler that must cope with a whole string arriving at once, such as a field that trims pasted whitespace. Because it emits no `keyPress`, logic tied to key presses does not run. ## Choosing the options - **`submitEditing: true`** tests a search that runs when the user taps the keyboard's return key via `onSubmitEditing`. - **`skipBlur: true`** keeps the field focused, for example to assert that suggestions stay open while the user is still typing. - **`skipPress: true`** suits an input focused programmatically, where no touch happened. ## Compared with fireEvent.changeText `fireEvent.changeText(input, 'dune')` calls `onChangeText` once with the final string. It fires no `focus`, `keyPress` or `blur`, ignores `maxLength`, and walks up the tree for a handler. It is acceptable for a quick unit check of a handler, but it does not reproduce typing.

  • Why does a search field with maxLength={20} not receive the 21st character from user.type?
    `user.type` checks each proposed value against `maxLength`. When a key would exceed it, only `keyPress` fires and no `change` or `changeText`, matching iOS. `fireEvent.changeText` has no such check and would pass a longer string straight to the handler.
  • How do you test a handler that runs on blur without the field leaving focus in between keystrokes?
    `user.type` blurs only once, at the end, so per-keystroke logic runs while focused and `onBlur` runs after the last character. If the test needs the field to stay focused, pass `skipBlur: true` and trigger the blur later with `fireEvent(input, 'blur')`.

saying these in an interview costs you the question

  • user.type sets the final value in a single changeText call.
  • user.paste appends the pasted text to the existing value.
  • user.type fires submitEditing by default.
  • user.type ignores maxLength, like fireEvent.changeText.
  • user.clear works on any element with text children.