skip to content

A React Native Testing Library query fails although screen.debug() prints the element; which accessibility rules can hide it, and what does includeHiddenElements change?

level: seniorimportance: should knowfreq 22%

answer

  1. queries skip hidden elements by default
  2. display none, aria-hidden, no-hide-descendants
  3. accessibilityElementsHidden on an ancestor
  4. a sibling with accessibilityViewIsModal
  5. opacity 0 does not hide

basics

~20 s

RNTL queries skip elements hidden from accessibility by default: display none, aria-hidden, accessibilityElementsHidden, importantForAccessibility no-hide-descendants on the element or an ancestor, or a modal sibling. includeHiddenElements: true, or configure's defaultIncludeHiddenElements, makes queries match them anyway.

solid answer

~30 s

`screen.debug()` prints the whole host tree, but React Native Testing Library queries filter out elements **hidden from accessibility**, because users could not perceive them. An element counts as hidden when it or any ancestor has `display: 'none'`, `aria-hidden` true, `accessibilityElementsHidden` true, or `importantForAccessibility="no-hide-descendants"`, or when it has a sibling host element with `aria-modal` or `accessibilityViewIsModal` true. `opacity: 0`, `accessible={false}` and `importantForAccessibility="no"` do not hide. Passing `{ includeHiddenElements: true }` (alias `hidden`) makes one query match hidden elements; `configure({ defaultIncludeHiddenElements: true })` changes the default. I use that per query to prove something is hidden, not globally to make tests pass.

code

tsx · 24 lines
tsx
import { Text, TextInput, View } from 'react-native';
import { isHiddenFromAccessibility, render, screen } from '@testing-library/react-native';

function LoginWithExpiredBanner() {
  return (
    <View>
      <View>
        <TextInput accessibilityLabel="Password" secureTextEntry />
      </View>
      <View accessibilityViewIsModal>
        <Text>Session expired</Text>
      </View>
    </View>
  );
}

test('the form behind a modal sibling is hidden', async () => {
  await render(<LoginWithExpiredBanner />);

  expect(screen.queryByLabelText('Password')).not.toBeOnTheScreen();

  const field = screen.getByLabelText('Password', { includeHiddenElements: true });
  expect(isHiddenFromAccessibility(field)).toBe(true);
});

go deeper

for a junior

Recall that RNTL queries skip elements users cannot perceive, such as display none, and that screen.debug() shows more than queries can find.

for a middle

List the hiding conditions, including ancestors and modal siblings, the ones that do not hide, and the per-query and global switches.

for a senior

Diagnose a failing query from debug output, decide whether the hiding is intended or a defect, and fix the test flow rather than widening queries.

for a principal

Set a policy that keeps hidden-element filtering on across the suite, so component tests act as an early accessibility gate for the app.

## The symptom A login test calls `screen.getByLabelText('Password')` and gets `Unable to find an element with accessibility label: Password`, yet `screen.debug()` clearly prints a `TextInput` with that label. The two disagree because `debug()` prints every host element, while **queries exclude elements hidden from accessibility by default**. The reasoning is that a test should find only what a user could perceive. ## What counts as hidden RNTL walks from the element up through its ancestors. The element is hidden if any node on that path meets one of these conditions: | Condition | Where it comes from | |---|---| | `style` flattens to `display: 'none'` | layout | | `aria-hidden={true}` | cross-platform accessibility prop | | `accessibilityElementsHidden={true}` | iOS accessibility prop | | `importantForAccessibility="no-hide-descendants"` | Android accessibility prop | | a **sibling** host element has `aria-modal` or `accessibilityViewIsModal` true | iOS modal semantics | RNTL applies all of these in tests regardless of the platform the prop targets. Equally important is what does **not** hide an element: - `opacity: 0`, since it is not treated as inaccessible; - `accessible={false}`, `role="none"` or `accessibilityRole="none"`; - `importantForAccessibility="no"`; only `"no-hide-descendants"` counts as hiding. The helper `isHiddenFromAccessibility(element)`, also exported as `isInaccessible`, runs the same check and is handy when diagnosing. ## A typical login-screen case The app shows a "Session expired" panel over the form, rendered as a sibling `View` with `accessibilityViewIsModal`. While it is present, the form container is a sibling of a modal view, so every field inside it counts as hidden and `getByLabelText('Password')` fails. That is correct: a VoiceOver user cannot reach the form either. The fix is in the test flow: dismiss the panel first, then query the form. ## includeHiddenElements and its default - **Per query**: `screen.getByText('Session expired', { includeHiddenElements: true })` also matches hidden elements. `hidden: true` is an alias kept for compatibility with the web Testing Library. - **Globally**: `configure({ defaultIncludeHiddenElements: true })` changes the default for every query; `defaultHidden` is its alias. The shipped default is `false`. ## How to use it well 1. When a query unexpectedly fails, run `screen.debug()` and check the element's ancestors for the conditions in the table. 2. Decide whether the hiding is **intended**, such as a modal or a collapsed section, or a **bug**, such as a stale `aria-hidden` left on a container. 3. If intended, change the test flow so the element is visible before querying it. 4. Use `includeHiddenElements: true` in a single query when the assertion is precisely that something exists but is hidden, typically together with `isHiddenFromAccessibility`. 5. Do not flip the global default to silence failures: it makes queries find elements users cannot reach and hides real accessibility regressions. ## Why this design is valuable Hidden-element filtering makes component tests behave like an assistive-technology user. A form accidentally covered by an invisible modal sibling, a section left with `aria-hidden` after an animation, or a screen with `display: 'none'` still mounted all break the test, which is exactly the signal a team wants before release.

  • Why does a Text with opacity: 0 still match getByText?
    RNTL does not treat `opacity: 0` as hidden from accessibility, because the element can still be reached by assistive technology. Only `display: 'none'`, the hiding accessibility props, and modal siblings exclude an element. To hide something from both users and queries, use one of those.
  • When is it reasonable to set defaultIncludeHiddenElements to true globally?
    Almost never in an app suite. It can suit a library that renders into containers it does not control, but for app tests it makes queries find elements users cannot reach and masks accessibility regressions. Prefer the per-query option where a hidden element is the point of the assertion.

saying these in an interview costs you the question

  • screen.debug() output lists only elements that queries can find.
  • An element with opacity: 0 is excluded from RNTL queries.
  • accessible={false} hides an element and its children from queries.
  • Setting includeHiddenElements globally is the standard fix for failing queries.
  • A modal sibling has no effect on queries in the test environment.