Skip to content

Latest commit

 

History

History
915 lines (717 loc) · 40.9 KB

README.md

File metadata and controls

915 lines (717 loc) · 40.9 KB

dom-testing-library

octopus

Simple and complete DOM testing utilities that encourage good testing practices.


Build Status Code Coverage version downloads MIT License

All Contributors PRs Welcome Code of Conduct

Watch on GitHub Star on GitHub Tweet

The problem

You want to write maintainable tests for your Web UI. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down.

This solution

The dom-testing-library is a very light-weight solution for testing DOM nodes (whether simulated with JSDOM as provided by default with jest or in the browser). The main utilities it provides involve querying the DOM for nodes in a way that's similar to how the user finds elements on the page. In this way, the library helps ensure your tests give you confidence in your UI code. The dom-testing-library's primary guiding principle is:

The more your tests resemble the way your software is used, the more confidence they can give you.

As part of this goal, the utilities this library provides facilitate querying the DOM in the same way the user would. Finding for elements by their label text (just like a user would), finding links and buttons from their text (like a user would), and more. It also exposes a recommended way to find elements by a data-testid as an "escape hatch" for elements where the text content and label do not make sense or is not practical.

This library encourages your applications to be more accessible and allows you to get your tests closer to using your components the way a user will, which allows your tests to give you more confidence that your application will work when a real user uses it.

What this library is not:

  1. A test runner or framework
  2. Specific to a testing framework (though we recommend Jest as our preference, the library works with any framework)

Table of Contents

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

npm install --save-dev dom-testing-library

Usage

Note:

  • Each of the get APIs below have a matching getAll API that returns all elements instead of just the first one, and query/getAll that return null/[] instead of throwing an error.
  • See TextMatch for details on the exact, trim, and collapseWhitespace options.
// src/__tests__/example.js
// query utilities:
import {
  getByLabelText,
  getByText,
  getByTestId,
  queryByTestId,
  // Tip: all queries are also exposed on an object
  // called "queries" which you could import here as well
  wait,
} from 'dom-testing-library'
// adds special assertions like toHaveTextContent and toBeInTheDOM
import 'dom-testing-library/extend-expect'

function getExampleDOM() {
  // This is just a raw example of setting up some DOM
  // that we can interact with. Swap this with your UI
  // framework of choice 😉
  const div = document.createElement('div')
  div.innerHTML = `
    <label for="username">Username</label>
    <input id="username" />
    <button>Print Username</button>
  `
  const button = div.querySelector('button')
  const input = div.querySelector('input')
  button.addEventListener('click', () => {
    // let's pretend this is making a server request, so it's async
    // (you'd want to mock this imaginary request in your unit tests)...
    setTimeout(() => {
      const printedUsernameContainer = document.createElement('div')
      printedUsernameContainer.innerHTML = `
        <div data-testid="printed-username">${input.value}</div>
      `
      div.appendChild(printedUsernameContainer)
    }, Math.floor(Math.random() * 200))
  })
  return div
}

test('examples of some things', async () => {
  const famousWomanInHistory = 'Ada Lovelace'
  const container = getExampleDOM()

  // Get form elements by their label text.
  // An error will be thrown if one cannot be found (accessibility FTW!)
  const input = getByLabelText(container, 'Username')
  input.value = famousWomanInHistory

  // Get elements by their text, just like a real user does.
  getByText(container, 'Print Username').click()

  await wait(() =>
    expect(queryByTestId(container, 'printed-username')).toBeInTheDOM(),
  )

  // getByTestId and queryByTestId are an escape hatch to get elements
  // by a test id (could also attempt to get this element by it's text)
  expect(getByTestId(container, 'printed-username')).toHaveTextContent(
    famousWomanInHistory,
  )
  // jest snapshots work great with regular DOM nodes!
  expect(container).toMatchSnapshot()
})

getByLabelText

