Skip to content

Latest commit

Β 

History

History
1127 lines (858 loc) Β· 30.9 KB

api-queries.mdx

File metadata and controls

1127 lines (858 loc) Β· 30.9 KB
id title
api-queries
Queries

import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem'

Variants

:::info getBy queries are shown by default in the query documentation below. :::

getBy

getBy* queries return the first matching node for a query, and throw an error if no elements match or if more than one match is found (use getAllBy instead).

getAllBy

getAllBy* queries return an array of all matching nodes for a query, and throw an error if no elements match.

queryBy

queryBy* queries return the first matching node for a query, and return null if no elements match. This is useful for asserting an element that is not present. This throws an error if more than one match is found (use queryAllBy instead).

queryAllBy

queryAllBy* queries return an array of all matching nodes for a query, and return an empty array ([]) if no elements match.

findBy

findBy* queries return a promise which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, then use findAllBy.

:::info this is a simple combination of getBy* queries and waitFor. The findBy* queries accept the waitFor options as the last argument. (i.e. screen.findByText('text', queryOptions, waitForOptions)) :::

findAllBy

findAllBy* queries return a promise which resolves to an array of elements when any elements are found which match the given query. The promise is rejected if no elements are found after a default timeout of 1000ms.

Options

The argument to a query can be a string, regular expression, or function. There are also options to adjust how node text is parsed.

See TextMatch for documentation on what can be passed to a query.

screen

All of the queries exported by DOM Testing Library accept a container as the first argument. Because querying the entire document.body is very common, DOM Testing Library also exports a screen object which has every query that is pre-bound to document.body (using the within functionality).

Here's how you use it:

import { screen } from '@testing-library/dom'
// NOTE: many framework-implementations of Testing Library
// re-export everything from `@testing-library/dom` so you
// may be able to import screen from your framework-implementation:
// import {render, screen} from '@testing-library/react'

const exampleHTML = `
  <label for="example">Example</label>
  <input id="example" />
`
document.body.innerHTML = exampleHTML
const exampleInput = screen.getByLabelText(/example/i)

:::caution You need a global DOM environment to use screen. If you're using jest, with the testEnvironment set to jsdom, a global DOM environment will be available for you.

If you're loading your test with a script tag, make sure it comes after the body. An example can be seen here. :::

screen.debug

For convenience screen also exposes a debug method in addition to the queries. This method is essentially a shortcut for console.log(prettyDOM()). It supports debugging the document, a single element, or an array of elements.

import { screen } from '@testing-library/dom'

document.body.innerHTML = `
  <button>test</button>
  <span>multi-test</span>
  <div>multi-test</div>
`

// debug document
screen.debug()
// debug single element
screen.debug(screen.getByText('test'))
// debug multiple elements
screen.debug(screen.getAllByText('multi-test'))

screen.logTestingPlaygroundURL

For debugging using testing-playground, screen exposes this convenient method which logs a URL that can be opened in a browser.

import { screen } from '@testing-library/dom'

document.body.innerHTML = `
  <button>test</button>
  <span>multi-test</span>
  <div>multi-test</div>
`

// log entire document to testing-playground
screen.logTestingPlaygroundURL()
// log a single element
screen.logTestingPlaygroundURL(screen.getByText('test'))

Queries

:::info These queries are the base queries and require you to pass a container as the first argument. Most framework-implementations of Testing Library provide a pre-bound version of these queries when you render your components with them which means you do not have to provide a container. In addition, if you just want to query document.body then you can use the screen export as demonstrated above (using screen is recommended). :::

ByLabelText

getByLabelText, queryByLabelText, getAllByLabelText, queryAllByLabelText, findByLabelText, findAllByLabelText

getByLabelText(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  text: TextMatch,
  options?: {
    selector?: string = '*',
    exact?: boolean = true,
    normalizer?: NormalizerFn,
  }): HTMLElement

This will search for the label that matches the given TextMatch, then find the element associated with that label.

The example below will find the input node for the following DOM structures:

// for/htmlFor relationship between label and form element id
<label for="username-input">Username</label>
<input id="username-input" />

// The aria-labelledby attribute with form elements
<label id="username-label">Username</label>
<input aria-labelledby="username-label" />

