Skip to content

Commit 2289371

Browse files
sompylasarKent C. Dodds
authored and
Kent C. Dodds
committed
feat(waitForElement): a wrapper around MutationObserver (#10)
* feat(waitForElements): add implementation, tests, docs Add `mutationobserver-shim` to `devDependencies` to provide `MutationObserver` in jest tests where `jsdom` has no built-in support for it: jsdom/jsdom#639 * docs(contributors): update sompylasar * CR changes - rename `waitForElements` to `waitForElement` - move `mutationobserver-shim` to dependencies, import from `waitForElement` to provide the polyfill to the users of `dom-testing-library` - fix `kcd-scripts` version to match `master` branch - add synchronous `callback` call to detect an element if it's already present before any DOM mutation happens - add/change tests about synchronous `callback` call - tweak variable names in docs examples - add docs about the default `container` option value - add docs example about querying multiple elements - add docs about the `mutationobserver-shim` polyfill - add docs link and anchor to `mutationObserverOptions` - add docs link to MDN from the second mention of `MutationObserver` * fix(waitForElement): ensure it works with default callback Should wait for the next DOM change, as advertised in the docs. The default value is `undefined` so that the `options` object can be used while still keeping the default callback: ``` waitForElement(undefined, {attributes: true}) ``` * CR: tweak docs examples for wait and waitForElement - use `container` in the examples as this is a more popular use case than the default value of global `document` - use full sentences with capital first letter and period in the example comments * CR: rename files to kebab-case * CR: await promise -> return promise @kentcdodds: > Rather than `await promise`, I'd prefer `return promise`. Maybe I'm being irrational here, but it feels better to me. @sompylasar: > I'm changing this, but if this line was the only one with `await` expression, then `eslint` would say `async` function must have an `await`. We are lucky that there are more `await`s in all the tests. > > P.S. I don't agree with this rule because `async` functions have their use for the error handling; `async` function is just the one that is wrapped in a `return new Promise(...)`. * CR: shorter timeouts and wait times for quicker tests
1 parent f77f943 commit 2289371

File tree

7 files changed

+483
-5
lines changed

7 files changed

+483
-5
lines changed

.all-contributorsrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@
115115
"profile": "https://sompylasar.github.io",
116116
"contributions": [
117117
"bug",
118-
"ideas"
118+
"ideas",
119+
"code",
120+
"doc"
119121
]
120122
},
121123
{

README.md

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ when a real user uses it.
7777
* [`getByText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbytextcontainer-htmlelement-text-textmatch-htmlelement)
7878
* [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement)
7979
* [`wait`](#wait)
80+
* [`waitForElement`](#waitforelement)
8081
* [Custom Jest Matchers](#custom-jest-matchers)
8182
* [`toBeInTheDOM`](#tobeinthedom)
8283
* [`toHaveTextContent`](#tohavetextcontent)
@@ -284,8 +285,8 @@ Here's a simple example:
284285

285286
```javascript
286287
// ...
287-
// wait until the callback does not throw an error. In this case, that means
288-
// it'll wait until we can get a form control with a label that matches "username"
288+
// Wait until the callback does not throw an error. In this case, that means
289+
// it'll wait until we can get a form control with a label that matches "username".
289290
await wait(() => getByLabelText(container, 'username'))
290291
getByLabelText(container, 'username').value = 'chucknorris'
291292
// ...
@@ -305,6 +306,68 @@ The default `interval` is `50ms`. However it will run your callback immediately
305306
on the next tick of the event loop (in a `setTimeout`) before starting the
306307
intervals.
307308

309+
### `waitForElement`
310+
311+
Defined as:
312+
313+
```typescript
314+
function waitForElement<T>(
315+
callback?: () => T | null | undefined,
316+
options?: {
317+
container?: HTMLElement
318+
timeout?: number
319+
mutationObserverOptions?: MutationObserverInit
320+
},
321+
): Promise<T>
322+
```
323+
324+
When in need to wait for DOM elements to appear, disappear, or change you can use `waitForElement`.
325+
The `waitForElement` function is a small wrapper
326+
around the
327+
[`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
328+
Here's a simple example:
329+
330+
```javascript
331+
// ...
332+
// Wait until the callback does not throw an error and returns a truthy value. In this case, that means
333+
// it'll wait until we can get a form control with a label that matches "username".
334+
// The difference from `wait` is that rather than running your callback on
335+
// an interval, it's run as soon as there are DOM changes in the container
336+
// and returns the value returned by the callback.
337+
const usernameElement = await waitForElement(
338+
() => getByLabelText(container, 'username'),
339+
{container},
340+
)
341+
usernameElement.value = 'chucknorris'
342+
// ...
343+
```
344+
345+
You can also wait for multiple elements at once:
346+
347+
```javascript
348+
const [usernameElement, passwordElement] = waitForElement(
349+
() => [
350+
getByLabelText(container, 'username'),
351+
getByLabelText(container, 'password'),
352+
],
353+
{container},
354+
)
355+
```
356+
357+
Using [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) is more efficient than polling the DOM at regular intervals with `wait`. This library sets up a [`'mutationobserver-shim'`](https://github.com/megawac/MutationObserver.js) on the global `window` object for cross-platform compatibility with older browsers and the [`jsdom`](https://github.com/jsdom/jsdom/issues/639) that is usually used in Node-based tests.
358+
359+
The default `callback` is a no-op function (used like `await waitForElement()`). This can
360+
be helpful if you only need to wait for the next DOM change (see [`mutationObserverOptions`](#mutationobserveroptions) to learn which changes are detected).
361+
362+
The default `container` is the global `document`. Make sure the elements you wait for will be attached to it, or set a different `container`.
363+
364+
The default `timeout` is `4500ms` which will keep you under
365+
[Jest's default timeout of `5000ms`](https://facebook.github.io/jest/docs/en/jest-object.html#jestsettimeouttimeout).
366+
367+
<a name="mutationobserveroptions"></a>The default `mutationObserverOptions` is `{subtree: true, childList: true}` which will detect
368+
additions and removals of child elements (including text nodes) in the `container` and any of its descendants.
369+
It won't detect attribute changes unless you add `attributes: true` to the options.
370+
308371
## Custom Jest Matchers
309372

310373
There are two simple API which extend the `expect` API of jest for making assertions easier.
@@ -599,7 +662,7 @@ Thanks goes to these people ([emoji key][emojis]):
599662
<!-- prettier-ignore -->
600663
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/kentcdodds/dom-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/kentcdodds/dom-testing-library/commits?author=alejandronanez "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1402095?v=4" width="100px;"/><br /><sub><b>Matt Parrish</b></sub>](https://github.com/pbomb)<br />[🐛](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Apbomb "Bug reports") [💻](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Code") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Tests") | [<img src="https://avatars1.githubusercontent.com/u/1288694?v=4" width="100px;"/><br /><sub><b>Justin Hall</b></sub>](https://github.com/wKovacs64)<br />[📦](#platform-wKovacs64 "Packaging/porting to new platform") |
601664
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
602-
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💬](#question-gnapse "Answering Questions") [💻](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") |
665+
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") [💻](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Code") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💬](#question-gnapse "Answering Questions") [💻](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [📖](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") |
603666

604667
<!-- ALL-CONTRIBUTORS-LIST:END -->
605668

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
],
3636
"dependencies": {
3737
"jest-matcher-utils": "^22.4.3",
38-
"wait-for-expect": "^0.4.0"
38+
"wait-for-expect": "^0.4.0",
39+
"mutationobserver-shim": "^0.3.2"
3940
},
4041
"devDependencies": {
4142
"jest-in-case": "^1.0.2",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[
4+
`it returns immediately if the callback returns the value before any mutations 1`
5+
] = `
6+
<div
7+
data-test-attribute="something changed once"
8+
>
9+
<div
10+
data-testid="initial-element"
11+
/>
12+
</div>
13+
`
14+
15+
exports[`it throws if timeout is exceeded 1`] = `
16+
Array [
17+
[Error: Timed out in waitForElement.],
18+
]
19+
`
20+
21+
exports[`it throws if timeout is exceeded 2`] = `
22+
<div
23+
data-test-attribute="something changed twice"
24+
/>
25+
`
26+
27+
exports[
28+
`it throws the same error that the callback has thrown if timeout is exceeded 1`
29+
] = `
30+
Array [
31+
[Error: Unable to find an element by: [data-testid="test"]],
32+
]
33+
`
34+
35+
exports[
36+
`it throws the same error that the callback has thrown if timeout is exceeded 2`
37+
] = `
38+
<div
39+
data-test-attribute="something changed twice"
40+
/>
41+
`
42+
43+
exports[
44+
`it waits for the callback to return a value and only reacts to DOM mutations 1`
45+
] = `
46+
<div>
47+
<div
48+
data-testid="initial-element"
49+
/>
50+
<div
51+
data-testid="another-element-that-causes-mutation-1"
52+
/>
53+
<div
54+
data-testid="another-element-that-causes-mutation-2"
55+
/>
56+
<div
57+
data-testid="another-element-that-causes-mutation-3"
58+
/>
59+
<div
60+
data-testid="another-element-that-causes-mutation-4"
61+
/>
62+
<div
63+
data-testid="another-element-that-causes-mutation-5"
64+
/>
65+
<div
66+
data-testid="the-element-we-are-looking-for"
67+
/>
68+
</div>
69+
`
70+
71+
exports[`it waits for the next DOM mutation with default callback 1`] = `
72+
<body>
73+
<div />
74+
</body>
75+
`
76+
77+
exports[`it waits for the next DOM mutation with default callback 2`] = `
78+
<body>
79+
<div />
80+
</body>
81+
`

0 commit comments

Comments
 (0)