Get by HTML element with React Testing Library?
Asked Answered
T

4

100

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");
Thirza answered 17/1, 2019 at 11:3 Comment(0)
A
175

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.

Ares answered 18/1, 2019 at 9:1 Comment(7)
It does making testing for some things harder. Say I have a loader that is displayed initially on page load. I want to make sure it renders before, say, a table of data appears. The spinner does not have any text associated with it.Worthless
And what about when the HTML element tag is exactly the thing you wish to cover against regressions? perhaps for accessibility purposes you want to ensure that its an h2 and the test should break.Valle
Then test that it's an h2, but that's usually the exception rather than the ruleAres
What I dont understand is why react test framework does not support a getByDataAttribute("data-custom-value") method. These are not implementation details, they can be on any elements, which can change without breaking tests. Why limit things to a test specific data attribute?Whereof
Because most of the time that's not what you want to test. You can still do it with RTL but it's not the best approach for most applicationsAres
You could use the getAllByRole("button") or getByRole("button"), se testing-library.com/docs/queries/byroleYocum
How would you grab a container if call to render is wrapped in act utility?Yoakum
L
38

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()
})
Lindberg answered 19/8, 2021 at 16:5 Comment(2)
Definitely. Using *ByRole queries (whenever possible) is a more idiomatic solution when using React Testing Library.Religiose
+1 this is a much better answer than the accepted one - it tests the semantics that matter and are picked up by screenreaders, crawlers etc, whereas the other tests the implementation detail that doesn't matter. getByRole('button') will match both <button> and <a role="button">, and the above example will match both <h6> and <a role="heading" aria-level="6"> - and this is good because tools that care about those roles will (if properly built...) treat them the same.Doykos
A
7

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.

Amphidiploid answered 2/7, 2022 at 18:38 Comment(0)
T
1

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 }
  )
}
Thundercloud answered 28/10, 2022 at 9:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.