In React Native Testing Library, does toHaveTextContent match the whole text or a substring, and how does toHaveAccessibleName compute what it compares?
answer
- exact by default, after normalizing
- trim and collapse whitespace
- regex or exact: false for partial
- children's text joined with no separator
- label beats text content for names
basics
~20 stoHaveTextContent 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.
solid answer
~40 s`toHaveTextContent(text, options)` collects the text of the element and all its descendants, joining the pieces with no separator, normalizes it (trim, collapse runs of whitespace) and, for a string, requires **equality** by default. So `toHaveTextContent('required')` fails on "Question 3 is required"; use `/required/` or `{ exact: false }`, which is a case-insensitive substring match. Because pieces are joined with no separator, a `View` holding `<Text>Error:</Text><Text>Required</Text>` has the text content `Error:Required`. `toHaveAccessibleName(name)` compares something else: the accessible name a screen reader would read, taken from `aria-labelledby`/`accessibilityLabelledBy`, then `aria-label`/`accessibilityLabel`, then `alt` on an `Image` or the `placeholder` of a `TextInput`, and only then descendant text joined with spaces. An icon-only Next button labelled "Next question" has that name even though it has no text content.
code
typescript · 14 linesimport { render, screen } from '@testing-library/react-native';
test('error text and button names', async () => {
await render(<SurveyScreen initialErrors={{ q3: 'required' }} />);
const error = screen.getByTestId('q3-error');
expect(error).toHaveTextContent('Question 3 is required');
expect(error).toHaveTextContent(/is required$/);
expect(error).toHaveTextContent('required', { exact: false });
const next = screen.getByRole('button', { name: 'Next question' });
expect(next).toHaveAccessibleName('Next question');
expect(next).toHaveTextContent('');
});go deeper
Recall that toHaveTextContent is an exact match by default and that a regex or exact: false makes it partial.
Explain the normalization, the no-separator join of descendant text, and the precedence RNTL uses to compute an accessible name.
Choose between text content and accessible name based on what the user or assistive technology experiences, and update expectations after the v14 accessible-name changes.
Encourage assertions on accessible names for interactive controls so the suite doubles as a regression check on labelling.
## Two text-shaped matchers, two different sources **React Native Testing Library (RNTL)** has two built-in matchers that compare strings against an element: `toHaveTextContent()` and `toHaveAccessibleName()`. They accept the same kind of argument, a string or `RegExp` plus the text match options `exact` and `normalizer`, but they compare against different things, and the default match is stricter than many people expect. ## How `toHaveTextContent()` builds the text RNTL walks the element's children recursively and concatenates every string it finds, **with no separator**. Then it applies the default normalizer: 1. **trim** leading and trailing whitespace; 2. **collapse** every run of whitespace, including newlines, to a single space. Examples from the survey screen: - `<Text>Question {n} is required</Text>` with `n = 3` has children `"Question "`, `"3"`, `" is required"` and the text content `Question 3 is required`. - `<View><Text>Error:</Text><Text>Required</Text></View>` asserted on the `View` has the text content `Error:Required`, because sibling `Text` elements are joined without a space. ## Exact by default With a string argument, the comparison is **full equality** after normalization: | Assertion on "Question 3 is required" | Result | |---|---| | `toHaveTextContent('Question 3 is required')` | passes | | `toHaveTextContent('required')` | fails: not the whole text | | `toHaveTextContent('required', { exact: false })` | passes: case-insensitive substring | | `toHaveTextContent(/required$/)` | passes: regex test | | `toHaveTextContent('question 3 is required')` | fails: exact match is case-sensitive | A `RegExp` is tested against the normalized text, so anchors and flags behave as usual. A custom `normalizer` replaces the default one entirely; `getDefaultNormalizer({ trim: false })` builds a variant of it. ## How `toHaveAccessibleName()` computes the name The accessible name is what VoiceOver or TalkBack reads for the element. RNTL 14 computes it in this order: 1. `aria-labelledby` / `accessibilityLabelledBy`: the text of the referenced elements (an array in `accessibilityLabelledBy` is joined with spaces); 2. `aria-label` / `accessibilityLabel`; 3. `alt` on an `Image`, or the `placeholder` of the `TextInput` being asserted on; 4. otherwise the names of its descendants, joined with **spaces** between separate elements. So the survey's icon-only Next button, `<Pressable role="button" accessibilityLabel="Next question"><ArrowIcon /></Pressable>`, has the accessible name `Next question` and empty text content. A `Pressable` holding a `Text` "Submit" and an `accessibilityLabel` of "Submit survey" has the name `Submit survey`, because the label takes precedence over the text. With no argument, `expect(element).toHaveAccessibleName()` asserts only that **some** name exists, a useful check that an icon button is labelled at all. ## Options shared with the text queries The `exact` and `normalizer` options are the same **text match options** the `*ByText`, `*ByLabelText` and `*ByHintText` queries take, so a rule learned for queries carries over: - `exact: false` means case-insensitive substring; - a `RegExp` ignores `exact`; - the default normalizer trims and collapses whitespace. One neighbour matcher is easy to confuse with these. A `TextInput`'s typed value is **not** text content: its children hold no strings, so `toHaveTextContent('Alice')` on the name field fails. Use `toHaveDisplayValue('Alice')`, which reads `value`, then the value typed through `userEvent`, then `defaultValue`, and throws on anything that is not a host `TextInput`. ## Which one to use - The words the user **reads**, such as an error message: `toHaveTextContent()`. - The words assistive technology **announces**, especially for icon buttons and images: `toHaveAccessibleName()`. - `getByRole('button', { name: 'Next question' })` already matches on the same computed name, so querying by role and name often makes a separate name assertion unnecessary. ## Changes in RNTL 14 worth knowing The v14 release reworked accessible-name calculation to match React Native more closely: labels take precedence over text content, `Image` `alt` counts as a label, a root `TextInput` can use its `placeholder`, child names are joined with spaces, and child `TextInput` placeholders no longer contribute to a parent's name. Tests that passed in v13 because of a looser fallback may need their expected names updated. ## Pitfalls - Expecting `toHaveTextContent('required')` to be a "contains" check. - Asserting text content on a container of sibling `Text` elements and forgetting that no space is inserted. - Asserting the visible text of a button whose `accessibilityLabel` differs, then being surprised that `getByRole(..., { name })` needs the label.
- Why does toHaveTextContent('') pass on the icon-only Next button?Its children contain no strings, so the collected text content is the empty string, and an exact match against an empty string passes. The button still has an accessible name because its `accessibilityLabel` supplies one.
- How do you match text where whitespace inside the string matters, such as two spaces in a code?Pass a custom normalizer, for example `getDefaultNormalizer({ collapseWhitespace: false })`, in the options argument. The default normalizer collapses runs of whitespace before comparing, so two spaces and one space would otherwise be treated as equal.
saying these in an interview costs you the question
- toHaveTextContent('required') passes when the text merely contains 'required'.
- Text content of sibling Text elements is joined with spaces.
- An exact toHaveTextContent match ignores letter case.
- An icon button with an accessibilityLabel has no accessible name.
- Visible text always wins over accessibilityLabel in the accessible name.