Skip to content

Commit 74ecfa6

Browse files
author
Kent C. Dodds
committed
feat(render): add new query capabilities for improved tests
**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 74ecfa6

16 files changed

+528
-134
lines changed

Diff for: README.md

+179-29
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]
221+
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.
162226

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.
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,8 +270,39 @@ Feel free to contribute more!
193270

194271
## FAQ
195272

273+
**Which `get*` method should I use?**
274+
275+
<details>
276+
277+
<summary>expand for the answer</summary>
278+
279+
Based on [the Guiding Principles](#guiding-principles), your test should
280+
resemble how your code (component, page, etc.) as much as possible. With this
281+
in mind, we recommend this order of priority:
282+
283+
1. `getByLabelText`: Only really good for form fields, but this is the number 1
284+
method a user finds those elements, so it should be your top preference.
285+
2. `getByPlaceholderText`: [A placeholder is not a substitute for a label](https://www.nngroup.com/articles/form-design-placeholders/).
286+
But if that's all you have, then it's better than alternatives.
287+
3. `getByText`: Not useful for forms, but this is the number 1 method a user
288+
finds other elements (like buttons to click), so it should be your top
289+
preference for non-form elements.
290+
4. `getByTestId`: The user cannot see (or hear) these, so this is only
291+
recommended for cases where you can't match by text or it doesn't make sense
292+
(the text is dynamic).
293+
294+
Other than that, you can also use the `container` to query the rendered
295+
component as well (using the regular
296+
[`querySelector` API](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)).
297+
298+
</details>
299+
196300
**How do I update the props of a rendered component?**
197301

302+
<details>
303+
304+
<summary>expand for the answer</summary>
305+
198306
It'd probably be better if you test the component that's doing the prop updating
199307
to ensure that the props are being updated correctly (see
200308
[the Guiding Principles section](#guiding-principles)). That said, if you'd
@@ -215,8 +323,14 @@ expect(getByTestId('number-display').textContent).toBe('2')
215323
[Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/number-display.js)
216324
for a full example of this.
217325

326+
</details>
327+
218328
**If I can't use shallow rendering, how do I mock out components in tests?**
219329

330+
<details>
331+
332+
<summary>expand for the answer</summary>
333+
220334
In general, you should avoid mocking out components (see
221335
[the Guiding Principles section](#guiding-principles)). However if you need to,
222336
then it's pretty trivial using
@@ -265,15 +379,28 @@ something more
265379
Learn more about how Jest mocks work from my blog post:
266380
["But really, what is a JavaScript mock?"](https://blog.kentcdodds.com/but-really-what-is-a-javascript-mock-10d060966f7d)
267381

382+
</details>
383+
268384
**What if I want to verify that an element does NOT exist?**
269385

386+
<details>
387+
388+
<summary>expand for the answer</summary>
389+
270390
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.
271391

272392
```javascript
273393
expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
274394
```
275395

276-
**I don't want to use `data-testid` attributes for everything. Do I have to?**
396+
</details>
397+
398+
**I really don't like `data-testid`s, but none of the other queries make sense.
399+
Do I have to use a `data-testid`?**
400+
401+
<details>
402+
403+
<summary>expand for the answer</summary>
277404

278405
Definitely not. That said, a common reason people don't like the `data-testid`
279406
attribute is they're concerned about shipping that to production. I'd suggest
@@ -298,7 +425,14 @@ const allLisInDiv = container.querySelectorAll('div li')
298425
const rootElement = container.firstChild
299426
```
300427

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?**
428+
</details>
429+
430+
**What if I’m iterating over a list of items that I want to put the
431+
`data-testid="item"` attribute on. How do I distinguish them from each other?**
432+
433+
<details>
434+
435+
<summary>expand for the answer</summary>
302436

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

@@ -322,8 +456,14 @@ const {getByTestId} = render(/* your component with the items */)
322456
const thirdItem = getByTestId(`item-${items[2].id}`)
323457
```
324458

325-
**What about enzyme is "bloated with complexity and features" and "encourage poor testing
326-
practices"?**
459+
</details>
460+
461+
**What about enzyme is "bloated with complexity and features" and "encourage
462+
poor testing practices"?**
463+
464+
<details>
465+
466+
<summary></summary>
327467

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

335475
The guiding principle for this library is:
336476

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)
477+
> The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
338478
339479
Because users can't directly interact with your app's component instances,
340480
assert on their internal state or what components they render, or call their
@@ -345,8 +485,14 @@ That's not to say that there's never a use case for doing those things, so they
345485
should be possible to accomplish, just not the default and natural way to test
346486
react components.
347487

488+
</details>
489+
348490
**How does `flushPromises` work and why would I need it?**
349491

492+
<details>
493+
494+
<summary>expand for the answer</summary>
495+
350496
As mentioned [before](#flushpromises), `flushPromises` uses
351497
[`setImmediate`][set-immediate] to schedule resolving a promise after any pending
352498
tasks in
@@ -366,6 +512,8 @@ that this is only effective if you've mocked out your async requests to resolve
366512
immediately (like the `axios` mock we have in the examples). It will not `await`
367513
for promises that are not already resolved by the time you attempt to flush them.
368514

515+
</details>
516+
369517
## Other Solutions
370518

371519
In preparing this project,
@@ -378,7 +526,7 @@ this one instead.
378526

379527
## Guiding Principles
380528

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)
529+
> [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle]
382530
383531
We try to only expose methods and utilities that encourage you to write tests
384532
that closely resemble how your react components are used.
@@ -443,3 +591,5 @@ MIT
443591
[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
444592
[all-contributors]: https://github.com/kentcdodds/all-contributors
445593
[set-immediate]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
594+
[guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106
595+
[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)