// Wrapper labels
<label>Username <input /></label>

// Wrapper labels where the label text is in another child element
<label>
  <span>Username</span>
  <input />
</label>

// aria-label attributes
// Take care because this is not a label that users can see on the page,
// so the purpose of your input must be obvious to visual users.
<input aria-label="username" />

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const inputNode = screen.getByLabelText('Username')
import { render, screen } from '@testing-library/react'

render(<Login />)

const inputNode = screen.getByLabelText('username')
cy.findByLabelText('username').should('exist')

It will NOT find the input node for label text broken up by elements. You can use getByRole('textbox', { name: 'Username' }) instead which is robust against switching to aria-label or aria-labelledby.

If it is important that you query an actual <label> element you can provide a selector in the options:

// Multiple elements labelled via aria-labelledby
<label id="username">Username</label>
<input aria-labelledby="username" />
<span aria-labelledby="username">Please enter your username</span>

// Multiple labels with the same text
<label>
  Username
  <input />
</label>
<label>
  Username
  <textarea></textarea>
</label>
const inputNode = screen.getByLabelText('Username', { selector: 'input' })

:::danger getByLabelText will not work in the case where a for attribute on a <label> element matches an id attribute on a non-form element. :::

// This case is not valid
// for/htmlFor between label and an element that is not a form element
<section id="photos-section">
  <label for="photos-section">Photos</label>
</section>

ByPlaceholderText

getByPlaceholderText, queryByPlaceholderText, getAllByPlaceholderText, queryAllByPlaceholderText, findByPlaceholderText, findAllByPlaceholderText

getByPlaceholderText(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  text: TextMatch,
  options?: {
    exact?: boolean = true,
    normalizer?: NormalizerFn,
  }): HTMLElement

This will search for all elements with a placeholder attribute and find one that matches the given TextMatch.

<input placeholder="Username" />

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const inputNode = screen.getByPlaceholderText('Username')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const inputNode = screen.getByPlaceholderText('Username')
cy.findByPlaceholderText('Username').should('exist')

:::tip A placeholder is not a good substitute for a label so you should generally use getByLabelText instead. :::

ByText

getByText, queryByText, getAllByText, queryAllByText, findByText, findAllByText

getByText(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  text: TextMatch,
  options?: {
    selector?: string = '*',
    exact?: boolean = true,
    ignore?: string|boolean = 'script, style',
    normalizer?: NormalizerFn,
  }): HTMLElement

This will search for all elements that have a text node with textContent matching the given TextMatch.

<a href="/about">About ℹ️</a>

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const aboutAnchorNode = screen.getByText(/about/i)
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const aboutAnchorNode = screen.getByText(/about/i)
cy.findByText(/about/i).should('exist')

It also works with inputs whose type attribute is either submit or button:

<input type="submit" value="Send data" />

:::info See getByLabelText for more details on how and when to use the selector option :::

The ignore option accepts a query selector. If the node.matches returns true for that selector, the node will be ignored. This defaults to 'script' because generally you don't want to select script tags, but if your content is in an inline script file, then the script tag could be returned.

If you'd rather disable this behavior, set ignore to false.

ByAltText

getByAltText, queryByAltText, getAllByAltText, queryAllByAltText, findByAltText, findAllByAltText

getByAltText(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  text: TextMatch,
  options?: {
    exact?: boolean = true,
    normalizer?: NormalizerFn,
  }): HTMLElement

