Skip to content

Commit 80a53ef

Browse files
committed
added result.error and change result.current into a getter that can throw if value is not valid
1 parent e13623d commit 80a53ef

File tree

5 files changed

+160
-15
lines changed

5 files changed

+160
-15
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Renders a test component that will call the provided `callback`, including any h
138138

139139
- `result` (`object`)
140140
- `current` (`any`) - the return value of the `callback` function
141+
- `error` (`Error`) - the error that was thrown if the `callback` function threw an error during rendering
141142
- `waitForNextUpdate` (`function`) - returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as the result of a asynchronous action.
142143
- `rerender` (`function([newProps])`) - function to rerender the test component including any hooks called in the `callback` function. If `newProps` are passed, the will replace the `initialProps` passed the the `callback` function for future renders.
143144
- `unmount` (`function()`) - function to unmount the test component, commonly used to trigger cleanup effects for `useEffect` hooks.

src/index.js

+40-14
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,64 @@ import React from 'react'
22
import { render, cleanup, act } from 'react-testing-library'
33

44
function TestHook({ callback, hookProps, children }) {
5-
children(callback(hookProps))
5+
try {
6+
children(callback(hookProps))
7+
} catch (e) {
8+
children(undefined, e)
9+
}
610
return null
711
}
812

13+
function resultContainer() {
14+
let value = null
15+
let error = null
16+
const resolvers = []
17+
18+
const result = {
19+
get current() {
20+
if (error) {
21+
throw error
22+
}
23+
return value
24+
},
25+
get error() {
26+
return error
27+
}
28+
}
29+
30+
return {
31+
result,
32+
addResolver: (resolver) => {
33+
resolvers.push(resolver)
34+
},
35+
updateResult: (val, err) => {
36+
value = val
37+
error = err
38+
resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
39+
}
40+
}
41+
}
42+
943
function renderHook(callback, { initialProps, ...options } = {}) {
10-
const result = { current: null }
44+
const { result, updateResult, addResolver } = resultContainer()
1145
const hookProps = { current: initialProps }
12-
const resolvers = []
13-
const waitForNextUpdate = () =>
14-
new Promise((resolve) => {
15-
resolvers.push(resolve)
16-
})
1746

1847
const toRender = () => (
1948
<TestHook callback={callback} hookProps={hookProps.current}>
20-
{(res) => {
21-
result.current = res
22-
resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
23-
}}
49+
{updateResult}
2450
</TestHook>
2551
)
2652

2753
const { unmount, rerender: rerenderComponent } = render(toRender(), options)
2854

2955
return {
3056
result,
31-
waitForNextUpdate,
32-
unmount,
57+
waitForNextUpdate: () => new Promise((resolve) => addResolver(resolve)),
3358
rerender: (newProps = hookProps.current) => {
3459
hookProps.current = newProps
3560
rerenderComponent(toRender())
36-
}
61+
},
62+
unmount
3763
}
3864
}
3965

test/errorHook.test.js

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useState, useEffect } from 'react'
2+
import { renderHook } from 'src'
3+
4+
describe('error hook tests', () => {
5+
function useError(throwError) {
6+
if (throwError) {
7+
throw new Error('expected')
8+
}
9+
return true
10+
}
11+
12+
const somePromise = () => Promise.resolve()
13+
14+
function useAsyncError(throwError) {
15+
const [value, setValue] = useState()
16+
useEffect(() => {
17+
somePromise().then(() => {
18+
setValue(throwError)
19+
})
20+
}, [throwError])
21+
return useError(value)
22+
}
23+
24+
describe('synchronous', () => {
25+
test('should raise error', () => {
26+
const { result } = renderHook(() => useError(true))
27+
28+
expect(() => {
29+
expect(result.current).not.toBe(undefined)
30+
}).toThrow(Error('expected'))
31+
})
32+
33+
test('should capture error', () => {
34+
const { result } = renderHook(() => useError(true))
35+
36+
expect(result.error).toEqual(Error('expected'))
37+
})
38+
39+
test('should not capture error', () => {
40+
const { result } = renderHook(() => useError(false))
41+
42+
expect(result.current).not.toBe(undefined)
43+
expect(result.error).toBe(undefined)
44+
})
45+
46+
test('should reset error', () => {
47+
const { result, rerender } = renderHook((throwError) => useError(throwError), {
48+
initialProps: true
49+
})
50+
51+
expect(result.error).not.toBe(undefined)
52+
53+
rerender(false)
54+
55+
expect(result.current).not.toBe(undefined)
56+
expect(result.error).toBe(undefined)
57+
})
58+
})
59+
60+
describe('asynchronous', () => {
61+
test('should raise async error', async () => {
62+
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true))
63+
64+
await waitForNextUpdate()
65+
66+
expect(() => {
67+
expect(result.current).not.toBe(undefined)
68+
}).toThrow(Error('expected'))
69+
})
70+
71+
test('should capture async error', async () => {
72+
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true))
73+
74+
await waitForNextUpdate()
75+
76+
expect(result.error).toEqual(Error('expected'))
77+
})
78+
79+
test('should not capture async error', async () => {
80+
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(false))
81+
82+
await waitForNextUpdate()
83+
84+
expect(result.current).not.toBe(undefined)
85+
expect(result.error).toBe(undefined)
86+
})
87+
88+
test('should reset async error', async () => {
89+
const { result, waitForNextUpdate, rerender } = renderHook(
90+
(throwError) => useAsyncError(throwError),
91+
{
92+
initialProps: true
93+
}
94+
)
95+
96+
await waitForNextUpdate()
97+
98+
expect(result.error).not.toBe(undefined)
99+
100+
rerender(false)
101+
102+
await waitForNextUpdate()
103+
104+
expect(result.current).not.toBe(undefined)
105+
expect(result.error).toBe(undefined)
106+
})
107+
})
108+
})

test/typescript/renderHook.ts

+9
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ function checkTypesWhenHookReturnsVoid() {
6565
const _rerender: () => void = rerender
6666
}
6767

68+
function checkTypesWithError() {
69+
const { result } = renderHook(() => useCounter())
70+
71+
// check types
72+
const _result: {
73+
error: Error
74+
} = result
75+
}
76+
6877
async function checkTypesForWaitForNextUpdate() {
6978
const { waitForNextUpdate } = renderHook(() => {})
7079

typings/index.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export function renderHook<P, R>(
77
} & RenderOptions
88
): {
99
readonly result: {
10-
current: R
10+
readonly current: R,
11+
readonly error: Error
1112
}
1213
readonly waitForNextUpdate: () => Promise<void>
1314
readonly unmount: RenderResult['unmount']

0 commit comments

Comments
 (0)