Skip to content

Commit f972da8

Browse files
author
Alex
committed
causes waitForElement to throw an error if callback is not defined. changes tests to account for new behavior. removes the documentation around the default callback. adds MutationObserver documentation for waitForDomChange
1 parent cdb4633 commit f972da8

File tree

5 files changed

+76
-91
lines changed

5 files changed

+76
-91
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ intervals.
488488

489489
```typescript
490490
function waitForElement<T>(
491-
callback?: () => T | null | undefined,
491+
callback: () => T,
492492
options?: {
493493
container?: HTMLElement
494494
timeout?: number
@@ -531,9 +531,6 @@ const [usernameElement, passwordElement] = waitForElement(
531531

532532
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.
533533

534-
The default `callback` is a no-op function (used like `await waitForElement()`). This can
535-
be helpful if you only need to wait for the next DOM change (see [`mutationObserverOptions`](#mutationobserveroptions) to learn which changes are detected).
536-
537534
The default `container` is the global `document`. Make sure the elements you wait for will be attached to it, or set a different `container`.
538535

539536
The default `timeout` is `4500ms` which will keep you under
@@ -589,6 +586,16 @@ container.setAttribute('data-cool', 'false')
589586
*/
590587
```
591588

589+
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.
590+
591+
The default `container` is the global `document`. Make sure the elements you wait for will be attached to it, or set a different `container`.
592+
593+
The default `timeout` is `4500ms` which will keep you under
594+
[Jest's default timeout of `5000ms`](https://facebook.github.io/jest/docs/en/jest-object.html#jestsettimeouttimeout).
595+
596+
<a name="mutationobserveroptions"></a>The default `mutationObserverOptions` is `{subtree: true, childList: true, attributes: true, characterData: true}` which will detect
597+
additions and removals of child elements (including text nodes) in the `container` and any of its descendants. It will also detect attribute changes.
598+
592599
### `fireEvent`
593600

594601
```typescript

src/__tests__/__snapshots__/wait-for-element.js.snap

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,3 @@ exports[`it waits for the callback to return a value and only reacts to DOM muta
7979
/>
8080
</div>
8181
`;
82-
83-
exports[`it waits for the next DOM mutation with default callback 1`] = `
84-
<body>
85-
<div />
86-
</body>
87-
`;
88-
89-
exports[`it waits for the next DOM mutation with default callback 2`] = `
90-
<body>
91-
<div />
92-
</body>
93-
`;

src/__tests__/wait-for-element.js

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import {waitForElement, wait} from '../'
33
import 'jest-dom/extend-expect'
44
import {render} from './helpers/test-utils'
55

6-
async function skipSomeTime(delayMs) {
7-
await new Promise(resolve => setTimeout(resolve, delayMs))
8-
}
6+
const skipSomeTime = delayMs =>
7+
new Promise(resolve => setTimeout(resolve, delayMs))
98

10-
async function skipSomeTimeForMutationObserver(delayMs = 50) {
11-
// Using `setTimeout` >30ms instead of `wait` here because `mutationobserver-shim` uses `setTimeout` ~30ms.
12-
await skipSomeTime(delayMs, 50)
13-
}
9+
// Using `setTimeout` >30ms instead of `wait` here because `mutationobserver-shim` uses `setTimeout` ~30ms.
10+
const skipSomeTimeForMutationObserver = (delayMs = 50) =>
11+
skipSomeTime(delayMs, 50)
1412

1513
test('it waits for the callback to return a value and only reacts to DOM mutations', async () => {
1614
const {container, getByTestId} = render(
@@ -66,10 +64,7 @@ test('it waits for the callback to return a value and only reacts to DOM mutatio
6664
const successHandler = jest.fn().mockName('successHandler')
6765
const errorHandler = jest.fn().mockName('errorHandler')
6866

69-
const promise = waitForElement(callback, {container}).then(
70-
successHandler,
71-
errorHandler,
72-
)
67+
waitForElement(callback, {container}).then(successHandler, errorHandler)
7368

7469
// One synchronous `callback` call is expected.
7570
expect(callback).toHaveBeenCalledTimes(1)
@@ -96,37 +91,6 @@ test('it waits for the callback to return a value and only reacts to DOM mutatio
9691
expect(errorHandler).toHaveBeenCalledTimes(0)
9792
expect(container).toMatchSnapshot()
9893
expect(testEl.parentNode).toBe(container)
99-
100-
return promise
101-
})
102-
103-
test('it waits for the next DOM mutation with default callback', async () => {
104-
const successHandler = jest.fn().mockName('successHandler')
105-
const errorHandler = jest.fn().mockName('errorHandler')
106-
107-
const promise = waitForElement().then(successHandler, errorHandler)
108-
109-
// Promise callbacks are always asynchronous.
110-
expect(successHandler).toHaveBeenCalledTimes(0)
111-
expect(errorHandler).toHaveBeenCalledTimes(0)
112-
113-
await skipSomeTimeForMutationObserver()
114-
115-
// No more expected calls without DOM mutations.
116-
expect(successHandler).toHaveBeenCalledTimes(0)
117-
expect(errorHandler).toHaveBeenCalledTimes(0)
118-
119-
document.body.appendChild(document.createElement('div'))
120-
expect(document.body).toMatchSnapshot()
121-
122-
await skipSomeTimeForMutationObserver()
123-
124-
expect(successHandler).toHaveBeenCalledTimes(1)
125-
expect(successHandler).toHaveBeenCalledWith(undefined)
126-
expect(errorHandler).toHaveBeenCalledTimes(0)
127-
expect(document.body).toMatchSnapshot()
128-
129-
return promise
13094
})
13195

13296
test('it waits characterData mutation', async () => {
@@ -138,10 +102,7 @@ test('it waits characterData mutation', async () => {
138102
const successHandler = jest.fn().mockName('successHandler')
139103
const errorHandler = jest.fn().mockName('errorHandler')
140104

141-
const promise = waitForElement(callback, {container}).then(
142-
successHandler,
143-
errorHandler,
144-
)
105+
waitForElement(callback, {container}).then(successHandler, errorHandler)
145106

146107
// Promise callbacks are always asynchronous.
147108
expect(successHandler).toHaveBeenCalledTimes(0)
@@ -162,8 +123,6 @@ test('it waits characterData mutation', async () => {
162123
expect(errorHandler).toHaveBeenCalledTimes(0)
163124
expect(callback).toHaveBeenCalledTimes(2)
164125
expect(container).toMatchSnapshot()
165-
166-
return promise
167126
})
168127

169128
test('it waits for the attributes mutation', async () => {
@@ -175,7 +134,7 @@ test('it waits for the attributes mutation', async () => {
175134
const successHandler = jest.fn().mockName('successHandler')
176135
const errorHandler = jest.fn().mockName('errorHandler')
177136

178-
const promise = waitForElement(callback, {
137+
waitForElement(callback, {
179138
container,
180139
}).then(successHandler, errorHandler)
181140

@@ -196,8 +155,6 @@ test('it waits for the attributes mutation', async () => {
196155
expect(successHandler).toHaveBeenCalledTimes(1)
197156
expect(successHandler).toHaveBeenCalledWith('PASSED')
198157
expect(errorHandler).toHaveBeenCalledTimes(0)
199-
200-
return promise
201158
})
202159

203160
test('it throws if timeout is exceeded', async () => {
@@ -207,7 +164,7 @@ test('it throws if timeout is exceeded', async () => {
207164
const successHandler = jest.fn().mockName('successHandler')
208165
const errorHandler = jest.fn().mockName('errorHandler')
209166

210-
const promise = waitForElement(callback, {
167+
waitForElement(callback, {
211168
container,
212169
timeout: 70,
213170
mutationObserverOptions: {attributes: true},
@@ -232,8 +189,6 @@ test('it throws if timeout is exceeded', async () => {
232189
expect(errorHandler).toHaveBeenCalledTimes(1)
233190
expect(errorHandler.mock.calls[0]).toMatchSnapshot()
234191
expect(container).toMatchSnapshot()
235-
236-
return promise
237192
})
238193

239194
test('it throws the same error that the callback has thrown if timeout is exceeded', async () => {
@@ -243,7 +198,7 @@ test('it throws the same error that the callback has thrown if timeout is exceed
243198
const successHandler = jest.fn().mockName('successHandler')
244199
const errorHandler = jest.fn().mockName('errorHandler')
245200

246-
const promise = waitForElement(callback, {
201+
waitForElement(callback, {
247202
container,
248203
timeout: 70,
249204
mutationObserverOptions: {attributes: true},
@@ -268,8 +223,6 @@ test('it throws the same error that the callback has thrown if timeout is exceed
268223
expect(errorHandler).toHaveBeenCalledTimes(1)
269224
expect(errorHandler.mock.calls[0]).toMatchSnapshot()
270225
expect(container).toMatchSnapshot()
271-
272-
return promise
273226
})
274227

275228
test('it returns immediately if the callback returns the value before any mutations', async () => {
@@ -283,7 +236,7 @@ test('it returns immediately if the callback returns the value before any mutati
283236
const successHandler = jest.fn().mockName('successHandler')
284237
const errorHandler = jest.fn().mockName('errorHandler')
285238

286-
const promise = waitForElement(callback, {
239+
waitForElement(callback, {
287240
container,
288241
timeout: 70,
289242
mutationObserverOptions: {attributes: true},
@@ -309,8 +262,6 @@ test('it returns immediately if the callback returns the value before any mutati
309262
expect(errorHandler).toHaveBeenCalledTimes(0)
310263

311264
expect(container).toMatchSnapshot()
312-
313-
return promise
314265
})
315266

316267
test('does not get into infinite setTimeout loop after MutationObserver notification', async () => {
@@ -356,4 +307,47 @@ test('does not get into infinite setTimeout loop after MutationObserver notifica
356307
// Expect no more setTimeout calls
357308
jest.advanceTimersByTime(100)
358309
expect(setTimeout).toHaveBeenCalledTimes(3)
310+
jest.useRealTimers()
311+
})
312+
313+
test('works if a container is not defined', async () => {
314+
render(``)
315+
const el = document.createElement('p')
316+
document.body.appendChild(el)
317+
const callback = jest
318+
.fn(() => el.textContent === 'I changed!')
319+
.mockName('callback')
320+
const successHandler = jest.fn().mockName('successHandler')
321+
const errorHandler = jest.fn().mockName('errorHandler')
322+
323+
waitForElement(callback).then(successHandler, errorHandler)
324+
325+
await skipSomeTimeForMutationObserver()
326+
327+
expect(callback).toHaveBeenCalledTimes(1)
328+
expect(successHandler).toHaveBeenCalledTimes(0)
329+
expect(errorHandler).toHaveBeenCalledTimes(0)
330+
331+
el.innerHTML = 'I changed!'
332+
await skipSomeTimeForMutationObserver()
333+
334+
expect(callback).toHaveBeenCalledTimes(2)
335+
expect(successHandler).toHaveBeenCalledTimes(1)
336+
expect(errorHandler).toHaveBeenCalledTimes(0)
337+
338+
document.getElementsByTagName('html')[0].innerHTML = '' // cleans the document
339+
})
340+
341+
test('throws an error if callback is not a function', async () => {
342+
const successHandler = jest.fn().mockName('successHandler')
343+
const errorHandler = jest.fn().mockName('errorHandler')
344+
345+
waitForElement().then(successHandler, errorHandler)
346+
347+
await skipSomeTimeForMutationObserver()
348+
349+
expect(errorHandler).toHaveBeenLastCalledWith(
350+
'waitForElement requires a callback as the first parameter',
351+
)
352+
expect(successHandler).toHaveBeenCalledTimes(0)
359353
})

src/wait-for-element.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'mutationobserver-shim'
22

33
function waitForElement(
4-
callback = undefined,
4+
callback,
55
{
66
container = document,
77
timeout = 4500,
@@ -14,8 +14,13 @@ function waitForElement(
1414
} = {},
1515
) {
1616
return new Promise((resolve, reject) => {
17-
// Disabling eslint prefer-const below: either prefer-const or no-use-before-define triggers.
18-
let lastError, observer, timer // eslint-disable-line prefer-const
17+
if (typeof callback !== 'function') {
18+
reject('waitForElement requires a callback as the first parameter')
19+
}
20+
let lastError
21+
const timer = setTimeout(onTimeout, timeout)
22+
const observer = new window.MutationObserver(onMutation)
23+
observer.observe(container, mutationObserverOptions)
1924
function onDone(error, result) {
2025
clearTimeout(timer)
2126
setImmediate(() => observer.disconnect())
@@ -26,10 +31,6 @@ function waitForElement(
2631
}
2732
}
2833
function onMutation() {
29-
if (callback === undefined) {
30-
onDone(null, undefined)
31-
return
32-
}
3334
try {
3435
const result = callback()
3536
if (result) {
@@ -45,12 +46,7 @@ function waitForElement(
4546
function onTimeout() {
4647
onDone(lastError || new Error('Timed out in waitForElement.'), null)
4748
}
48-
timer = setTimeout(onTimeout, timeout)
49-
observer = new window.MutationObserver(onMutation)
50-
observer.observe(container, mutationObserverOptions)
51-
if (callback !== undefined) {
52-
onMutation()
53-
}
49+
onMutation()
5450
})
5551
}
5652

typings/wait-for-element.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export function waitForElement<T>(
2-
callback?: () => T,
2+
callback: () => T,
33
options?: {
44
container?: HTMLElement
55
timeout?: number
66
mutationObserverOptions?: MutationObserverInit
77
},
8-
): Promise<T | undefined>
8+
): Promise<T>

0 commit comments

Comments
 (0)