Skip to content

Commit 12c7ed1

Browse files
author
Kent C. Dodds
authored
feat(render): add new query capabilities for improved tests (#17)
**What**: Add the following methods - queryByText - getByText - queryByPlaceholderText - getByPlaceholderText - queryByLabelText - getByLabelText **Why**: Closes #16 These will really improve the usability of this module. These also align much better with the guiding principles 👍 **How**: - Created a `queries.js` file where we have all the logic for the queries and their associated getter functions - Migrate tests where it makes sense - Update docs considerably. **Checklist**: * [x] Documentation * [x] Tests * [x] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? --> * [ ] Added myself to contributors table N/A
1 parent 2eb804a commit 12c7ed1

16 files changed

+541
-138
lines changed

Diff for: README.md

+192-33
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ components. It provides light utility functions on top of `react-dom` and
4848
* [`Simulate`](#simulate)
4949
* [`flushPromises`](#flushpromises)
5050
* [`render`](#render)
51-
* [More on `data-testid`s](#more-on-data-testids)
51+
* [`TextMatch`](#textmatch)
52+
* [`query` APIs](#query-apis)
5253
* [Examples](#examples)
5354
* [FAQ](#faq)
5455
* [Other Solutions](#other-solutions)
@@ -76,7 +77,7 @@ This library has a `peerDependencies` listing for `react-dom`.
7677
import React from 'react'
7778
import {render, Simulate, flushPromises} from 'react-testing-library'
7879
import axiosMock from 'axios'
79-
import Fetch from '../fetch'
80+
import Fetch from '../fetch' // see the tests for a full implementation
8081

8182
test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
8283
// Arrange
@@ -86,10 +87,10 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
8687
}),
8788
)
8889
const url = '/greeting'
89-
const {getByTestId, container} = render(<Fetch url={url} />)
90+
const {getByText, getByTestId, container} = render(<Fetch url={url} />)
9091

9192
// Act
92-
Simulate.click(getByTestId('load-greeting'))
93+
Simulate.click(getByText('Load Greeting'))
9394

9495
// let's wait for our mocked `get` request promise to resolve
9596
await flushPromises()
@@ -146,39 +147,115 @@ unmount()
146147
// your component has been unmounted and now: container.innerHTML === ''
147148
```
148149

150+
#### `getByLabelText(text: TextMatch, options: {selector: string = '*'}): HTMLElement`
151+
152+
This will search for the label that matches the given [`TextMatch`](#textmatch),
153+
then find the element associated with that label.
154+
155+
```javascript
156+
const inputNode = getByLabelText('Username')
157+
158+
// this would find the input node for the following DOM structures:
159+
// The "for" attribute (NOTE: in JSX with React you'll write "htmlFor" rather than "for")
160+
// <label for="username-input">Username</label>
161+
// <input id="username-input" />
162+
//
163+
// The aria-labelledby attribute
164+
// <label id="username-label">Username</label>
165+
// <input aria-labelledby="username-label" />
166+
//
167+
// Wrapper labels
168+
// <label>Username <input /></label>
169+
//
170+
// It will NOT find the input node for this:
171+
// <label><span>Username</span> <input /></label>
172+
//
173+
// For this case, you can provide a `selector` in the options:
174+
const inputNode = getByLabelText('username-input', {selector: 'input'})
175+
// and that would work
176+
```
177+
178+
> Note: This method will throw an error if it cannot find the node. If you don't
179+
> want this behavior (for example you wish to assert that it doesn't exist),
180+
> then use `queryByLabelText` instead.
181+
182+
#### `getByPlaceholderText(text: TextMatch): HTMLElement`
183+
184+
This will search for all elements with a placeholder attribute and find one
185+
that matches the given [`TextMatch`](#textmatch).
186+
187+
```javascript
188+
// <input placeholder="Username" />
189+
const inputNode = getByPlaceholderText('Username')
190+
```
191+
192+
> NOTE: a placeholder is not a good substitute for a label so you should
193+
> generally use `getByLabelText` instead.
194+
195+
#### `getByText(text: TextMatch): HTMLElement`
196+
197+
This will search for all elements that have a text node with `textContent`
198+
matching the given [`TextMatch`](#textmatch).
199+
200+
```javascript
201+
// <a href="/about">About ℹ️</a>
202+
const aboutAnchorNode = getByText('about')
203+
```
204+
149205
#### `getByTestId`
150206

151-
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` except
152-
that it will throw an Error if no matching element is found. Read more about
153-
`data-testid`s below.
207+
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``.
154208

155209
```javascript
210+
// <input data-testid="username-input" />
156211
const usernameInputElement = getByTestId('username-input')
157-
usernameInputElement.value = 'new value'
158-
Simulate.change(usernameInputElement)
159212
```
160213

161-
#### `queryByTestId`
214+
> In the spirit of [the guiding principles](#guiding-principles), it is
215+
> recommended to use this only after `getByLabel`, `getByPlaceholderText` or
216+
> `getByText` don't work for your use case. Using data-testid attributes do
217+
> not resemble how your software is used and should be avoided if possible.
218+
> That said, they are _way_ better than querying based on DOM structure.
219+
> Learn more about `data-testid`s from the blog post
220+
> ["Making your UI tests resilient to change"][data-testid-blog-post]
162221
163-
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``
164-
(Note: just like `querySelector`, this could return null if no matching element
165-
is found, which may lead to harder-to-understand error messages). Read more about
166-
`data-testid`s below.
222+
## `TextMatch`
223+
224+
Several APIs accept a `TextMatch` which can be a `string`, `regex` or a
225+
`function` which returns `true` for a match and `false` for a mismatch.
226+
227+
Here's an example
167228

168229
```javascript
169-
// assert something doesn't exist
170-
// (you couldn't do this with `getByTestId`)
171-
expect(queryByTestId('username-input')).toBeNull()
230+
// <div>Hello World</div>
231+
// all of the following will find the div
232+
getByText('Hello World') // full match
233+
getByText('llo worl') // substring match
234+
getByText('hello world') // strings ignore case
235+
getByText(/Hello W?oRlD/i) // regex
236+
getByText((content, element) => content.startsWith('Hello')) // function
237+
238+
// all of the following will NOT find the div
239+
getByText('Goodbye World') // non-string match
240+
getByText(/hello world/) // case-sensitive regex with different case
241+
// function looking for a span when it's actually a div
242+
getByText((content, element) => {
243+
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
244+
})
172245
```
173246

174-
## More on `data-testid`s
247+
## `query` APIs
175248

176-
The `getByTestId` and `queryByTestId` utilities refer to the practice of using `data-testid`
177-
attributes to identify individual elements in your rendered component. This is
178-
one of the practices this library is intended to encourage.
249+
Each of the `get` APIs listed in [the `render`](#render) section above have a
250+
complimentary `query` API. The `get` APIs will throw errors if a proper node
251+
cannot be found. This is normally the desired effect. However, if you want to
252+
make an assertion that an element is _not_ present in the DOM, then you can use
253+
the `query` API instead:
179254

180-
Learn more about this practice in the blog post:
181-
["Making your UI tests resilient to change"](https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d37a6ee37269)
255+
```javascript
256+
const submitButton = queryByText('submit')
257+
expect(submitButton).toBeNull() // it doesn't exist
258+
```
182259

183260
## Examples
184261

@@ -193,7 +270,61 @@ Feel free to contribute more!
193270

194271
## FAQ
195272

196-
**How do I update the props of a rendered component?**
273+
<details>
274+
275+
<summary>Which get method should I use?</summary>
276+
277+
Based on [the Guiding Principles](#guiding-principles), your test should
278+
resemble how your code (component, page, etc.) as much as possible. With this
279+
in mind, we recommend this order of priority:
280+
281+
1. `getByLabelText`: Only really good for form fields, but this is the number 1
282+
method a user finds those elements, so it should be your top preference.
283+
2. `getByPlaceholderText`: [A placeholder is not a substitute for a label](https://www.nngroup.com/articles/form-design-placeholders/).
284+
But if that's all you have, then it's better than alternatives.
285+
3. `getByText`: Not useful for forms, but this is the number 1 method a user
286+
finds other elements (like buttons to click), so it should be your top
287+
preference for non-form elements.
288+
4. `getByTestId`: The user cannot see (or hear) these, so this is only
289+
recommended for cases where you can't match by text or it doesn't make sense
290+
(the text is dynamic).
291+
292+
Other than that, you can also use the `container` to query the rendered
293+
component as well (using the regular
294+
[`querySelector` API](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)).
295+
296+
</details>
297+
298+
<details>
299+
300+
<summary>Can I write unit tests with this library?</summary>
301+
302+
Definitely yes! You can write unit and integration tests with this library.
303+
See below for more on how to mock dependencies (because this library
304+
intentionally does NOT support shallow rendering) if you want to unit test a
305+
high level component. The tests in this project show several examples of
306+
unit testing with this library.
307+
308+
As you write your tests, keep in mind:
309+
310+
> The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
311+
312+
</details>
313+
314+
<details>
315+
316+
<summary>What if my app is localized and I don't have access to the text in test?</summary>
317+
318+
This is fairly common. Our first bit of advice is to try to get the default
319+
text used in your tests. That will make everything much easier (more than just
320+
using this utility). If that's not possible, then you're probably best
321+
to just stick with `data-testid`s (which is not bad anyway).
322+
323+
</details>
324+
325+
<details>
326+
327+
<summary>How do I update the props of a rendered component?</summary>
197328

198329
It'd probably be better if you test the component that's doing the prop updating
199330
to ensure that the props are being updated correctly (see
@@ -215,7 +346,11 @@ expect(getByTestId('number-display').textContent).toBe('2')
215346
[Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/number-display.js)
216347
for a full example of this.
217348

218-
**If I can't use shallow rendering, how do I mock out components in tests?**
349+
</details>
350+
351+
<details>
352+
353+
<summary>If I can't use shallow rendering, how do I mock out components in tests?</summary>
219354

220355
In general, you should avoid mocking out components (see
221356
[the Guiding Principles section](#guiding-principles)). However if you need to,
@@ -265,15 +400,23 @@ something more
265400
Learn more about how Jest mocks work from my blog post:
266401
["But really, what is a JavaScript mock?"](https://blog.kentcdodds.com/but-really-what-is-a-javascript-mock-10d060966f7d)
267402

268-
**What if I want to verify that an element does NOT exist?**
403+
</details>
404+
405+
<details>
406+
407+
<summary>What if I want to verify that an element does NOT exist?</summary>
269408

270409
You typically will get access to rendered elements using the `getByTestId` utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the `queryByTestId` utility which will return the element if found or `null` if not.
271410

272411
```javascript
273412
expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
274413
```
275414

276-
**I don't want to use `data-testid` attributes for everything. Do I have to?**
415+
</details>
416+
417+
<details>
418+
419+
<summary>I really don't like data-testids, but none of the other queries make sense. Do I have to use a data-testid?</summary>
277420

278421
Definitely not. That said, a common reason people don't like the `data-testid`
279422
attribute is they're concerned about shipping that to production. I'd suggest
@@ -298,7 +441,11 @@ const allLisInDiv = container.querySelectorAll('div li')
298441
const rootElement = container.firstChild
299442
```
300443

301-
**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?**
444+
</details>
445+
446+
<details>
447+
448+
<summary>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?</summary>
302449

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

@@ -322,8 +469,12 @@ const {getByTestId} = render(/* your component with the items */)
322469
const thirdItem = getByTestId(`item-${items[2].id}`)
323470
```
324471

325-
**What about enzyme is "bloated with complexity and features" and "encourage poor testing
326-
practices"?**
472+
</details>
473+
474+
<details>
475+
476+
<summary>What about enzyme is "bloated with complexity and features" and "encourage
477+
poor testing practices"?</summary>
327478

328479
Most of the damaging features have to do with encouraging testing implementation
329480
details. Primarily, these are
@@ -334,7 +485,7 @@ state/properties) (most of enzyme's wrapper APIs allow this).
334485

335486
The guiding principle for this library is:
336487

337-
> The less your tests resemble the way your software is used, the less confidence they can give you. - [17 Feb 2018](https://twitter.com/kentcdodds/status/965052178267176960)
488+
> The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
338489
339490
Because users can't directly interact with your app's component instances,
340491
assert on their internal state or what components they render, or call their
@@ -345,7 +496,11 @@ That's not to say that there's never a use case for doing those things, so they
345496
should be possible to accomplish, just not the default and natural way to test
346497
react components.
347498

348-
**How does `flushPromises` work and why would I need it?**
499+
</details>
500+
501+
<details>
502+
503+
<summary>How does flushPromises work and why would I need it?</summary>
349504

350505
As mentioned [before](#flushpromises), `flushPromises` uses
351506
[`setImmediate`][set-immediate] to schedule resolving a promise after any pending
@@ -366,6 +521,8 @@ that this is only effective if you've mocked out your async requests to resolve
366521
immediately (like the `axios` mock we have in the examples). It will not `await`
367522
for promises that are not already resolved by the time you attempt to flush them.
368523

524+
</details>
525+
369526
## Other Solutions
370527

371528
In preparing this project,
@@ -378,7 +535,7 @@ this one instead.
378535

379536
## Guiding Principles
380537

381-
> [The less your tests resemble the way your software is used, the less confidence they can give you.](https://twitter.com/kentcdodds/status/965052178267176960)
538+
> [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle]
382539
383540
We try to only expose methods and utilities that encourage you to write tests
384541
that closely resemble how your react components are used.
@@ -443,3 +600,5 @@ MIT
443600
[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
444601
[all-contributors]: https://github.com/kentcdodds/all-contributors
445602
[set-immediate]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
603+
[guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106
604+
[data-testid-blog-post]: https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d37a6ee37269

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@types/react-dom": "^16.0.4",
3030
"axios": "^0.18.0",
3131
"history": "^4.7.2",
32+
"jest-in-case": "^1.0.2",
3233
"kcd-scripts": "^0.36.1",
3334
"react": "^16.2.0",
3435
"react-dom": "^16.2.0",

Diff for: src/__tests__/__snapshots__/element-queries.js.snap

+11-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`getByTestId finds matching element 1`] = `
4-
<span
5-
data-testid="test-component"
6-
/>
7-
`;
8-
9-
exports[`getByTestId throws error when no matching element exists 1`] = `"Unable to find element by [data-testid=\\"unknown-data-testid\\"]"`;
10-
11-
exports[`queryByTestId finds matching element 1`] = `
12-
<span
13-
data-testid="test-component"
14-
/>
15-
`;
3+
exports[`get throws a useful error message 1`] = `"Unable to find a label with the text of: LucyRicardo"`;
4+
5+
exports[`get throws a useful error message 2`] = `"Unable to find an element with the placeholder text of: LucyRicardo"`;
6+
7+
exports[`get throws a useful error message 3`] = `"Unable to find an element with the text: LucyRicardo. 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."`;
8+
9+
exports[`get throws a useful error message 4`] = `"Unable to find an element by: [data-testid=\\"LucyRicardo\\"]"`;
10+
11+
exports[`label with no form control 1`] = `"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`;
12+
13+
exports[`totally empty label 1`] = `"Found a label with the text of: , however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`;

Diff for: src/__tests__/__snapshots__/fetch.js.snap

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22

33
exports[`Fetch makes an API call and displays the greeting when load-greeting is clicked 1`] = `
44
<div>
5-
<button
6-
data-testid="load-greeting"
7-
>
5+
<button>
86
Fetch
97
</button>
10-
<span
11-
data-testid="greeting-text"
12-
>
8+
<span>
139
hello there
1410
</span>
1511
</div>

0 commit comments

Comments
 (0)