skip to content

Built-In Assertion Matchers

RNTL ships Jest matchers for visibility, text, state and props, replacing jest-native, and a render wrapper injects providers. Interviewers ask what v14 removed and how suites migrate.

on this pageshow

explore

questions

5

With React Native Testing Library, what is the difference between toBeOnTheScreen and toBeVisible, and which one proves a survey's error message is actually shown?

level: juniorimportance: must knowfreq 58%

answer

  1. mounted is not the same as seen
  2. toBeOnTheScreen: attached to the tree
  3. toBeVisible walks every ancestor
  4. display none, opacity 0, hidden from accessibility
  5. queryBy plus not.toBeOnTheScreen for absence

basics

~20 s

toBeOnTheScreen passes when the element is still attached to the rendered tree; toBeVisible also fails if it or any ancestor has display: 'none', opacity: 0, is hidden from accessibility, or sits in a Modal with visible={false}. Only toBeVisible proves the message is shown.

solid answer

~40 s

Both are RNTL's built-in matchers for host elements. `toBeOnTheScreen()` checks one thing: the element is still attached to the tree that `render` produced, so it fails after an unmount. `toBeVisible()` walks the element and every ancestor and fails if any of them has `display: 'none'` or `opacity: 0` in its flattened style, is hidden from accessibility (`aria-hidden`, `accessibilityElementsHidden`, `importantForAccessibility="no-hide-descendants"`), or is a `Modal` with `visible={false}`. In a survey screen that keeps the error `Text` mounted and toggles its opacity, `getByText(...)` finds it and `toBeOnTheScreen()` passes even before the user presses Submit; only `toBeVisible()` tells shown from hidden. To prove the message is not rendered at all, use `expect(screen.queryByText('Please answer question 3')).not.toBeOnTheScreen()`.

code

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

test('shows the required-answer error only after submit', async () => {
  const user = userEvent.setup();
  await render(<SurveyScreen />);

  const error = screen.getByText('Please answer question 3');
  expect(error).toBeOnTheScreen(); // true before and after: it is always mounted
  expect(error).not.toBeVisible(); // opacity: 0 before submit

  await user.press(screen.getByRole('button', { name: 'Submit' }));
  expect(error).toBeVisible();
});

go deeper

for a junior

Recall that toBeOnTheScreen means attached to the tree and toBeVisible means not hidden by display none, opacity 0, accessibility props or a closed Modal.

for a middle

Explain how toBeVisible walks ancestors, why opacity 0 still lets queries find an element, and how to assert absence with queryBy and not.toBeOnTheScreen.

for a senior

Spot false-positive tests that assert mounting when the requirement is visibility, and choose assertions that survive refactors of how a component hides content.

for a principal

Set suite guidance on which user-visible claims each matcher proves, so reviews catch assertions that pass regardless of the behaviour under test.

