116

Current Working Solution

Using this html:

<p data-testid="foo">Name: <strong>Bob</strong> <em>(special guest)</em></p>

I can use the React Testing Library getByTestId method to find the textContent:

expect(getByTestId('foo').textContent).toEqual('Name: Bob (special guest)')

Is there a better way?

I would like to simply use this html:

<p>Name: <strong>Bob</strong> <em>(special guest)</em></p>

And use React Testing Library's getByText method like this:

expect(getByText('Name: Bob (special guest)')).toBeTruthy()

But this does not work.

So, the question…

Is there a simpler way to use React Testing Library to find strings of text content with the tags striped out?

3
  • This even work if the p has property simply as id as can be for FormHelperText MUI Commented Mar 17, 2022 at 14:47
  • 1
    Great question, thanks. I'm surprised it didn't get more attention. Commented Nov 5, 2024 at 23:58
  • insufficient rep to answer, but I'm surprised nobody's suggested using this instead expect(getByTestId('foo')).toContainText('Name: Bob (special guest)') Commented Aug 27 at 13:10

9 Answers 9

83

For substring matching, you can pass { exact: false }:

https://testing-library.com/docs/dom-testing-library/api-queries#textmatch

const el = getByText('Name:', { exact: false })
expect(el.textContent).toEqual('Name: Bob (special guest)');
Sign up to request clarification or add additional context in comments.

2 Comments

This is by far the easiest solution. In this case something like: expect(getByText('Name:', { exact: false }).textContent).toEqual('Name: Bob (special guest)');
Great solution that doesn't involve testId matchers!
54

Update 2

Having used this many times, I've created a helper. Below is an example test using this helper.

Test helper:

// withMarkup.ts
import { MatcherFunction } from '@testing-library/react'

type Query = (f: MatcherFunction) => HTMLElement

const withMarkup = (query: Query) => (text: string): HTMLElement =>
  query((content: string, node: HTMLElement) => {
    const hasText = (node: HTMLElement) => node.textContent === text
    const childrenDontHaveText = Array.from(node.children).every(
      child => !hasText(child as HTMLElement)
    )
    return hasText(node) && childrenDontHaveText
  })

export default withMarkup

Test:

// app.test.tsx
import { render } from '@testing-library/react'
import App from './App'
import withMarkup from '../test/helpers/withMarkup'

it('tests foo and bar', () => {
  const { getByText } = render(<App />)
  const getByTextWithMarkup = withMarkup(getByText)
  getByTextWithMarkup('Name: Bob (special guest)')
})

Update 1

Here is an example where a new matcher getByTextWithMarkup is created. Note that this function extends getByText in a test, thus it must be defined there. (Sure the function could be updated to accept getByText as a parameter.)

import { render } from "@testing-library/react";
import "jest-dom/extend-expect";

test("pass functions to matchers", () => {
  const Hello = () => (
    <div>
      Hello <span>world</span>
    </div>
  );
  const { getByText } = render(<Hello />);

  const getByTextWithMarkup = (text: string) => {
    getByText((content, node) => {
      const hasText = (node: HTMLElement) => node.textContent === text
      const childrenDontHaveText = Array.from(node.children).every(
        child => !hasText(child as HTMLElement)
      )
      return hasText(node) && childrenDontHaveText
    })
  }

  getByTextWithMarkup('Hello world')
})

Here is a solid answer from the 4th of Five Things You (Probably) Didn't Know About Testing Library from Giorgio Polvara's Blog:


Queries accept functions too

You have probably seen an error like this one:

Unable to find an element with the text: Hello world. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Usually, it happens because your HTML looks like this:

<div>Hello <span>world</span></div>

The solution is contained inside the error message: "[...] you can provide a function for your text matcher [...]".

What's that all about? It turns out matchers accept strings, regular expressions or functions.

The function gets called for each node you're rendering. It receives two arguments: the node's content and the node itself. All you have to do is to return true or false depending on if the node is the one you want.

An example will clarify it:

import { render } from "@testing-library/react";
import "jest-dom/extend-expect";

test("pass functions to matchers", () => {
  const Hello = () => (
    <div>
      Hello <span>world</span>
    </div>
  );
  const { getByText } = render(<Hello />);

  // These won't match
  // getByText("Hello world");
  // getByText(/Hello world/);

  getByText((content, node) => {
    const hasText = node => node.textContent === "Hello world";
    const nodeHasText = hasText(node);
    const childrenDontHaveText = Array.from(node.children).every(
      child => !hasText(child)
    );

    return nodeHasText && childrenDontHaveText;
  });
});

We're ignoring the content argument because in this case, it will either be "Hello", "world" or an empty string.

What we are checking instead is that the current node has the right textContent. hasText is a little helper function to do that. I declared it to keep things clean.

That's not all though. Our div is not the only node with the text we're looking for. For example, body in this case has the same text. To avoid returning more nodes than needed we are making sure that none of the children has the same text as its parent. In this way we're making sure that the node we're returning is the smallest—in other words the one closes to the bottom of our DOM tree.


Read the rest of Five Things You (Probably) Didn't Know About Testing Library

8 Comments

I'm not understanding why this is necessary since, as per the testing-library docs, getByText looks for the textContent already, and hence getByText("Hello World") should work, right (although it seems not to for some reason)?
That's because getByText is using the getNodeText helper which is looking for the textContent property of each text node. In your case, the only text nodes that are direct children of <p> are Name: and ` `. I'm not sure why RTL decides not to look for text nodes that are children of children in a recursive way. Maybe it's for performance reasons but that's the way it is. Maybe @kentcdodds can provide some more insights on this
Thinking about it RTL doesn't look for children of children because otherwise this getAllByText(<div><div>Hello</div></div>, 'Hello') would return two results. It makes sense
Nice answer. I also had to catch the exception thrown by getByText and re-throw another message with text, because it's not included in the error message when using a custom matcher. I think it would be great to have this helper included by default on @testing-library.
@PaoloMoretti - 👍🏼 Can you please post the solution you describe as another answer to this question?
|
46

If you are using testing-library/jest-dom in your project. You can also use toHaveTextContent.

expect(getByTestId('foo')).toHaveTextContent('Name: Bob (special guest)')

if you need a partial match, you can also use regex search patterns

expect(getByTestId('foo')).toHaveTextContent(/Name: Bob/)

Here's a link to the package

3 Comments

This is an assertion though, you first need something else to find the element. OP is looking for a query to find the element
This answer could be adapted with this assertion like const el = getByText('Name:', { exact: false }); expect(el).toHaveTextContent('Name: Bob (special guest)');
That's right. I hadn't realized that the OP was looking for a way to query an element by the text content.
15

The existing answers are outdated. The new *ByRole query supports this:

getByRole('button', {name: 'Bob (special guest)'})

6 Comments

How would that work in this case, where there is no 'button'?
@jarthur - Use the accessibility DOM to inspect the element you're targeting to determine its role.
I'm wondering in OP's context, there is no obvious role. Unless p has a default role?
@jarthur - <p> has a role of paragraph. However, oddly getByRole ignores paragraphs. So you need to use a different wrapper element that getByRole currently supports like a heading or a region.
@CoryHouse - how about if there are no element with accessible role and only elements like this: <div><b>[AL]</b> Albania</div> <div><b>[DZ]</b> Algeria</div> How can I query the first element by its text?
|
8

Update

The solution below works but for some cases, it might return more than one result. This is the correct implementation:

getByText((_, node) => {
  const hasText = node => node.textContent === "Name: Bob (special guest)";
  const nodeHasText = hasText(node);
  const childrenDontHaveText = Array.from(node.children).every(
    child => !hasText(child)
  );

  return nodeHasText && childrenDontHaveText;
});

You can pass a method to getbyText:

getByText((_, node) => node.textContent === 'Name: Bob (special guest)')

You could put the code into a helper function so you don't have to type it all the time:

  const { getByText } = render(<App />)
  const getByTextWithMarkup = (text) =>
    getByText((_, node) => node.textContent === text)

5 Comments

This solution can work in simple scenarios, however if it produced the error "Found multiple elements with the text: (_, node) => node.textContent === 'Name: Bob (special guest)'", then try the other answer's solution which checks child nodes as well.
Agree, the solution is actually taken from my blog :D
Thanks for your insight with this Giorgio. I keep coming back to these answers as I find I need these solutions in new tests. :)
Is there a way to modify this idea to work with cypress-testing-library?
Compiler warning: [node] Object is possibly null at Array.from(node.children.
2
getByText('Hello World'); // full string match
getByText('llo Worl', { exact: false }); // substring match
getByText('hello world', { exact: false }); // ignore case-sensitivity

source: https://testing-library.com/docs/react-testing-library/cheatsheet/#queries

1 Comment

This does not work for <div>hello<span> world</span></div>, which is what the OP is asking.
1

To avoid matching multiple elements, for some use cases simply only returning elements that actually have text content themselves, filters out unwanted parents just fine:

expect(
  // - content: text content of current element, without text of its children
  // - element.textContent: content of current element plus its children
  screen.getByText((content, element) => {
    return content !== '' && element.textContent === 'Name: Bob (special guest)';
  })
).toBeInTheDocument();

The above requires some content for the element one is testing, so works for:

<div>
  <p>Name: <strong>Bob</strong> <em>(special guest)</em></p>
</div>

...but not if <p> has no text content of its own:

<div>
  <p><em>Name: </em><strong>Bob</strong><em> (special guest)</em></p>
</div>

So, for a generic solution the other answers are surely better.

Comments

1

The other answers ended up in type errors or non-functional code at all. This worked for me.

Note: I'm using screen.* here

import React from 'react';
import { screen } from '@testing-library/react';

/**
 * Preparation: generic function for markup 
 * matching which allows a customized 
 * /query/ function.
 **/
namespace Helper {
    type Query = (f: MatcherFunction) => HTMLElement

    export const byTextWithMarkup = (query: Query, textWithMarkup: string) => {
        return query((_: string, node: Element | null) => {
            const hasText = (node: Element | null) => !!(node?.textContent === textWithMarkup);
            const childrenDontHaveText = node ? Array.from(node.children).every(
                child => !hasText(child as Element)
            ) : false;
        return hasText(node) && childrenDontHaveText
    })}
}


/**
 * Functions you use in your test code.
 **/
export class Jest {
    static getByTextWithMarkup = (textWithMarkup: string) =>  Helper.byTextWithMarkup(screen.getByText, textWithMarkup);
    static queryByTextWith = (textWithMarkup: string) =>  Helper.byTextWithMarkup(screen.queryByText, textWithMarkup);
}

Usage:

Jest.getByTextWithMarkup("hello world");
Jest.queryByTextWithMarkup("hello world");

Comments

-1

When looking for 'Visa ending with 4321' in

          Visa
        </strong>
         
        ending with 
        <strong>
          4321
        </strong>
         is expired

I could make this line work

screen.getByText(ignoreHTML('Visa ending with 4321'))

with the following function

function ignoreHTML(searchedText) {
    const inner = (content, node) => {
        const hasText = (node) => node.textContent.includes(searchedText)
        const nodeHasText = hasText(node)
        const childrenDontHaveText = Array.from(node.children).every(
            (child) => !hasText(child)
        )

        return nodeHasText && childrenDontHaveText
    }
    return inner
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.