React Testing Library query types

Posted 2023-10-15

This is part of my notes section of the site. It is where I post quick notes/cheatsheets/etc.
For more in-depth articles which are (hopefully) more interesting check out my blog


How to find elements (other than findByText)

ByText

getByText, queryByText, getAllByText, queryAllByText, findByText, findAllByText

The byText queries accepts a few arguments. Here are some common ways it is used.

byText.test.ts
✅ copied
import {render, screen} from '@testing-library/react' // exact match for text within a DOM element, e.g. // <div>Hello world</div> expect(screen.getByText('Hello world')).toBeInTheDocument() // partial match, or case insensitive expect(screen.getByText('HELLO WORLD', {exact: false})).toBeInTheDocument() expect(screen.getByText('world', {exact: false})).toBeInTheDocument()

ByRole

getByRole, queryByRole, getAllByRole, queryAllByRole, findByRole, findAllByRole

Queries the DOM for elements which match a role. These are the ARIA roles. You can find a list of them here

Here is an example, where we are trying to get the element with a button role:

role.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <button>hello</button> </div> } test('the title exists', async () => { render(<SomeComponent />) expect(screen.getByRole('button')).toBeInTheDocument() })

You might be asking **why can't we just do something like document.querySelector('button') (the same as we could in a browser to get a <button> component.

The reason is that React Testing Library encourages you to write tests with accessibility in mind. Selecting elements in this way is more realistic and encourages better practices when it comes to writing your HTML (JSX) in a proper semantic way.

The same test would also match a <div> which has the button role, e.g.:

role-aria.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { // Note this is no longer a <button> return <div> <div role="button" onClick={() => console.log("say hi")}> hello </div> </div> } test('the title exists', async () => { render(<SomeComponent />) expect(screen.getByRole('button')).toBeInTheDocument() })

Two important caveats:

  • do not go and set role="something" in your JSX/HTML just so you can easily select it in tests. This is the completely wrong approach! Write good semantic HTML, and use that in your tests to select an element.
  • if you have hidden (or aria-hidden) on an element, then getByRole() will not select it. You can add the {hidden: true} argument to find those hidden elements. Here is an example:
hidden-role-query.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <button aria-hidden>Show</button> <button>Hide</button> </div> } test('the title exists', async () => { render(<SomeComponent />) // or as there is only 1, you could do `screen.getByRole('button')` const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(1) expect(buttons[0].textContent).toBe('Hide') expect(buttons[0]).not.toHaveAttribute('aria-hidden') const buttonsIncludingHidden = screen.getAllByRole('button', {hidden: true}) expect(buttonsIncludingHidden).toHaveLength(2) expect(buttonsIncludingHidden[0].textContent).toBe('Show') expect(buttonsIncludingHidden[0]).toHaveAttribute('aria-hidden') expect(buttonsIncludingHidden[1].textContent).toBe('Hide') expect(buttonsIncludingHidden[1]).not.toHaveAttribute('aria-hidden') })

You can also use the {selected: true} to only include selected elements (with aria-selected attribute)

role-selected.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div role="tablist"> <button role="tab" aria-selected="true">Tab1</button> <button role="tab" aria-selected="false">Tab2</button> </div> } test('the title exists', async () => { render(<SomeComponent/>) expect(screen.getByRole('tab', {selected: true}).textContent).toBe('Tab1') expect(screen.getByRole('tab', {selected: false}).textContent).toBe('Tab2') })

There are other similar arguments:

  • busy - getByRole('alert', { busy: false }) for aria-busy
    • {busy: true} to select elements with aria-busy="true" attribute
    • {busy: false} to select elements with aria-busy="false" attribute
  • checked - getByRole('checkbox', { checked: true }) to get checked checkboxes
  • current - getByRole('link', { current: 'page' }) to get an element with that aria-current value
    • example: <a href="page/3" aria-current="page">3</a>
  • pressed - getByRole('button', { pressed: true }) for elements with aria-pressed
    • <button aria-pressed="true">Vote</button>
    • getByRole('button', { pressed: true })
  • heading - used to select specific h1/h2/h3/h4/h5 tags
    • getByRole('heading', {level: 1})
    • Can also be used to select other elements with the heading role, such as <span role="heading" aria-level="1"> (equivalent to h1)

ByLabelText

getByLabelText, queryByLabelText, getAllByLabelText, queryAllByLabelText, findByLabelText, findAllByLabelText

This will return elements (form elements) which are labelled by a specific text string. This will select form elements wrapped in a <label>...</label>, labelled by <label for="..."> (htmlFor in JSX), or aria labels:

labelled-by-text.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <label> Your First Name <input defaultValue="Fred" /> </label> <label htmlFor="last_name">Your Last Name</label> <input defaultValue="Flintstone" id="last_name" /> <div id="age">Your Age</div> <input aria-labelledby="age" defaultValue="99" /> <input aria-label="Your Occupation" defaultValue="Crane operator" /> </div> } test('selecting via labels', async () => { render(<SomeComponent/>) const firstNameInput = screen.getByLabelText('Your First Name') expect(firstNameInput).toHaveAttribute('value', 'Fred') const lastNameInput = screen.getByLabelText('Your Last Name') expect(lastNameInput).toHaveAttribute('value', 'Flintstone') const ageInput = screen.getByLabelText('Your Age') expect(ageInput).toHaveAttribute('value', '99') const occupationInput = screen.getByLabelText('Your Occupation') expect(occupationInput).toHaveAttribute('value', 'Crane operator') })