## Two matchers, two questions **React Native Testing Library (RNTL)** has shipped its own Jest matchers since v13; importing anything from `@testing-library/react-native` registers them, with no extra setup file. Two of them are easy to confuse because both sound like "the user can see it": - **`toBeOnTheScreen()`** answers "is this element still part of the rendered tree?" RNTL walks up from the element to its root and checks that the root is the current screen's container. An element you captured earlier fails it after it unmounts. - **`toBeVisible()`** answers "would this element be shown?" It inspects the element and every ancestor for the ways React Native hides content. ## What makes `toBeVisible()` fail The element counts as not visible if the element itself **or any ancestor**: | Condition | Example | |---|---| | `display: 'none'` in its flattened style | a collapsed section | | `opacity: 0` in its flattened style | a fade-out that ended at zero | | hidden from accessibility | `aria-hidden`, `accessibilityElementsHidden` (iOS prop), `importantForAccessibility="no-hide-descendants"` (Android prop) | | a host `Modal` with `visible={false}` | a closed dialog that is still mounted | Styles are flattened with `StyleSheet.flatten`, so an array style such as `[styles.error, { opacity: 0 }]` is handled. Content React keeps hidden, such as a suspended subtree or React 19.2's `<Activity mode="hidden">`, is given `display: 'none'` by RNTL 14 and so is not visible either. What it does **not** check: whether the element is scrolled out of a `ScrollView`, clipped by `overflow: 'hidden'`, positioned off-screen, or the same colour as its background. There is no layout in a Jest render, so "visible" means "not hidden by these props and styles". ## Why opacity is treated differently from display The two style checks are not symmetric across RNTL. `display: 'none'` removes an element from layout and from assistive technology, so RNTL treats it as **inaccessible**: default queries skip it and `toBeVisible()` fails. `opacity: 0` only makes pixels transparent; RNTL's source notes that it is not treated as inaccessible on iOS, so default queries **still find** an element at zero opacity while `toBeVisible()` reports it hidden. That asymmetry is exactly what produces the false positive below. ## Matchers and `null` RNTL's matchers expect a host element. `toBeOnTheScreen()` and `toBeVisible()` accept `null` only under `.not`, which is what makes `queryBy*` plus `.not.toBeOnTheScreen()` work. State matchers such as `toBeDisabled()` throw on `null` in both directions, so pair them with `getBy*`. ## The survey false positive The survey screen keeps its error message mounted and fades it in: ```tsx <Text style={[styles.error, { opacity: showError ? 1 : 0 }]}> Please answer question 3 </Text> ``` Before Submit is pressed: 1. `screen.getByText('Please answer question 3')` finds the element, because `opacity: 0` does not hide it from RNTL's default queries (`display: 'none'` and the accessibility-hiding props do). 2. `toBeOnTheScreen()` passes: the element is attached. 3. `toBeVisible()` fails: its own style has `opacity: 0`. A test that asserts `toBeOnTheScreen()` after pressing Submit therefore passes whether or not validation works. `toBeVisible()` is the assertion that matches the requirement. ## Asserting absence There are two different "not shown" claims: - **Not rendered at all** (conditional rendering, `{showError && <Text>…</Text>}`): `expect(screen.queryByText('Please answer question 3')).not.toBeOnTheScreen()`. `queryBy*` returns `null` instead of throwing, and `toBeOnTheScreen` accepts `null` under `.not`. - **Rendered but hidden**: find the element, then `expect(element).not.toBeVisible()`. If it is hidden with `display: 'none'` or an accessibility prop, the default query will not find it; query with `{ includeHiddenElements: true }` first. Note that `expect(null).not.toBeVisible()` also passes, so on a `queryBy*` result it cannot distinguish "absent" from "hidden". Pick the matcher that states the claim you mean. ## Common mistakes - Using `toBeOnTheScreen()` as a visibility check for content toggled with style. - Writing `expect(screen.getByText(...)).not.toBeOnTheScreen()`: `getBy*` throws before the matcher runs when the element is absent. - Asserting `toHaveStyle({ opacity: 1 })` instead of `toBeVisible()`: it inspects only the element's own style, not ancestors, accessibility hiding or a closed `Modal`. - Passing a composite component instance or a non-element: RNTL's matchers accept host elements only, which is all its queries return.

  • Why not assert expect(error).toHaveStyle({ opacity: 1 }) instead of toBeVisible?
    `toHaveStyle` compares only the style keys you pass against the element's own flattened style. It ignores a parent with `display: 'none'` or `opacity: 0`, accessibility hiding and a closed `Modal`, and it couples the test to how the component hides the text. `toBeVisible()` states the user-level claim and survives a refactor from opacity to conditional styling.
  • A test captured an element, then the component re-rendered and replaced it. Why does toBeOnTheScreen now fail on the old reference?
    The captured instance was detached from the tree when React replaced it, so walking up from it no longer reaches the screen's container. Re-query after the update, for example with `getByText` again, instead of holding element references across state changes.

saying these in an interview costs you the question

  • toBeOnTheScreen checks that the element is visible to the user.
  • toBeVisible checks whether the element is inside the scroll viewport.
  • An element with opacity 0 cannot be found by getByText.
  • expect(screen.getByText(x)).not.toBeOnTheScreen() proves x is absent.
  • RNTL's matchers need an extend-expect import in the Jest setup file.
open as a page

In React Native Testing Library, why can toBeDisabled fail on a survey Submit button that ignores presses, and what state do toBeDisabled and toBeChecked read?

level: middleimportance: must knowfreq 45%

basics

~20 s

toBeDisabled reads the host element's aria-disabled or accessibilityState.disabled, and its ancestors', not whether onPress does anything. A button that just ignores presses is enabled to RNTL. toBeChecked reads a Switch's value, or aria-checked on a checkbox, radio or switch role.

open as a page

In React Native Testing Library 14, how do you give every rendered survey screen its providers, and what does the render wrapper option do that inline wrapping does not?

level: middleimportance: should knowfreq 40%

basics

~10 s

Pass providers through render's wrapper option, a component that receives the tested element as children, or build an async renderWithProviders helper around it. RNTL re-applies the wrapper on every rerender, so providers survive updates.

open as a page

In React Native Testing Library, does toHaveTextContent match the whole text or a substring, and how does toHaveAccessibleName compute what it compares?

level: middleimportance: should knowfreq 32%

basics

~20 s

toHaveTextContent compares the element's full concatenated text, trimmed and whitespace-collapsed, exactly by default; use a RegExp or { exact: false } for a partial match. toHaveAccessibleName compares the computed name: labelledby, then label, then text content.

open as a page

Upgrading a React Native suite from React Native Testing Library 13 to 14, what breaks, and what do the rntl-v14 codemods fix for you?

level: seniorimportance: should knowfreq 38%

basics

~10 s

RNTL 14 needs React 19 and React Native 0.78+, makes render, renderHook, fireEvent and act async, swaps react-test-renderer for test-renderer and removes update, UNSAFE_root, UNSAFE_*ByType/ByProps, concurrentRoot and createNodeMock. Codemods update dependencies and add awaits.

open as a page