getByLabelText(
  container: HTMLElement,
  text: TextMatch,
  options?: {
    selector?: string = '*',
    exact?: boolean = true,
    collapseWhitespace?: boolean = true,
    trim?: boolean = true,
  }): HTMLElement

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

const inputNode = getByLabelText(container, 'Username')

// this would find the input node for the following DOM structures:
// The "for" attribute (NOTE: in JSX with React you'll write "htmlFor" rather than "for")
// <label for="username-input">Username</label>
// <input id="username-input" />
//
// The aria-labelledby attribute
// <label id="username-label">Username</label>
// <input aria-labelledby="username-label" />
//
// Wrapper labels
// <label>Username <input /></label>
//
// It will NOT find the input node for this:
// <label><span>Username</span> <input /></label>
//
// For this case, you can provide a `selector` in the options:
const inputNode = getByLabelText(container, 'username', {selector: 'input'})
// and that would work
// Note that <input aria-label="username" /> will also work, but take
// care because this is not a label that users can see on the page. So
// the purpose of your input should be obvious for those users.

Note: This method will throw an error if it cannot find the node. If you don't want this behavior (for example you wish to assert that it doesn't exist), then use queryByLabelText instead.

getByPlaceholderText

getByPlaceholderText(
  container: HTMLElement,
  text: TextMatch,
  options?: {
    exact?: boolean = true,
    collapseWhitespace?: boolean = false,
    trim?: boolean = true,
  }): HTMLElement

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

// <input placeholder="Username" />
const inputNode = getByPlaceholderText(container, 'Username')

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

getByText

getByText(
  container: HTMLElement,
  text: TextMatch,
  options?: {
    selector?: string = '*',
    exact?: boolean = true,
    collapseWhitespace?: boolean = true,
    trim?: boolean = true,
  }): HTMLElement

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

// <a href="/about">About ℹ️</a>
const aboutAnchorNode = getByText(container, 'about')

NOTE: see getbylabeltext for more details on how and when to use the selector option

getByAltText

getByAltText(
  container: HTMLElement,
  text: TextMatch,
  options?: {
    exact?: boolean = true,
    collapseWhitespace?: boolean = false,
    trim?: boolean = true,
  }): 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" />
const incrediblesPosterImg = getByAltText(container, /incredibles.*poster$/i)

getByTitle

getByTitle(
  container: HTMLElement,
  title: TextMatch,
  options?: {
    exact?: boolean = true,
    collapseWhitespace?: boolean = false,
    trim?: boolean = true,
  }): HTMLElement

Returns the element that has the matching title attribute.

// <span title="Delete" id="2" />
const deleteElement = getByTitle(container, 'Delete')

getByValue

getByValue(
  container: HTMLElement,
  value: TextMatch,
  options?: {
    exact?: boolean = true,
    collapseWhitespace?: boolean = false,
    trim?: boolean = true,
  }): HTMLElement

Returns the element that has the matching value.

// <input type="text" id="lastName" defaultValue="Norris" />
const lastNameInput = getByValue('Norris')

getByTestId