ByPlaceholderText

getByPlaceholderText, queryByPlaceholderText, getAllByPlaceholderText, queryAllByPlaceholderText, findByPlaceholderText, findAllByPlaceholderText

Use these queries to find inputs with a placeholder="..." value.

by-placeholder.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <input placeholder="Enter your name" /> </div> } test('selecting via placeholder', async () => { render(<SomeComponent/>) expect(screen.getByPlaceholderText('Enter your name')).toBeInTheDocument() })

Using placeholders is not as accessible as using proper labels, so this should not be your default way to select form elements (see the ByLabelText section for a better alternative).

ByDisplayValue

getByDisplayValue, queryByDisplayValue, getAllByDisplayValue, queryAllByDisplayValue, findByDisplayValue, findAllByDisplayValue

Use this to query for inputs which have their value attribute set to a specific thing.

I find this useful to check a selected value for a dropdown (and haven't ever used it for normal <input> or <textarea>)

Here is an example:

by-display-value.test.jsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <input defaultValue="Fred" /> <input value="Fred2" onChange={() => /* not implemented */ undefined}/> <textarea defaultValue="Fred3" /> <select> <option value="H1">Harry potter</option> <option selected value="V1">Voldemort</option> </select> </div> } test('selecting via display value', async () => { render(<SomeComponent/>) // normal inputs: expect(screen.getByDisplayValue('Fred')).toBeInTheDocument() expect(screen.getByDisplayValue('Fred2')).toBeInTheDocument() // textarea: expect(screen.getByDisplayValue('Fred3')).toBeInTheDocument() // select dropdowns: expect(screen.getByDisplayValue('Voldemort')).toBeInTheDocument() expect(screen.queryByDisplayValue('Harry potter')).toBeNull() })

ByAltText

getByAltText, queryByAltText, getAllByAltText, queryAllByAltText, findByAltText, findAllByAltText

This can be used to select <img> tags with a certain alt tag. You can use {exact: false} in a similar way to getByText() for non-exact matches.

by-alt-text.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <img alt="Cat" src="/cat.jpg" /> <img alt="Dog" src="/dog.jpg" /> <img alt="Cats and Dogs" src="/dog.jpg" /> </div> } test('selecting via alt attribute', async () => { render(<SomeComponent/>) expect(screen.getByAltText('Cat')).toBeInTheDocument() expect(screen.getByAltText('Dog')).toBeInTheDocument() const allWithCatsAsAlt = screen.getAllByAltText('cat', {exact: false}) expect(allWithCatsAsAlt).toHaveLength(2) expect(allWithCatsAsAlt[0]).toHaveAttribute('alt', 'Cat') expect(allWithCatsAsAlt[1]).toHaveAttribute('alt', 'Cats and Dogs') })

ByTitle

getByTitle, queryByTitle, getAllByTitle, queryAllByTitle, findByTitle, findAllByTitle

This works similarly to getByAltText, except it will look for any element with a specific title attribute.

You can also use this to select SVGs with those titles.

by-title.test.tsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <div title="Number of votes">4</div> <svg> <title>Locked</title> <g><path/></g> </svg> </div> } test('selecting via title attribute', async () => { render(<SomeComponent/>) expect(screen.getByTitle('Number of votes').textContent).toBe('4') expect(screen.getByTitle('Locked')).toBeInTheDocument() })

