How to get element by proximity to another element in React Testing Library

Viewed 7084

I've been trying to write some tests using React Testing Library and following their guiding principles, in that the tests should test the application components in the way the user would use it.

I have an component that renders a list of objects with several fields about each, resulting in a DOM like this:

<div>
  <h1>Fluffy</h1>
  <h2>Cat</h2>
  <span>3 years old</span>
</div>
<div>
  <h1>Oscar</h1>
  <h2>Cat</h2>
  <span>2 years old</span>
</div>
<div>
  <h1>Charlie</h1>
  <h2>Dog</h2>
  <span>3 years old</span>
</div>

I would like to assert that each object is rendered correctly with the relevant fields, but I can't see how to do this using React Testing Library. So far I have:

it('renders the animal names, species, and ages', () => {
  render(<MyAnimalsComponent />)

  const fluffyName = screen.getByRole('heading', { name: 'Fluffy' })
  expect(fluffyName).toBeInTheDocument()

  // The problem here is that there are multiple headings with the name "Cat" (Fluffy and Oscar) and I have no way of checking that the one that is returned is actually the one for Fluffy.
  const fluffySpecies = screen.getByRole('heading', { name: 'Cat' })
  expect(fluffySpecies).toBeInTheDocument()

  // Likewise, the age "3 years old" is rendered for both Fluffy and Charlie. How do I make sure I get the one that is rendered in the same container as Fluffy's name?
  const fluffyAge = screen.getByText('3 years old')
  expect(fluffyAge).toBeInTheDocument()
})

Is there any way to use the query methods in React Testing Library to only find elements with a common parent to another element? Or a way to get the parent of an element and then only find elements that are children of that element?

What is the best way to accomplish this while still following the guiding principles of React Testing Library?

1 Answers

One way to get around this issue is to use the top-level getByRole and getByText methods and pass in an element as the first argument to limit the query to that element.

For my example, this involved setting the role attribute on some wrapper elements to make it easier to semantically find the elements:

<div role="list">
  <div role="listitem">
    <h1>Fluffy</h1>
    <h2>Cat</h2>
    <span>3 years old</span>
  </div>
  <div role="listitem">
    <h1>Oscar</h1>
    <h2>Cat</h2>
    <span>2 years old</span>
  </div>
  <div role="listitem">
    <h1>Charlie</h1>
    <h2>Dog</h2>
    <span>3 years old</span>
  </div>
</div>

Then in the test, I found the different list items and then performed the queries on those items:

import { render, screen, getByText, getByRole } from '@testing-library/react'

it('renders the animal names, species, and ages', () => {
  render(<MyAnimalsComponent />)

  const animals = screen.getAllByRole('listitem')

  const fluffyName = getByRole(animals[0], 'heading', { name: 'Fluffy' })
  expect(fluffyName).toBeInTheDocument()

  const fluffySpecies = getByRole(animals[0], 'heading', { name: 'Cat' })
  expect(fluffySpecies).toBeInTheDocument()

  const fluffyAge = getByText(animals[0], '3 years old')
  expect(fluffyAge).toBeInTheDocument()
})

Update: The same thing can be accomplished using the within methods which is arguably easier to understand:

import { render, screen, within } from '@testing-library/react'

it('renders the animal names, species, and ages', () => {
  render(<MyAnimalsComponent />)

  const animals = screen.getAllByRole('listitem')

  const fluffyName = within(animals[0]).getByRole('heading', { name: 'Fluffy' })
  expect(fluffyName).toBeInTheDocument()

  const fluffySpecies = within(animals[0]).getByRole('heading', { name: 'Cat' })
  expect(fluffySpecies).toBeInTheDocument()

  const fluffyAge = within(animals[0]).getByText('3 years old')
  expect(fluffyAge).toBeInTheDocument()
})
Related