Get by HTML element with React Testing Library?

I'm using the getByTestId function in React Testing Library:

const button = wrapper.getByTestId("button");
expect(heading.textContent).toBe("something");

Is it possible / advisable to search for HTML elements instead? So something like this:

const button = wrapper.getByHTML("button");
const heading = wrapper.getByHTML("h1");
119564 次浏览

I'm not sure what wrapper is in this case. But to answer your two questions: yes it's possible to get by HTML element and no, it's not advisable.

This is how you would do it:

// Possible but not advisable
const { container } = render(<MyComponent />)
// `container` is just a DOM node
const button = container.querySelector('button')

Since you get back a DOM node you can use all the normal DOM APIs such as querySelector.

Now, why is this not advisable. A big selling point of react-testing-library is that you test your components as a user does. This means not relying on implementation details. For instance, you don't have direct access to a component's state.

Writing tests this way is a bit harder but allows you to write more robust tests.

In your case, I would argue that the underlying HTML is an implementation detail. What happens if you change your HTML structure so that the h1 is now an h2 or a div? The test will break. If instead, you look at these elements by text the tag becomes irrelevant.

In some cases, the normal query helpers are not enough. For those events you can use a data-testid and use getByTestId.

Depending on the type of element you're querying for, you might also find the byRole API to be useful:

https://testing-library.com/docs/queries/byrole/

For example, the level was particularly useful for me to test that default <h1> elem was correctly being overridden:

it('correctly renders override header level', () => {
const { getByRole } = render(<Heading overrideHeadingLevel="h6" />)


expect(getByRole('heading', { level: 6 })).toBeInTheDocument()
})

another possible solution

consider rendering your component.

render(<ReactComponent />);


const button = screen.getByText((content, element) => element.tagName.toLowerCase() === 'button');

and if you have multiple buttons use getAllByText and refer to the target element you need to pick.

As already mentioned, it is advisable to use behavior centric selectors (aria, text etc...) but I came across a case where I needed to query by tag name (I have an html string with paragraph tags in it, and want to truncate this in a component to see if the html tags stay intact - and getByRole('paragraph') does not work).

That being said, I wanted to show how one could create custom queries for this case, instead of using container.querySelector. While that's fine for most cases, a custom query will allow for more control on what to do when none are found, too many are found, etc... (& custom error messages).

The documentation only has examples for custom queries based on attributes, so what we can do is the following:

import { queryHelpers } from '@testing-library/dom';
import { queries as libraryQueries } from '@testing-library/react';


function getAllByTagName(
container: HTMLElement,
tagName: keyof JSX.IntrinsicElements,
) {
return Array.from(container.querySelectorAll<HTMLElement>(tagName));
}


function getByTagName(
container: HTMLElement,
tagName: keyof JSX.IntrinsicElements,
) {
const result = getAllByTagName(container, tagName);


if (result.length > 1) {
throw queryHelpers.getElementError(
`Found multiple elements with the tag ${tagName}`,
container,
);
}
return result[0] || null;
}


export const queries = {
...libraryQueries,
getAllByTagName,
getByTagName,
};


Now we can use these queries in our render method's options.

import { queries } from '../helpers/queries'


// it('...')
const { getAllByTagName } = render(<MyComponent />, { queries } )
const paragraphs = getAllByTagName('p')

The queries can also be globally added via a custom render method (where providers might be included, etc.)

import { render } from '@testing-library/react';
import { queries } from '../helpers/queries'


function renderWithProviders(node: JSX.Element) {
return render(
<SomeProvider>{node}</SomeProvider>, { queries }
)
}