ByTestId

Sometimes selecting elements can be difficult or ambiguous - especially if you have a complex DOM or multiple elements labelled in the same way. I don't recommend going for this route often, but in those awkward cases, you can add a data-testid="something" to your JSX and use that to select the element in your tests.

by-test-id.test.jsx
✅ copied
import {render, screen} from '@testing-library/react' function SomeComponent() { return <div> <div data-testid="price">$1.00</div> <div data-testid="discount">$0.50</div> </div> } test('selecting via testid attribute', async () => { render(<SomeComponent/>) expect(screen.getByTestId('price').textContent).toBe('$1.00') expect(screen.getByTestId('discount').textContent).toBe('$0.50') })

Which query to use?

There is a priority guide on the RTL site

Here is that guide, it is worth a read (but make sure to carry on and read what the paragraph I write after this!)

  1. Queries Accessible to Everyone Queries that reflect the experience of visual/mouse users as well as those that use assistive technology.
    1. getByRole: This can be used to query every element that is exposed in the accessibility tree. With the name option you can filter the returned elements by their accessible name. This should be your top preference for just about everything. There's not much you can't get with this (if you can't, it's possible your UI is inaccessible). Most often, this will be used with the name option like so: getByRole('button', {name: /submit/i}).
    2. getByLabelText: This method is really good for form fields. When navigating through a website form, users find elements using label text. This method emulates that behavior, so it should be your top preference.
    3. getByPlaceholderText: A placeholder is not a substitute for a label. But if that's all you have, then it's better than alternatives.
    4. getByText: Outside of forms, text content is the main way users find elements. This method can be used to find non-interactive elements (like divs, spans, and paragraphs).
    5. getByDisplayValue: The current value of a form element can be useful when navigating a page with filled-in values.
  2. Semantic Queries HTML5 and ARIA compliant selectors. Note that the user experience of interacting with these attributes varies greatly across browsers and assistive technology.
    1. getByAltText: If your element is one which supports alt text (img, area, input, and any custom element), then you can use this to find that element.
    2. getByTitle: The title attribute is not consistently read by screenreaders, and is not visible by default for sighted users
  3. Test IDs
    1. getByTestId: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense (e.g. the text is dynamic).

But why shouldn't you always go for getByRole if it is at the top of the list??

It is slow! Getting an element with getByRole on complex components with a lot of elements can be much slower than a simpler one like getByText. Getting by text is quite an easy comparison (especially when it is in strict exact match). Get by role has to parse every element, parse the CSS styles (it needs to know if an element is hidden/visible), and then it can figure out what each role is.

You might not notice it - especially on small components. But if you are running integration tests on the top level 'page' components it is something that can come up.

There is an interesting Github discussion here.

Subscribe to my
Full Stack Typescript Developer
newsletter

If you enjoyed my content and want more Full Stack Typescript content, then enter your email below.

I send a few links every couple of weeks, that I personally found useful from around the web.

Focusing on Testing, Typescript, NextJS, and also
engineering soft skills posts.

Welcome to my site and blog - Code Driven Development.

This is the 'notes' section, which I use to quickly write up blog posts. For more in depth and longer posts check out My blog