Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a31f0fc

Browse files
authored
Merge pull request #21 from mpeyper/error-updates
Added error handling in hook callback
2 parents fe5a897 + 80a53ef commit a31f0fc

File tree

5 files changed

+160
-15
lines changed

5 files changed

+160
-15
lines changed

‎README.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 40 additions & 14 deletions
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

Lines changed: 108 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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)

AltStyle によって変換されたページ (->オリジナル) /