Search code examples
unit-testingjestjstesting-library

How to access text broken by multiple elements in Testing Library


here is my code example

<div class="dropdown" data-testid="dropdown">
  <div class="option" data-testid="option">
   <b>[AF]</b> Afghanistan
  </div>
  <div class="option" data-testid="option">
   <b>[AL]</b> Albania
  </div>
  <div class="option" data-testid="option">
   <b>[DZ]</b> Algeria
  </div>
 </div>

What I need to do is to access the first element by its text to trigger the userEvent.selectOptions

const dropdown = getByTestId('dropdown');
userEvent.selectOptions(dropdown, screen.getByText('[AF] Afghanistan'))

What I get is :

TestingLibraryElementError: Unable to find an element with the text: [AF] Afghanistan. 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.

It's obviously because the text I'm trying to query is broken by elements.

Please note, that this is not a select/option dropdown and it doesn't have any accessible roles so I can only query it by screen.getByText


Solution

  • getBy* queries accept a TextMatch as an argument, which means the argument can be either a string, a regex, or a function. In this case, we can use a function (rather than a string) and target the specific element we want to select.

    Here's a helper function that applies this logic and makes it reusable.

    const getByTextContent = (text) => {
        // Passing function to `getByText`
        return screen.getByText((content, element) => {
            const hasText = element => element.textContent === text
            const elementHasText = hasText(element)
            const childrenDontHaveText = Array.from(element?.children || []).every(child => !hasText(child))
            return elementHasText && childrenDontHaveText
      })
    }
    

    To quickly explain what's going on: The query will traverse all elements in the DOM, but we want to avoid returning more elements than needed. We make sure that none of the children has the same text as its parent. This ensures that the element we return is the one that actually contains the target text.

    The getByTextContent helper function can then be used in your tests as follows.

    const dropdown = getByTestId('dropdown');
    userEvent.selectOptions(dropdown, getByTextContent('[AF] Afghanistan'))