Skip to content

Commit 8f31599

Browse files
committed
Added timeout to waitForNextUpdate
1 parent eb400b1 commit 8f31599

File tree

4 files changed

+109
-50
lines changed

4 files changed

+109
-50
lines changed

docs/api-reference.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ more hooks for testing.
3434
The `props` passed into the callback will be the `initialProps` provided in the `options` to
3535
`renderHook`, unless new props are provided by a subsequent `rerender` call.
3636

37-
### `options`
37+
### `options` (Optional)
3838

3939
An options object to modify the execution of the `callback` function. See the
4040
[`renderHook` Options](/reference/api#renderhook-options) section for more details.
@@ -68,15 +68,6 @@ The `renderHook` method returns an object that has a following properties:
6868
The `current` value or the `result` will reflect whatever is returned from the `callback` passed to
6969
`renderHook`. Any thrown values will be reflected in the `error` value of the `result`.
7070
71-
### `waitForNextUpdate`
72-
73-
```js
74-
function waitForNextUpdate(): Promise<void>
75-
```
76-
77-
- `waitForNextUpdate` (`function`) - returns a `Promise` that resolves the next time the hook
78-
renders, commonly when state is updated as the result of a asynchronous action
79-
8071
### `rerender`
8172
8273
```js
@@ -96,9 +87,39 @@ function unmount(): void
9687
A function to unmount the test component. This is commonly used to trigger cleanup effects for
9788
`useEffect` hooks.
9889

90+
### `...asyncUtils`
91+
92+
Utilities to assist with testing asynchronous behaviour. See the
93+
[Async Utils](/reference/api#async-utils) section for more details.
94+
9995
---
10096

10197
## `act`
10298

10399
This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) that is exported by
104100
`react-test-renderer`.
101+
102+
---
103+
104+
## Async Utilities
105+
106+
### `waitForNextUpdate`
107+
108+
```js
109+
function waitForNextUpdate(options?: WaitOptions): Promise<void>
110+
```
111+
112+
`waitForNextUpdate` returns a `Promise` that resolves the next time the hook renders, commonly when
113+
state is updated as the result of an asynchronous update.
114+
115+
An options object is accepted as the first parameter to modify it's execution. See the
116+
[`wait` Options](/reference/api#wait-options) section for more details.
117+
118+
### `wait` Options
119+
120+
The async utilities accepts the following options:
121+
122+
#### `timeout`
123+
124+
The amount of time in milliseconds (ms) to wait. By default, the `wait` utilities will wait
125+
indefinitely.

src/asyncUtils.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { act } from 'react-test-renderer'
2+
3+
function createTimeoutError(utilName, timeout) {
4+
const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`)
5+
timeoutError.timeout = true
6+
return timeoutError
7+
}
8+
9+
function asyncUtils(addResolver) {
10+
let nextUpdatePromise = null
11+
12+
const resolveOnNextUpdate = ({ timeout }) => (resolve, reject) => {
13+
let timeoutId
14+
if (timeout > 0) {
15+
timeoutId = setTimeout(
16+
() => reject(createTimeoutError('waitForNextUpdate', timeout)),
17+
timeout
18+
)
19+
}
20+
addResolver(() => {
21+
clearTimeout(timeoutId)
22+
nextUpdatePromise = null
23+
resolve()
24+
})
25+
}
26+
27+
const waitForNextUpdate = async (options = {}) => {
28+
if (!nextUpdatePromise) {
29+
nextUpdatePromise = new Promise(resolveOnNextUpdate(options))
30+
await act(() => nextUpdatePromise)
31+
}
32+
return await nextUpdatePromise
33+
}
34+
35+
return {
36+
waitForNextUpdate
37+
}
38+
}
39+
40+
export default asyncUtils

src/index.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { Suspense } from 'react'
22
import { act, create } from 'react-test-renderer'
3+
import asyncUtils from './asyncUtils'
34

45
function TestHook({ callback, hookProps, onError, children }) {
56
try {
@@ -73,20 +74,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
7374
})
7475
const { unmount, update } = testRenderer
7576

76-
let waitingForNextUpdate = null
77-
const resolveOnNextUpdate = (resolve) => {
78-
addResolver((...args) => {
79-
waitingForNextUpdate = null
80-
resolve(...args)
81-
})
82-
}
83-
8477
return {
8578
result,
86-
waitForNextUpdate: () => {
87-
waitingForNextUpdate = waitingForNextUpdate || act(() => new Promise(resolveOnNextUpdate))
88-
return waitingForNextUpdate
89-
},
9079
rerender: (newProps = hookProps.current) => {
9180
hookProps.current = newProps
9281
act(() => {
@@ -97,7 +86,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
9786
act(() => {
9887
unmount()
9988
})
100-
}
89+
},
90+
...asyncUtils(addResolver)
10191
}
10292
}
10393

test/asyncHook.test.js

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,66 @@
1-
import { useState, useEffect } from 'react'
1+
import { useState, useRef, useEffect } from 'react'
22
import { renderHook } from 'src'
33

44
describe('async hook tests', () => {
5-
const getSomeName = () => Promise.resolve('Betty')
6-
7-
const useName = (prefix) => {
8-
const [name, setName] = useState('nobody')
5+
const useSequence = (...values) => {
6+
const [first, ...otherValues] = values
7+
const [value, setValue] = useState(first)
8+
const index = useRef(0)
99

1010
useEffect(() => {
11-
getSomeName().then((theName) => {
12-
setName(prefix ? `${prefix} ${theName}` : theName)
13-
})
14-
}, [prefix])
15-
16-
return name
11+
const interval = setInterval(() => {
12+
setValue(otherValues[index.current])
13+
index.current++
14+
}, 50)
15+
return () => {
16+
clearInterval(interval)
17+
}
18+
}, [...values])
19+
20+
return value
1721
}
1822

1923
test('should wait for next update', async () => {
20-
const { result, waitForNextUpdate } = renderHook(() => useName())
24+
const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
2125

22-
expect(result.current).toBe('nobody')
26+
expect(result.current).toBe('first')
2327

2428
await waitForNextUpdate()
2529

26-
expect(result.current).toBe('Betty')
30+
expect(result.current).toBe('second')
2731
})
2832

2933
test('should wait for multiple updates', async () => {
30-
const { result, waitForNextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), {
31-
initialProps: { prefix: 'Mrs.' }
32-
})
34+
const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second', 'third'))
3335

34-
expect(result.current).toBe('nobody')
36+
expect(result.current).toBe('first')
3537

3638
await waitForNextUpdate()
3739

38-
expect(result.current).toBe('Mrs. Betty')
39-
40-
rerender({ prefix: 'Ms.' })
40+
expect(result.current).toBe('second')
4141

4242
await waitForNextUpdate()
4343

44-
expect(result.current).toBe('Ms. Betty')
44+
expect(result.current).toBe('third')
4545
})
4646

4747
test('should resolve all when updating', async () => {
48-
const { result, waitForNextUpdate } = renderHook(({ prefix }) => useName(prefix), {
49-
initialProps: { prefix: 'Mrs.' }
50-
})
48+
const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
5149

52-
expect(result.current).toBe('nobody')
50+
expect(result.current).toBe('first')
5351

5452
await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()])
5553

56-
expect(result.current).toBe('Mrs. Betty')
54+
expect(result.current).toBe('second')
55+
})
56+
57+
test('should reject if timeout exceeded when waiting for next update', async () => {
58+
const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second'))
59+
60+
expect(result.current).toBe('first')
61+
62+
await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow(
63+
Error('Timed out in waitForNextUpdate after 10ms.')
64+
)
5765
})
5866
})

0 commit comments

Comments
 (0)