This will return the element (normally an <img>) that has the given alt text. Note that it only supports elements which accept an alt attribute: <img>, <input>, and <area> (intentionally excluding <applet> as it's deprecated).

<img alt="Incredibles 2 Poster" src="/incredibles-2.png" />

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const incrediblesPosterImg = screen.getByAltText(/incredibles.*? poster/i)
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const incrediblesPosterImg = screen.getByAltText(/incredibles.*? poster/i)
cy.findByAltText(/incredibles.*? poster/i).should('exist')

ByTitle

getByTitle, queryByTitle, getAllByTitle, queryAllByTitle, findByTitle, findAllByTitle

getByTitle(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  title: TextMatch,
  options?: {
    exact?: boolean = true,
    normalizer?: NormalizerFn,
  }): HTMLElement

Returns the element that has the matching title attribute.

Will also find a title element within an SVG.

<span title="Delete" id="2"></span>
<svg>
  <title>Close</title>
  <g><path /></g>
</svg>

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const deleteElement = screen.getByTitle('Delete')
const closeElement = screen.getByTitle('Close')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const deleteElement = screen.getByTitle('Delete')
const closeElement = screen.getByTitle('Close')
cy.findByTitle('Delete').should('exist')
cy.findByTitle('Close').should('exist')

ByDisplayValue

getByDisplayValue, queryByDisplayValue, getAllByDisplayValue, queryAllByDisplayValue, findByDisplayValue, findAllByDisplayValue

getByDisplayValue(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  value: TextMatch,
  options?: {
    exact?: boolean = true,
    normalizer?: NormalizerFn,
  }): HTMLElement

Returns the input, textarea, or select element that has the matching display value.

input

<input type="text" id="lastName" />
document.getElementById('lastName').value = 'Norris'

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const lastNameInput = screen.getByDisplayValue('Norris')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const lastNameInput = screen.getByDisplayValue('Norris')
cy.findByDisplayValue('Norris').should('exist')

textarea

<textarea id="messageTextArea" />
document.getElementById('messageTextArea').value = 'Hello World'

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const messageTextArea = screen.getByDisplayValue('Hello World')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const messageTextArea = screen.getByDisplayValue('Hello World')
cy.findByDisplayValue('Hello World').should('exist')

select

In case of select, this will search for a <select> whose selected <option> matches the given TextMatch.

<select>
  <option value="">State</option>
  <option value="AL">Alabama</option>
  <option selected value="AK">Alaska</option>
  <option value="AZ">Arizona</option>
</select>

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const selectElement = screen.getByDisplayValue('Alaska')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const selectElement = screen.getByDisplayValue('Alaska')
cy.findByDisplayValue('Alaska').should('exist')

ByRole

getByRole, queryByRole, getAllByRole, queryAllByRole, findByRole, findAllByRole

getByRole(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  role: TextMatch,
  options?: {
    exact?: boolean = true,
    hidden?: boolean = false,
    name?: TextMatch,
    normalizer?: NormalizerFn,
    selected?: boolean,
    checked?: boolean,
    pressed?: boolean,
    expanded?: boolean,
    queryFallbacks?: boolean,
    level?: number,
  }): HTMLElement

Queries for elements with the given role (and it also accepts a TextMatch). Default roles are taken into consideration e.g. <button /> has the button role without explicitly setting the role attribute. Here you can see a table of HTML elements with their default and desired roles.

Please note that setting a role and/or aria-* attribute that matches the implicit ARIA semantics is unnecessary and is not recommended as these properties are already set by the browser, and we must not use the role and aria-* attributes in a manner that conflicts with the semantics described. For example, a button element can't have the role attribute of heading, because the button element has default characteristics that conflict with the heading role.

:::caution Roles are matched literally by string equality, without inheriting from the ARIA role hierarchy. As a result, querying a superclass role like checkbox will not include elements with a subclass role like switch. :::

You can query the returned element(s) by their accessible name. The accessible name is for simple cases equal to e.g. the label of a form element, or the text content of a button, or the value of the aria-label attribute. It can be used to query a specific element if multiple elements with the same role are present on the rendered content. For an in-depth guide check out "What is an accessible name?" from ThePacielloGroup. If you only query for a single element with getByText('The name') it's oftentimes better to use getByRole(expectedRole, { name: 'The name' }). The accessible name query does not replace other queries such as *ByAlt or *ByTitle. While the accessible name can be equal to these attributes, it does not replace the functionality of these attributes. For example <img aria-label="fancy image" src="fancy.jpg" /> will be returned for both getByAltText('fancy image') and getByRole('image', { name: 'fancy image' }). However, the image will not display its description if fancy.jpg could not be loaded. Whether you want assert this functionality in your test or not is up to you.

If you set hidden to true elements that are normally excluded from the accessibility tree are considered for the query as well. The default behavior follows https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion with the exception of role="none" and role="presentation" which are considered in the query in any case. For example in

<body>
  <main aria-hidden="true">
    <button>Open dialog</button>
  </main>
  <div role="dialog">
    <button>Close dialog</button>
  </div>
</body>

getByRole('button') would only return the Close dialog-button. To make assertions about the Open dialog-button you would need to use getAllByRole('button', { hidden: true }).

The default value for hidden can be configured.

selected

You can filter the returned elements by their selected state by setting selected: true or selected: false.

For example in

<body>
  <div role="tablist">
    <button role="tab" aria-selected="true">Native</button>
    <button role="tab" aria-selected="false">React</button>
    <button role="tab" aria-selected="false">Cypress</button>
  </div>
</body>

you can get the "Native"-tab by calling getByRole('tab', { selected: true }). To learn more about the selected state and which elements can have this state see ARIA aria-selected.

checked

You can filter the returned elements by their checked state by setting checked: true or checked: false.

For example in

<body>
  <section>
    <button role="checkbox" aria-checked="true">Sugar</button>
    <button role="checkbox" aria-checked="false">Gummy bears</button>
    <button role="checkbox" aria-checked="false">Whipped cream</button>
  </section>
</body>

you can get the "Sugar" option by calling getByRole('checkbox', { checked: true }). To learn more about the checked state and which elements can have this state see ARIA aria-checked.

:::info Checkboxes have a "mixed" state, which is considered neither checked nor unchecked (details here). :::

pressed

Buttons can have a pressed state. You can filter the returned elements by their pressed state by setting pressed: true or pressed: false.

For example in

<body>
  <section>
    <button aria-pressed="true">πŸ‘</button>
    <button aria-pressed="false">πŸ‘Ž</button>
  </section>
</body>

you can get the "πŸ‘" button by calling getByRole('button', { pressed: true }). To learn more about the pressed state see ARIA aria-pressed.

expanded

You can filter the returned elements by their expanded state by setting expanded: true or expanded: false.

For example in

<body>
  <nav>
    <ul>
      <li>
        <a aria-expanded="false" aria-haspopup="true" href="..."
          >Expandable Menu Item</a
        >
        <ul>
          <li><a href="#">Submenu Item 1</a></li>
          <li><a href="#">Submenu Item 1</a></li>
        </ul>
      </li>
      <li><a href="#">Regular Menu Item</a></li>
    </ul>
  </nav>
</body>

you can get the "Expandable Menu Item" link by calling getByRole('link', { expanded: false }). To learn more about the checked state and which elements can have this state see ARIA aria-checked.

<div role="dialog">...</div>

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const dialogContainer = screen.getByRole('dialog')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const dialogContainer = screen.getByRole('dialog')
cy.findByRole('dialog').should('exist')

queryFallbacks

By default, it's assumed that the first role of each element is supported, so only the first role can be queried. If you need to query an element by any of its fallback roles instead, you can use queryFallbacks: true.

For example, getByRole('switch') would always match <div role="switch checkbox" /> because it's the first role, while getByRole('checkbox') would not. However, getByRole('checkbox', { queryFallbacks: true }) would enable all fallback roles and therefore match the same element.

:::info An element doesn't have multiple roles in a given environment. It has a single one. Multiple roles in the attribute are evaluated from left to right until the environment finds the first role it understands. This is useful when new roles get introduced and you want to start supporting those as well as older environments that don't understand that role (yet). :::

level

An element with the heading role can be queried by any heading level getByRole('heading') or by a specific heading level using the level option getByRole('heading', { level: 2 }).

The level option queries the element(s) with the heading role matching the indicated level determined by the semantic HTML heading elements <h1>-<h6> or matching the aria-level attribute.

Given the example below,

<body>
  <section>
    <h1>Heading Level One</h1>
    <h2>First Heading Level Two</h2>
    <h3>Heading Level Three</h3>
    <div role="heading" aria-level="2">Second Heading Level Two</div>
  </section>
</body>

you can query the Heading Level Three heading using getByRole('heading', { level: 3 }).

getByRole('heading', { level: 1 })
// <h1>Heading Level One</h1>

getAllByRole('heading', { level: 2 })
// [
//   <h2>First Heading Level Two</h2>,
//   <div role="heading" aria-level="2">Second Heading Level Two</div>
// ]

While it is possible to explicitly set role="heading" and aria-level attribute on an element, it is strongly encouraged to use the semantic HTML headings <h1>-<h6>.

To learn more about the aria-level property, see ARIA aria-level.

:::danger The level option is only applicable to the heading role. An error will be thrown when used with any other role. :::

ByTestId

getByTestId, queryByTestId, getAllByTestId, queryAllByTestId, findByTestId, findAllByTestId

getByTestId(
  container: HTMLElement, // if you're using `screen`, then skip this argument
  text: TextMatch,
  options?: {
    exact?: boolean = true,
    normalizer?: NormalizerFn,
  }): HTMLElement

A shortcut to container.querySelector(`[data-testid="${yourId}"]`) (and it also accepts a TextMatch).

<div data-testid="custom-element" />

<Tabs defaultValue="native" values={[ { label: 'Native', value: 'native', }, { label: 'React', value: 'react', }, { label: 'Cypress', value: 'cypress', }, ] }>

import { screen } from '@testing-library/dom'

const element = screen.getByTestId('custom-element')
import { render, screen } from '@testing-library/react'

render(<MyComponent />)
const element = screen.getByTestId('custom-element')
cy.findByTestId('custom-element').should('exist')

:::caution In the spirit of the guiding principles, it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure or styling css class names. Learn more about data-testids from the blog post "Making your UI tests resilient to change" :::

Overriding data-testid

The ...ByTestId functions in DOM Testing Library use the attribute data-testid by default, following the precedent set by React Native Web which uses a testID prop to emit a data-testid attribute on the element, and we recommend you adopt that attribute where possible. But if you already have an existing codebase that uses a different attribute for this purpose, you can override this value via configure({testIdAttribute: 'data-my-test-attribute'}).

TextMatch

Several APIs accept a TextMatch which can be a string, regex or a function which returns true for a match and false for a mismatch.

Precision

Some APIs accept an object as the final argument that can contain options that affect the precision of string matching:

  • exact: Defaults to true; matches full strings, case-sensitive. When false, matches substrings and is not case-sensitive.
    • exact has no effect on regex or function arguments.
    • In most cases using a regex instead of a string gives you more control over fuzzy matching and should be preferred over { exact: false }.
  • normalizer: An optional function which overrides normalization behavior. See Normalization.

Normalization

Before running any matching logic against text in the DOM, DOM Testing Library automatically normalizes that text. By default, normalization consists of trimming whitespace from the start and end of text, and collapsing multiple adjacent whitespace characters into a single space.

If you want to prevent that normalization, or provide alternative normalization (e.g. to remove Unicode control characters), you can provide a normalizer function in the options object. This function will be given a string and is expected to return a normalized version of that string.

:::info Specifying a value for normalizer replaces the built-in normalization, but you can call getDefaultNormalizer to obtain a built-in normalizer, either to adjust that normalization or to call it from your own normalizer. :::

getDefaultNormalizer takes an options object which allows the selection of behaviour:

  • trim: Defaults to true. Trims leading and trailing whitespace
  • collapseWhitespace: Defaults to true. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space.

Normalization Examples

To perform a match against text without trimming:

screen.getByText('text', {
  normalizer: getDefaultNormalizer({ trim: false }),
})

To override normalization to remove some Unicode characters whilst keeping some (but not all) of the built-in normalization behavior:

screen.getByText('text', {
  normalizer: (str) =>
    getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''),
})

TextMatch Examples

Given the following HTML:

<div>Hello World</div>

Will find the div:

// Matching a string:
screen.getByText('Hello World') // full string match
screen.getByText('llo Worl', { exact: false }) // substring match
screen.getByText('hello world', { exact: false }) // ignore case

// Matching a regex:
screen.getByText(/World/) // substring match
screen.getByText(/world/i) // substring match, ignore case
screen.getByText(/^hello world$/i) // full string match, ignore case
screen.getByText(/Hello W?oRlD/i) // advanced regex

// Matching with a custom function:
screen.getByText((content, element) => content.startsWith('Hello'))

Will not find the div:

// full string does not match
screen.getByText('Goodbye World')

// case-sensitive regex with different case
screen.getByText(/hello world/)

// function looking for a span when it's actually a div:
screen.getByText((content, element) => {
  return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})