getByTestId(
  container: HTMLElement,
  text: TextMatch,
  options?: {
    exact?: boolean = true,
    collapseWhitespace?: boolean = false,
    trim?: boolean = true,
  }): HTMLElement`

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

// <input data-testid="username-input" />
const usernameInputElement = getByTestId(container, 'username-input')

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"

wait

function wait(
  callback?: () => void,
  options?: {
    timeout?: number
    interval?: number
  },
): Promise<void>

When in need to wait for non-deterministic periods of time you can use wait, to wait for your expectations to pass. The wait function is a small wrapper around the wait-for-expect module. Here's a simple example:

// ...
// Wait until the callback does not throw an error. In this case, that means
// it'll wait until we can get a form control with a label that matches "username".
await wait(() => getByLabelText(container, 'username'))
getByLabelText(container, 'username').value = 'chucknorris'
// ...

This can be useful if you have a unit test that mocks API calls and you need to wait for your mock promises to all resolve.

The default callback is a no-op function (used like await wait()). This can be helpful if you only need to wait for one tick of the event loop (in the case of mocked API calls with promises that resolve immediately).

The default timeout is 4500ms which will keep you under Jest's default timeout of 5000ms.

The default interval is 50ms. However it will run your callback immediately on the next tick of the event loop (in a setTimeout) before starting the intervals.

waitForElement

function waitForElement<T>(
  callback?: () => T | null | undefined,
  options?: {
    container?: HTMLElement
    timeout?: number
    mutationObserverOptions?: MutationObserverInit
  },
): Promise<T>

When in need to wait for DOM elements to appear, disappear, or change you can use waitForElement. The waitForElement function is a small wrapper around the MutationObserver. Here's a simple example:

// ...
// Wait until the callback does not throw an error and returns a truthy value. In this case, that means
// it'll wait until we can get a form control with a label that matches "username".
// The difference from `wait` is that rather than running your callback on
// an interval, it's run as soon as there are DOM changes in the container
// and returns the value returned by the callback.
const usernameElement = await waitForElement(
  () => getByLabelText(container, 'username'),
  {container},
)
usernameElement.value = 'chucknorris'
// ...

You can also wait for multiple elements at once:

const [usernameElement, passwordElement] = waitForElement(
  () => [
    getByLabelText(container, 'username'),
    getByLabelText(container, 'password'),
  ],
  {container},
)

Using MutationObserver is more efficient than polling the DOM at regular intervals with wait. This library sets up a 'mutationobserver-shim' on the global window object for cross-platform compatibility with older browsers and the jsdom that is usually used in Node-based tests.

The default callback is a no-op function (used like await waitForElement()). This can be helpful if you only need to wait for the next DOM change (see mutationObserverOptions to learn which changes are detected).

The default container is the global document. Make sure the elements you wait for will be attached to it, or set a different container.

The default timeout is 4500ms which will keep you under Jest's default timeout of 5000ms.

The default mutationObserverOptions is {subtree: true, childList: true} which will detect additions and removals of child elements (including text nodes) in the container and any of its descendants. It won't detect attribute changes unless you add attributes: true to the options.

fireEvent

fireEvent(node: HTMLElement, event: Event)

Fire DOM events.

// <button>Submit</button>
fireEvent(
  getByText(container, 'Submit'),
  new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
  }),
)

fireEvent[eventName]

fireEvent[eventName](node: HTMLElement, eventProperties: Object)

Convenience methods for firing DOM events. Check out src/events.js for a full list as well as default eventProperties.

// <button>Submit</button>
const rightClick = {button: 2}
fireEvent.click(getElementByText('Submit'), rightClick)
// default `button` property for click events is set to `0` which is a left click.

getNodeText

getNodeText(node: HTMLElement)

Returns the complete text content of a html element, removing any extra whitespace. The intention is to treat text in nodes exactly as how it is perceived by users in a browser, where any extra whitespace within words in the html code is not meaningful when the text is rendered.

// <div>
//   Hello
//     World  !
// </div>
const text = getNodeText(container.querySelector('div')) // "Hello World !"

This function is also used internally when querying nodes by their text content. This enables functions like getByText and queryByText to work as expected, finding elements in the DOM similarly to how users would do.

Custom Jest Matchers

When using jest, we recommend that you import a set of custom matchers that make it easier to check several aspects of the state of a DOM element. These are provided by jest-dom, but are also included for convenience to be imported from this library directly:

import 'dom-testing-library/extend-expect'

// <span data-testid="greetings">Hello World</span>
expect(queryByTestId(container, 'greetings')).toBeInTheDOM()
expect(queryByTestId(container, 'greetings')).not.toHaveTextContent('Bye bye')
// ...

> Note: when using `toBeInTheDOM`, make sure you use a query function
> (like `queryByTestId`) rather than a get function (like `getByTestId`).
> Otherwise the `get*` function could throw an error before your assertion.

Check out jest-dom's documentation for a full list of available matchers.

Using other assertion libraries

If you're not using jest, you may be able to find a similar set of custom assertions for your library of choice. Here's a list of alternatives to jest-dom for other popular assertion libraries:

If you're aware of some other alternatives, please make a pull request and add it here!

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 }.
  • trim: Defaults to true; trim leading and trailing whitespace.
  • collapseWhitespace: Defaults to true. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space.

TextMatch Examples

// <div>
//  Hello World
// </div>

// WILL find the div:

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

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

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

// WILL NOT find the div:

getByText(container, 'Goodbye World') // full string does not match
getByText(container, /hello world/) // case-sensitive regex with different case
// function looking for a span when it's actually a div:
getByText(container, (content, element) => {
  return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})

query APIs

Each of the get APIs listed in the 'Usage' section above have a complimentary query API. The get APIs will throw errors if a proper node cannot be found. This is normally the desired effect. However, if you want to make an assertion that an element is not present in the DOM, then you can use the query API instead:

const submitButton = queryByText(container, 'submit')
expect(submitButton).toBeNull() // it doesn't exist
// or if you're using the custom matchers:
expect(submitButton).not.toBeInTheDOM()

queryAll and getAll APIs

Each of the query APIs have a corresponsing queryAll version that always returns an Array of matching nodes. getAll is the same but throws when the array has a length of 0.

const submitButtons = queryAllByText(container, 'submit')
expect(submitButtons).toHaveLength(3) // expect 3 elements
expect(submitButtons[0]).toBeInTheDOM()

within and getQueriesForElement APIs

within (an alias to getQueriesForElement) takes a DOM element and binds it to the raw query functions, allowing them to be used without specifying a container. It is the recommended approach for libraries built on this API and is in use in react-testing-library and vue-testing-library.

Example: To get the text 'hello' only within a section called 'messages', you could do:

import {within} from 'dom-testing-library'

const {getByText} = within(document.body.getElementById('messages'))
const helloMessage = getByText('hello')

Debugging

When you use any get calls in your test cases, the current state of the container (DOM) gets printed on the console. For example:

// <div>Hello world</div>
getByText(container, 'Goodbye world') // will fail by throwing error

The above test case will fail, however it prints the state of your DOM under test, so you will get to see:

Unable to find an element with the text: Goodbye world. 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.
Here is the state of your container:
<div>
  <div>
    Hello World!
  </div>
</div>

Note: Since the DOM size can get really large, you can set the limit of DOM content to be printed via environment variable DEBUG_PRINT_LIMIT. The default value is 7000. You will see ... in the console, when the DOM content is stripped off, because of the length you have set or due to default size limit. Here's how you might increase this limit when running tests:

DEBUG_PRINT_LIMIT=10000 npm test

This works on macOS/linux, you'll need to do something else for windows. If you'd like a solution that works for both, see cross-env

prettyDOM

This helper function can be used to print out readable representation of the DOM tree of a node. This can be helpful for instance when debugging tests.

It is defined as:

function prettyDOM(node: HTMLElement, maxLength?: number): string

It receives the root node to print out, and an optional extra argument to limit the size of the resulting string, for cases when it becomes too large.

This function is usually used alongside console.log to temporarily print out DOM trees during tests for debugging purposes:

const div = document.createElement('div')
div.innerHTML = '<div><h1>Hello World</h1></div>'
console.log(prettyDOM(div))
// <div>
//   <h1>Hello World</h1>
// </div>

This function is what also powers the automatic debugging output described above.

Implementations

This library was not built to be used on its own. The original implementation of these utilities was in the react-testing-library.

Implementations include:

FAQ

Which get method should I use?

Based on the Guiding Principles, your test should resemble how your code (component, page, etc.) as much as possible. With this in mind, we recommend this order of priority:

  1. getByLabelText: Only really good for form fields, but this is the number 1 method a user finds those elements, so it should be your top preference.
  2. getByPlaceholderText: A placeholder is not a substitute for a label. But if that's all you have, then it's better than alternatives.
  3. getByText: Not useful for forms, but this is the number 1 method a user finds other elements (like buttons to click), so it should be your top preference for non-form elements.
  4. getByAltText: If your element is one which supports alt text (img, area, and input), then you can use this to find that element.
  5. getByTestId: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by text or it doesn't make sense (the text is dynamic).

Other than that, you can also use the container to query the rendered component as well (using the regular querySelector API).

Can I write unit tests with this library?

Definitely yes! You can write unit, integration, functional, and end-to-end tests with this library.

What if my app is localized and I don't have access to the text in test?

This is fairly common. Our first bit of advice is to try to get the default text used in your tests. That will make everything much easier (more than just using this utility). If that's not possible, then you're probably best to just stick with data-testids (which is not too bad anyway).

I really don't like data-testids, but none of the other queries make sense. Do I have to use a data-testid?

Definitely not. That said, a common reason people don't like the data-testid attribute is they're concerned about shipping that to production. I'd suggest that you probably want some simple E2E tests that run in production on occasion to make certain that things are working smoothly. In that case the data-testid attributes will be very useful. Even if you don't run these in production, you may want to run some E2E tests that run on the same code you're about to ship to production. In that case, the data-testid attributes will be valuable there as well.

All that said, if you really don't want to ship data-testid attributes, then you can use this simple babel plugin to remove them.

If you don't want to use them at all, then you can simply use regular DOM methods and properties to query elements off your container.

const firstLiInDiv = container.querySelector('div li')
const allLisInDiv = container.querySelectorAll('div li')
const rootElement = container.firstChild
What if I’m iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?

You can make your selector just choose the one you want by including :nth-child in the selector.

const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')

Or you could include the index or an ID in your attribute:

;`<li data-testid="item-${item.id}">{item.text}</li>`

And then you could use the getByTestId utility:

const items = [
  /* your items */
]
const container = render(/* however you render this stuff */)
const thirdItem = getByTestId(container, `item-${items[2].id}`)

Other Solutions

I'm not aware of any! Please feel free to make a pull request to add any here.

Guiding Principles

The more your tests resemble the way your software is used, the more confidence they can give you.

We try to only expose methods and utilities that encourage you to write tests that closely resemble how your web pages are used.

Utilities are included in this project based on the following guiding principles:

  1. If it relates to rendering components, it deals with DOM nodes rather than component instances, nor should it encourage dealing with component instances.
  2. It should be generally useful for testing the application components in the way the user would use it. We are making some trade-offs here because we're using a computer and often a simulated browser environment, but in general, utilities should encourage tests that use the components the way they're intended to be used.
  3. Utility implementations and APIs should be simple and flexible.

At the end of the day, what we want is for this library to be pretty light-weight, simple, and understandable.

Contributors

Thanks goes to these people (emoji key):


Kent C. Dodds

💻 📖 🚇 ⚠️

Ryan Castner

📖

Daniel Sandiego

💻

Paweł Mikołajczyk

💻

Alejandro Ñáñez Ortiz

📖

Matt Parrish

🐛 💻 📖 ⚠️

Justin Hall

📦

Anto Aravinth

💻 ⚠️ 📖

Jonah Moses

📖

Łukasz Gandecki

💻 ⚠️ 📖

Ivan Babak

🐛 🤔 💻 📖

Jesse Day

💻

Ernesto García

💬 💻 📖

Josef Maxx Blake

💻 📖 ⚠️

Alex Cook

📖 💡

Daniel Cook

💻 📖 ⚠️

Thomas Chia

🐛 💻

Tim Deschryver

💻 ⚠️

Alex Krolick

💻

Maddi Joyce

💻

Peter Kamps

🐛 💻 ⚠️

Jonathan Stoye

📖

Sanghyeon Lee

💡

Justice Mba

💻 📖 🤔

Wayne Crouch

💻

Ben Elliott

💻

Ruben Costa

💻

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT