Skip to content

Commit c5099d9

Browse files
committed
Refactored waitForValueToChange to use wait under the hook and updated docs
1 parent 72880f8 commit c5099d9

File tree

4 files changed

+167
-78
lines changed

4 files changed

+167
-78
lines changed

docs/api-reference.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,32 +103,33 @@ This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act)
103103

104104
## Async Utilities
105105

106-
### `wait`
106+
### `waitForNextUpdate`
107107

108108
```js
109-
function wait(callback: function(): boolean|void, options?: WaitOptions): Promise<void>
109+
function waitForNextUpdate(options?: WaitOptions): Promise<void>
110110
```
111111

112-
Returns a `Promise` that resolves if the provided callback executes without exception and returns a
113-
truthy or `undefined` value. It is safe to use the [`result` of `renderHook`](/reference/api#result)
114-
in the callback to perform assertion or to test values.
115-
116-
The callback is tested after each render of the hook.
112+
Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as
113+
the result of an asynchronous update.
117114

118-
See the [`wait` Options](/reference/api#wait-options) section for more details on the optional
119-
`options` parameter.
115+
See the [`wait` Options](/reference/api#wait-options) section for more details on the available
116+
`options`.
120117

121-
### `waitForNextUpdate`
118+
### `wait`
122119

123120
```js
124-
function waitForNextUpdate(options?: WaitOptions): Promise<void>
121+
function wait(callback: function(): boolean|void, options?: WaitOptions): Promise<void>
125122
```
126123

127-
Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as
128-
the result of an asynchronous update.
124+
Returns a `Promise` that resolves if the provided callback executes without exception and returns a
125+
truthy or `undefined` value. It is safe to use the [`result` of `renderHook`](/reference/api#result)
126+
in the callback to perform assertion or to test values.
127+
128+
The callback is tested after each render of the hook. By default, errors raised from the callback
129+
will be suppressed (`suppressErrors = true`).
129130

130-
See the [`wait` Options](/reference/api#wait-options) section for more details on the optional
131-
`options` parameter.
131+
See the [`wait` Options](/reference/api#wait-options) section for more details on the available
132+
`options`.
132133

133134
### `waitForValueToChange`
134135

@@ -140,10 +141,11 @@ Returns a `Promise` that resolves if the value returned from the provided select
140141
expected that the [`result` of `renderHook`](/reference/api#result) to select the value for
141142
comparison.
142143

143-
The value is selected for comparison after each render of the hook.
144+
The value is selected for comparison after each render of the hook. By default, errors raised from
145+
selecting the value will not be suppressed (`suppressErrors = false`).
144146

145-
See the [`wait` Options](/reference/api#wait-options) section for more details on the optional
146-
`options` parameter.
147+
See the [`wait` Options](/reference/api#wait-options) section for more details on the available
148+
`options`.
147149

148150
### `wait` Options
149151

@@ -152,3 +154,10 @@ The async utilities accepts the following options:
152154
#### `timeout`
153155

154156
The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied.
157+
158+
#### `suppressErrors`
159+
160+
If this option is set to `true`, any errors that occur while waiting are treated as a failed check.
161+
If this option is set to `false`, any errors that occur while waiting cause the promise to be
162+
rejected. Please refer to the [utility descriptions](/reference/api#async-utilities) for the default
163+
values of this option (if applicable).

docs/usage/advanced-hooks.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ you, your team, and your project.
9595
## Async
9696

9797
Sometimes, a hook can trigger asynchronous updates that will not be immediately reflected in the
98-
`result.current` value. Luckily, `renderHook` returns a utility that allows the test to wait for the
99-
hook to update using `async/await` (or just promise callbacks if you prefer) called
100-
`waitForNextUpdate`.
98+
`result.current` value. Luckily, `renderHook` returns some utilities that allows the test to wait
99+
for the hook to update using `async/await` (or just promise callbacks if you prefer). The most basic
100+
async utility is called `waitForNextUpdate`.
101101

102102
Let's further extend `useCounter` to have an `incrementAsync` callback that will update the `count`
103103
after `100ms`:
@@ -132,11 +132,14 @@ test('should increment counter after delay', async () => {
132132
})
133133
```
134134

135+
For more details on the the other async utilities, please refer to the
136+
[API Reference](/reference/api#async-utilities).
137+
135138
### Suspense
136139

137-
`waitForNextUpdate` will also wait for hooks that suspends using
138-
[React's `Suspense`](https://reactjs.org/docs/code-splitting.html#suspense) functionality finish
139-
rendering.
140+
All the [async utilities](/reference/api#async-utilities) will also wait for hooks that suspends
141+
using [React's `Suspense`](https://reactjs.org/docs/code-splitting.html#suspense) functionality to
142+
complete rendering.
140143

141144
## Errors
142145

src/asyncUtils.js

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import { act } from 'react-test-renderer'
22

3-
function actForResult(callback) {
4-
let value
5-
act(() => {
6-
value = callback()
7-
})
8-
return value
9-
}
10-
11-
function createTimeoutError(utilName, timeout) {
3+
function createTimeoutError(utilName, { timeout }) {
124
const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`)
135
timeoutError.timeout = true
146
return timeoutError
@@ -17,65 +9,77 @@ function createTimeoutError(utilName, timeout) {
179
function asyncUtils(addResolver) {
1810
let nextUpdatePromise = null
1911

20-
const resolveOnNextUpdate = ({ timeout }) => (resolve, reject) => {
21-
let timeoutId
22-
if (timeout > 0) {
23-
timeoutId = setTimeout(
24-
() => reject(createTimeoutError('waitForNextUpdate', timeout)),
25-
timeout
26-
)
27-
}
28-
addResolver(() => {
29-
clearTimeout(timeoutId)
30-
nextUpdatePromise = null
31-
resolve()
32-
})
33-
}
34-
3512
const waitForNextUpdate = async (options = {}) => {
3613
if (!nextUpdatePromise) {
37-
nextUpdatePromise = new Promise(resolveOnNextUpdate(options))
14+
const resolveOnNextUpdate = (resolve, reject) => {
15+
let timeoutId
16+
if (options.timeout > 0) {
17+
timeoutId = setTimeout(
18+
() => reject(createTimeoutError('waitForNextUpdate', options)),
19+
options.timeout
20+
)
21+
}
22+
addResolver(() => {
23+
clearTimeout(timeoutId)
24+
nextUpdatePromise = null
25+
resolve()
26+
})
27+
}
28+
29+
nextUpdatePromise = new Promise(resolveOnNextUpdate)
3830
await act(() => nextUpdatePromise)
3931
}
4032
return await nextUpdatePromise
4133
}
4234

43-
const wait = async (callback, options = {}) => {
44-
const initialTimeout = options.timeout
45-
while (true) {
46-
const startTime = Date.now()
35+
const wait = async (callback, { timeout, suppressErrors = true } = {}) => {
36+
const checkResult = () => {
4737
try {
48-
await waitForNextUpdate(options)
49-
const callbackResult = actForResult(callback)
50-
if (callbackResult || callbackResult === undefined) {
51-
break
52-
}
38+
const callbackResult = callback()
39+
return callbackResult || callbackResult === undefined
5340
} catch (e) {
54-
if (e.timeout) {
55-
throw createTimeoutError('wait', initialTimeout)
41+
if (!suppressErrors) {
42+
throw e
5643
}
5744
}
58-
options.timeout -= Date.now() - startTime
45+
}
46+
47+
const waitForResult = async () => {
48+
const initialTimeout = timeout
49+
while (true) {
50+
const startTime = Date.now()
51+
try {
52+
await waitForNextUpdate({ timeout })
53+
if (checkResult()) {
54+
return
55+
}
56+
} catch (e) {
57+
if (e.timeout) {
58+
throw createTimeoutError('wait', { timeout: initialTimeout })
59+
}
60+
throw e
61+
}
62+
timeout -= Date.now() - startTime
63+
}
64+
}
65+
66+
if (!checkResult()) {
67+
await waitForResult()
5968
}
6069
}
6170

6271
const waitForValueToChange = async (selector, options = {}) => {
63-
const initialTimeout = options.timeout
64-
const initialValue = actForResult(selector)
65-
while (true) {
66-
const startTime = Date.now()
67-
try {
68-
await waitForNextUpdate(options)
69-
if (actForResult(selector) !== initialValue) {
70-
break
71-
}
72-
} catch (e) {
73-
if (e.timeout) {
74-
throw createTimeoutError('waitForValueToChange', initialTimeout)
75-
}
76-
throw e
72+
const initialValue = selector()
73+
try {
74+
await wait(() => selector() !== initialValue, {
75+
suppressErrors: false,
76+
...options
77+
})
78+
} catch (e) {
79+
if (e.timeout) {
80+
throw createTimeoutError('waitForValueToChange', options)
7781
}
78-
options.timeout -= Date.now() - startTime
82+
throw e
7983
}
8084
}
8185

test/asyncHook.test.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ describe('async hook tests', () => {
99

1010
useEffect(() => {
1111
const interval = setInterval(() => {
12-
setValue(otherValues[index.current])
13-
index.current++
12+
setValue(otherValues[index.current++])
13+
if (index.current === otherValues.length) {
14+
clearInterval(interval)
15+
}
1416
}, 50)
1517
return () => {
1618
clearInterval(interval)
@@ -77,6 +79,57 @@ describe('async hook tests', () => {
7779
expect(complete).toBe(true)
7880
})
7981

82+
test('should not hang if expectation is already passing', async () => {
83+
const { result, wait } = renderHook(() => useSequence('first', 'second'))
84+
85+
expect(result.current).toBe('first')
86+
87+
let complete = false
88+
await wait(() => {
89+
expect(result.current).toBe('first')
90+
complete = true
91+
})
92+
expect(complete).toBe(true)
93+
})
94+
95+
test('should reject if callback throws error', async () => {
96+
const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
97+
98+
expect(result.current).toBe('first')
99+
100+
await expect(
101+
wait(
102+
() => {
103+
console.log(result.current)
104+
if (result.current === 'second') {
105+
throw new Error('Something Unexpected')
106+
}
107+
return result.current === 'third'
108+
},
109+
{
110+
suppressErrors: false
111+
}
112+
)
113+
).rejects.toThrow(Error('Something Unexpected'))
114+
})
115+
116+
test('should reject if callback immediately throws error', async () => {
117+
const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
118+
119+
expect(result.current).toBe('first')
120+
121+
await expect(
122+
wait(
123+
() => {
124+
throw new Error('Something Unexpected')
125+
},
126+
{
127+
suppressErrors: false
128+
}
129+
)
130+
).rejects.toThrow(Error('Something Unexpected'))
131+
})
132+
80133
test('should wait for truthy value', async () => {
81134
const { result, wait } = renderHook(() => useSequence('first', 'second', 'third'))
82135

@@ -142,4 +195,24 @@ describe('async hook tests', () => {
142195
})
143196
).rejects.toThrow(Error('Something Unexpected'))
144197
})
198+
199+
test('should not reject if selector throws error and suppress errors option is enabled', async () => {
200+
const { result, waitForValueToChange } = renderHook(() =>
201+
useSequence('first', 'second', 'third')
202+
)
203+
204+
expect(result.current).toBe('first')
205+
206+
await waitForValueToChange(
207+
() => {
208+
if (result.current === 'second') {
209+
throw new Error('Something Unexpected')
210+
}
211+
return result.current === 'third'
212+
},
213+
{ suppressErrors: true }
214+
)
215+
216+
expect(result.current).toBe('third')
217+
})
145218
})

0 commit comments

Comments
 (0)