Skip to content

Commit 9535eff

Browse files
eps1lonmpeyperkentcdodds
authored
feat: Add renderHook (#991)
Co-authored-by: Michael Peyper <[email protected]> Co-authored-by: Kent C. Dodds <[email protected]>
1 parent 2c451b3 commit 9535eff

File tree

4 files changed

+161
-2
lines changed

4 files changed

+161
-2
lines changed

src/__tests__/renderHook.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react'
2+
import {renderHook} from '../pure'
3+
4+
test('gives comitted result', () => {
5+
const {result} = renderHook(() => {
6+
const [state, setState] = React.useState(1)
7+
8+
React.useEffect(() => {
9+
setState(2)
10+
}, [])
11+
12+
return [state, setState]
13+
})
14+
15+
expect(result.current).toEqual([2, expect.any(Function)])
16+
})
17+
18+
test('allows rerendering', () => {
19+
const {result, rerender} = renderHook(
20+
({branch}) => {
21+
const [left, setLeft] = React.useState('left')
22+
const [right, setRight] = React.useState('right')
23+
24+
// eslint-disable-next-line jest/no-if
25+
switch (branch) {
26+
case 'left':
27+
return [left, setLeft]
28+
case 'right':
29+
return [right, setRight]
30+
31+
default:
32+
throw new Error(
33+
'No Props passed. This is a bug in the implementation',
34+
)
35+
}
36+
},
37+
{initialProps: {branch: 'left'}},
38+
)
39+
40+
expect(result.current).toEqual(['left', expect.any(Function)])
41+
42+
rerender({branch: 'right'})
43+
44+
expect(result.current).toEqual(['right', expect.any(Function)])
45+
})
46+
47+
test('allows wrapper components', async () => {
48+
const Context = React.createContext('default')
49+
function Wrapper({children}) {
50+
return <Context.Provider value="provided">{children}</Context.Provider>
51+
}
52+
const {result} = renderHook(
53+
() => {
54+
return React.useContext(Context)
55+
},
56+
{
57+
wrapper: Wrapper,
58+
},
59+
)
60+
61+
expect(result.current).toEqual('provided')
62+
})

src/pure.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,36 @@ function cleanup() {
218218
mountedContainers.clear()
219219
}
220220

221+
function renderHook(renderCallback, options = {}) {
222+
const {initialProps, wrapper} = options
223+
const result = React.createRef()
224+
225+
function TestComponent({renderCallbackProps}) {
226+
const pendingResult = renderCallback(renderCallbackProps)
227+
228+
React.useEffect(() => {
229+
result.current = pendingResult
230+
})
231+
232+
return null
233+
}
234+
235+
const {rerender: baseRerender, unmount} = render(
236+
<TestComponent renderCallbackProps={initialProps} />,
237+
{wrapper},
238+
)
239+
240+
function rerender(rerenderCallbackProps) {
241+
return baseRerender(
242+
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
243+
)
244+
}
245+
246+
return {result, rerender, unmount}
247+
}
248+
221249
// just re-export everything from dom-testing-library
222250
export * from '@testing-library/dom'
223-
export {render, cleanup, act, fireEvent}
251+
export {render, renderHook, cleanup, act, fireEvent}
224252

225253
/* eslint func-name-matching:0 */

types/index.d.ts

+46
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,52 @@ export function render(
9898
options?: Omit<RenderOptions, 'queries'>,
9999
): RenderResult
100100

101+
interface RenderHookResult<Result, Props> {
102+
/**
103+
* Triggers a re-render. The props will be passed to your renderHook callback.
104+
*/
105+
rerender: (props?: Props) => void
106+
/**
107+
* This is a stable reference to the latest value returned by your renderHook
108+
* callback
109+
*/
110+
result: {
111+
/**
112+
* The value returned by your renderHook callback
113+
*/
114+
current: Result
115+
}
116+
/**
117+
* Unmounts the test component. This is useful for when you need to test
118+
* any cleanup your useEffects have.
119+
*/
120+
unmount: () => void
121+
}
122+
123+
interface RenderHookOptions<Props> {
124+
/**
125+
* The argument passed to the renderHook callback. Can be useful if you plan
126+
* to use the rerender utility to change the values passed to your hook.
127+
*/
128+
initialProps?: Props
129+
/**
130+
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
131+
* reusable custom render functions for common data providers. See setup for examples.
132+
*
133+
* @see https://testing-library.com/docs/react-testing-library/api/#wrapper
134+
*/
135+
wrapper?: React.JSXElementConstructor<{children: React.ReactElement}>
136+
}
137+
138+
/**
139+
* Allows you to render a hook within a test React component without having to
140+
* create that component yourself.
141+
*/
142+
export function renderHook<Result, Props>(
143+
render: (initialProps: Props) => Result,
144+
options?: RenderHookOptions<Props>,
145+
): RenderHookResult<Result, Props>
146+
101147
/**
102148
* Unmounts React trees that were mounted with render.
103149
*/

types/test.tsx

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import {render, fireEvent, screen, waitFor} from '.'
2+
import {render, fireEvent, screen, waitFor, renderHook} from '.'
33
import * as pure from './pure'
44

55
export async function testRender() {
@@ -161,6 +161,29 @@ export function testBaseElement() {
161161
)
162162
}
163163

164+
export function testRenderHook() {
165+
const {result, rerender, unmount} = renderHook(() => React.useState(2)[0])
166+
167+
expectType<number, typeof result.current>(result.current)
168+
169+
rerender()
170+
171+
unmount()
172+
}
173+
174+
export function testRenderHookProps() {
175+
const {result, rerender, unmount} = renderHook(
176+
({defaultValue}) => React.useState(defaultValue)[0],
177+
{initialProps: {defaultValue: 2}},
178+
)
179+
180+
expectType<number, typeof result.current>(result.current)
181+
182+
rerender()
183+
184+
unmount()
185+
}
186+
164187
/*
165188
eslint
166189
testing-library/prefer-explicit-assert: "off",

0 commit comments

Comments
 (0)