dom-testing-library
Simple and complete DOM testing utilities that encourage good testing practices.



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:
- A test runner or framework
- Specific to a testing framework (though we recommend Jest as our
preference, the library works with any framework. See Using Without Jest)
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
/queryAll
that return null
/[]
instead of throwing an error. - See TextMatch for details on the
exact
, trim
, and collapseWhitespace
options.
import {
getByLabelText,
getByText,
getByTestId,
queryByTestId,
wait,
} from 'dom-testing-library'
import 'jest-dom/extend-expect'
function getExampleDOM() {
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', () => {
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()
const input = getByLabelText(container, 'Username')
input.value = famousWomanInHistory
getByText(container, 'Print Username').click()
await wait(() =>
expect(queryByTestId(container, 'printed-username')).toBeTruthy(),
)
expect(getByTestId(container, 'printed-username')).toHaveTextContent(
famousWomanInHistory,
)
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')
const inputNode = getByLabelText(container, 'username', {selector: 'input'})
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
.
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,
ignore?: string|boolean = 'script, style'
}): HTMLElement
This will search for all elements that have a text node with textContent
matching the given TextMatch
.
const aboutAnchorNode = getByText(container, /about/i)
NOTE: 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
.
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).
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.
const deleteElement = getByTitle(container, 'Delete')
Will also find a title
element within an SVG.
const closeElement = getByTitle(container, 'Close')
getByDisplayValue
getByDisplayValue(
container: HTMLElement,
value: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement
Returns the input
, textarea
, or select
element that has the matching display value.
input
const lastNameInput = getByDisplayValue(container, 'Norris')
textarea
const messageTextArea = getByDisplayValue(container, 'Hello World')
select
const selectElement = getByDisplayName(container, 'Alaska')
In case of select
, this will search for a <select>
whose selected <option>
matches the given TextMatch
.
getByRole
getByRole(
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement
A shortcut to container.querySelector(`[role="${yourRole}"]`)
(and it
also accepts a TextMatch
).
const dialogContainer = getByRole(container, 'dialog')
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
).
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-testid
s 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'})
.
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:
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,
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:
const usernameElement = await waitForElement(
() => getByLabelText(container, 'username'),
{container},
)
usernameElement.value = 'chucknorris'
You can also wait for multiple elements at once:
const [usernameElement, passwordElement] = await 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 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, attributes: true, characterData: true}
which will detect
additions and removals of child elements (including text nodes) in the container
and any of its descendants. It will also detect attribute changes.
waitForDomChange
function waitForDomChange<T>(options?: {
container?: HTMLElement
timeout?: number
mutationObserverOptions?: MutationObserverInit
}): Promise<T>
When in need to wait for the DOM to change you can use waitForDomChange
. The waitForDomChange
function is a small wrapper around the
MutationObserver
.
Here is an example where the promise will be resolved because the container is changed:
const container = document.createElement('div')
waitForDomChange({container})
.then(() => console.log('DOM changed!'))
.catch(err => console.log(`Error you need to deal with: ${err}`))
container.append(document.createElement('p'))
The promise will resolve with a mutationsList
which you can use to determine what kind of a change (or changes) affected the container
const container = document.createElement('div')
container.setAttribute('data-cool', 'true')
waitForDomChange({container}).then(mutationsList => {
const mutation = mutationsList[0]
console.log(
`was cool: ${mutation.oldValue}\ncurrently cool: ${
mutation.target.dataset.cool
}`,
)
})
container.setAttribute('data-cool', 'false')
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 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, attributes: true, characterData: true}
which will detect
additions and removals of child elements (including text nodes) in the container
and any of its descendants. It will also detect attribute changes.
fireEvent
fireEvent(node: HTMLElement, event: Event)
Fire DOM events.
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
.
const rightClick = {button: 2}
fireEvent.click(getByText('Submit'), rightClick)
target: When an event is dispatched on an element, the event has the
subjected element on a property called target
. As a convenience, if you
provide a target
property in the eventProperties
(second argument), then
those properties will be assigned to the node which is receiving the event.
This is particularly useful for a change event:
fireEvent.change(getByLabelText(/username/i), {target: {value: 'a'}})
fireEvent.change(getByLabelText(/picture/i), {
target: {
files: [new File(['(ββ‘_β‘)'], 'chucknorris.png', {type: 'image/png'})],
},
})
Keyboard events: There are three event types related to keyboard input - keyPress
, keyDown
, and keyUp
. When firing these you need to reference an element in the DOM and the key you want to fire.
fireEvent.keyDown(domNode, {key: 'Enter', code: 13})
You can find out which key code to use at https://keycode.info/.
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.
const text = getNodeText(container.querySelector('div'))
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, it is convenient to import a set of custom matchers that
make it easier to check several aspects of the state of a DOM element. For
example, you can use the ones provided by
jest-dom:
import 'jest-dom/extend-expect'
expect(queryByTestId(container, 'greetings')).not.toHaveTextContent('Bye bye')
Note: when using some of these matchers, you may need to 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.
Custom Queries
dom-testing-library
exposes many of the helper functions that are used to implement the default queries. You can use the helpers to build custom queries. For example, the code below shows a way to override the default testId
queries to use a different data-attribute. (Note: test files would import test-utils.js
instead of using dom-testing-library
directly).
const domTestingLib = require('dom-testing-library')
const {queryHelpers} = domTestingLib
export const queryByTestId = queryHelpers.queryByAttribute.bind(
null,
'data-test-id',
)
export const queryAllByTestId = queryHelpers.queryAllByAttribute.bind(
null,
'data-test-id',
)
export function getAllByTestId(container, id, ...rest) {
const els = queryAllByTestId(container, id, ...rest)
if (!els.length) {
throw queryHelpers.getElementError(
`Unable to find an element by: [data-test-id="${id}"]`,
container,
)
}
return els
}
export function getByTestId(...args) {
return queryHelpers.firstResultOrNull(getAllByTestId, ...args)
}
module.exports = {
...domTestingLib,
getByTestId,
getAllByTestId,
queryByTestId,
queryAllByTestId,
}
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
getByText(container, 'Hello World')
getByText(container, 'llo Worl', {exact: false})
getByText(container, 'hello world', {exact: false})
getByText(container, /World/)
getByText(container, /world/i)
getByText(container, /^hello world$/i)
getByText(container, /Hello W?oRlD/i)
getByText(container, (content, element) => content.startsWith('Hello'))
getByText(container, 'Goodbye World')
getByText(container, /hello world/)
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()
expect(submitButton).not.toBeTruthy()
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(submitButtons[0]).toBeTruthy()
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.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:
getByText(container, 'Goodbye world')
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))
This function is what also powers the automatic debugging output described above.
Configuration
The library can be configured via the configure
function, which accepts:
- a plain JS object; this will be merged into the existing configuration. e.g.
configure({testIdAttribute: 'not-data-testid'})
- a function; the function will be given the existing configuration, and should return a plain JS object which will be merged as above, e.g.
configure(existingConfig => ({something: [...existingConfig.something, 'extra value for the something array']}))
Configuration options:
testIdAttribute
: The attribute used by getByTestId
and related queries.
Defaults to data-testid
. See getByTestId
.
Great companion
A helper library named user-event
has been written in companion with dom-testing-library
which eases common interactions, such as typing, clicking etc. The type
-interaction will allow to easily dispatch the appropriate keyboard events for inserting text, or dispatch these events for each character of the to be typed text.
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:
Using Without Jest
If you're running your tests in the browser bundled with webpack (or similar)
then dom-testing-library
should work out of the box for you. However, most
people using dom-testing-library
are using it with
the Jest testing framework with the testEnvironment
set to jest-environment-jsdom
(which is the default configuration with Jest).
jsdom is a pure JavaScript implementation
of the DOM and browser APIs that runs in node. If you're not using Jest and
you would like to run your tests in Node, then you must install jsdom yourself.
There's also a package called
jsdom-global which can be used
to setup the global environment to simulate the browser APIs.
First, install jsdom and jsdom-global.
npm install --save-dev jsdom jsdom-global
With mocha, the test command would look something like this:
mocha --require jsdom-global/register
Note, depending on the version of Node you're running, you may also need to install
@babel/polyfill
(if you're using babel 7) or babel-polyfill
(for babel 6).
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:
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.getByPlaceholderText
: A placeholder is not a substitute for a label.
But if that's all you have, then it's better than alternatives.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.getByAltText
: If your element is one which supports alt
text
(img
, area
, and input
), then you can use this to find that element.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-testid
s (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 = [
]
const container = render()
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:
- If it relates to rendering components, it deals with DOM nodes rather than
component instances, nor should it encourage dealing with component
instances.
- 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.
- 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):
This project follows the all-contributors specification.
Contributions of any kind welcome!
LICENSE
MIT