skip to content

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%

answer

  1. it reads accessibility state, not behaviour
  2. aria-disabled or accessibilityState.disabled
  3. a disabled ancestor counts too
  4. Pressable disabled sets accessibilityState
  5. toBeChecked needs Switch or a checkable role

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.

solid answer

~40 s

RNTL's state matchers read the accessibility state React Native exposes, the same state a screen reader announces. `toBeDisabled()` passes when the host element or any ancestor has `aria-disabled` or `accessibilityState.disabled` set, and for a `TextInput` with `editable={false}`. A Submit button that stays pressable but returns early in `onPress`, or is only greyed out with a style, has no disabled state, so the matcher fails; that failure is correct, because a screen-reader user would also hear an enabled button. Passing `disabled={!canSubmit}` to `Pressable` fixes both, since `Pressable` merges `disabled` into the `accessibilityState` of the view it renders. `toBeEnabled()` is the opposite and avoids `.not.toBeDisabled()`. `toBeChecked()` reads a host `Switch`'s `value`, or `aria-checked`/`accessibilityState.checked` on an accessible element with the `checkbox`, `radio` or `switch` role; on anything else it throws.

code

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

test('submit enables once consent is given', async () => {
  const user = userEvent.setup();
  await render(<SurveyScreen />);

  const submit = screen.getByRole('button', { name: 'Submit' });
  const consent = screen.getByRole('checkbox', { name: 'I agree to the terms' });
  expect(submit).toBeDisabled();
  expect(consent).not.toBeChecked();

  await user.press(consent);
  expect(consent).toBeChecked();
  expect(submit).toBeEnabled();
});

go deeper

for a junior

Recall that toBeDisabled and toBeChecked read accessibility state, and that Pressable's disabled prop is what sets it.

for a middle

Explain the sources the matchers read (aria-* props, accessibilityState, editable, ancestors) and why toBeChecked throws without a checkable role.

for a senior

Use a failing state matcher to find real accessibility defects, and separate state assertions from interaction tests of the same control.

for a principal

Push a component library convention where every custom control declares its role and state, so tests and assistive technology read the same truth.

## State matchers read accessibility state **React Native Testing Library (RNTL)** ships a group of state matchers: `toBeDisabled()` and `toBeEnabled()`, `toBeChecked()` and `toBePartiallyChecked()`, `toBeSelected()`, `toBeExpanded()` and `toBeCollapsed()`, and `toBeBusy()`. In v13 they replaced the older `toHaveAccessibilityState()` matcher from `@testing-library/jest-native`. They all read the **accessibility state** of a host element, from either the `aria-*` props or the `accessibilityState` object, which is also what VoiceOver and TalkBack announce. They do **not** try to press the element or look at its handlers. "Disabled" means "declared disabled", not "does nothing when pressed". ## What `toBeDisabled()` reads An element is disabled for RNTL when: 1. it is a host `TextInput` that is not editable (`editable={false}`), or 2. it is a host `Text` with a truthy `disabled` prop, or 3. it has `aria-disabled` or `accessibilityState.disabled` set to true, or 4. **any ancestor** is disabled by one of these rules. Rule 4 matters: a wrapper with `aria-disabled` or `accessibilityState.disabled` around a form section makes every control inside it disabled for the matcher, which was also true of the jest-native matcher of the same name, but not of the removed `toHaveAccessibilityState()`. ## The survey Submit button ```tsx // ❌ looks disabled, is not disabled <Pressable role="button" onPress={canSubmit ? onSubmit : undefined} style={[styles.submit, !canSubmit && styles.dimmed]}> <Text>Submit</Text> </Pressable> ``` `expect(screen.getByRole('button', { name: 'Submit' })).toBeDisabled()` fails here. The button ignores presses and looks dimmed, but nothing declares it disabled, so a screen reader announces an ordinary button. The fix is one prop: ```tsx // ✅ disabled for users, assistive technology and RNTL alike <Pressable role="button" disabled={!canSubmit} onPress={onSubmit} style={[styles.submit, !canSubmit && styles.dimmed]}> <Text>Submit</Text> </Pressable> ``` `Pressable` merges its `disabled` prop into the `accessibilityState` it passes to its host `View`, and core `Button` does the same, so the matcher passes and the press is blocked. ## When several sources disagree A host element can carry both `aria-disabled` and `accessibilityState.disabled`. RNTL reads `aria-disabled` first and falls back to `accessibilityState.disabled`, the same precedence React Native's `Pressable` uses when it builds the state it passes down; on `Pressable`, an explicit `disabled` prop then overrides both. So: - `<Pressable aria-disabled={false} accessibilityState={{ disabled: true }}>` is enabled for the matcher; - `<Pressable disabled accessibilityState={{ disabled: false }}>` is disabled, because `disabled` is merged last; - a `TextInput` with `editable={false}` is disabled even with no accessibility props, mirroring how it behaves for users. ## `toBeEnabled()` and the other opposites `toBeEnabled()` passes exactly when `toBeDisabled()` fails. Both exist so tests read positively: `expect(submit).toBeEnabled()` rather than `expect(submit).not.toBeDisabled()`. `toBeExpanded()` and `toBeCollapsed()` are different: for an element with no expanded state at all, neither passes. ## What `toBeChecked()` reads, and where it throws | Matcher | Reads | Works on | |---|---|---| | `toBeChecked()` | `value` on a `Switch`; otherwise `aria-checked` / `accessibilityState.checked` is `true` | host `Switch`, or accessible element with role `checkbox`, `radio` or `switch` | | `toBePartiallyChecked()` | checked state `'mixed'` | accessible element with role `checkbox` | | `toBeSelected()` | `aria-selected` / `accessibilityState.selected` | any host element | | `toBeBusy()` | `aria-busy` / `accessibilityState.busy` | any host element | The survey's "I agree to the terms" control, built as `<Pressable role="checkbox" aria-checked={agreed}>`, works with `toBeChecked()`. The same control without a role makes the matcher **throw** an error saying it works only on `Switch` instances or accessible elements with a checkable role. That is deliberate: a checkbox without a role is not a checkbox to assistive technology. ## Why this design helps - A test that fails on a styled-only disabled button has found a real accessibility defect. - Asserting through accessibility state keeps tests independent of whether the component uses `aria-*` props or `accessibilityState`; both are read. - Behaviour, whether pressing does anything, belongs in an interaction test: press it and assert that the mocked `submitSurvey` was not called. ## Pitfalls - Asserting `toBeDisabled()` on the `Text` label inside the button: it is disabled only through its ancestor, which works, but asserting on the element with the `button` role states the intent. - Using `toHaveProp('disabled', true)` on the host view: the `disabled` prop of `Pressable` is not forwarded as a prop of that name, so the check fails or tests an implementation detail. - Expecting `toBeChecked()` to work on any `Pressable` that toggles a tick icon.

  • The Submit button is a Pressable with disabled={true}, but the test asserts on the Text 'Submit' found by getByText. Does toBeDisabled pass?
    Yes. `toBeDisabled()` also passes when an ancestor is disabled, and the `Pressable`'s host view carries `accessibilityState.disabled`. It works, but querying the element with the `button` role and asserting on it states the intent more clearly.
  • How do you test that pressing a disabled Submit button does not submit?
    That is behaviour, not state: press it with `userEvent` and assert the mocked `submitSurvey` was not called. The matcher tells you the button is declared disabled; the interaction test tells you the declaration is honoured.
  • Why does toBeChecked throw instead of failing on a Pressable with no role?
    RNTL only accepts a host `Switch` or an accessible element whose role supports a checked state. Without the `checkbox`, `radio` or `switch` role the element is not checkable to assistive technology, so RNTL reports a misuse rather than a plain false result.

saying these in an interview costs you the question

  • toBeDisabled passes when onPress does nothing.
  • A dimmed style is enough for RNTL to treat a button as disabled.
  • toBeDisabled looks only at the element itself, never its ancestors.
  • toBeChecked works on any Pressable that toggles a tick icon.
  • toHaveAccessibilityState is still the matcher for disabled state in RNTL 14.