Skip to content

Commit

Permalink
feat: Add renderHook (#991)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Peyper <mpeyper7@gmail.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
  • Loading branch information
3 people committed Apr 15, 2022
1 parent 2c451b3 commit 9535eff
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
62 changes: 62 additions & 0 deletions src/__tests__/renderHook.js
@@ -0,0 +1,62 @@
import React from 'react'
import {renderHook} from '../pure'

test('gives comitted result', () => {
const {result} = renderHook(() => {
const [state, setState] = React.useState(1)

React.useEffect(() => {
setState(2)
}, [])

return [state, setState]
})

expect(result.current).toEqual([2, expect.any(Function)])
})

test('allows rerendering', () => {
const {result, rerender} = renderHook(
({branch}) => {
const [left, setLeft] = React.useState('left')
const [right, setRight] = React.useState('right')

// eslint-disable-next-line jest/no-if
switch (branch) {
case 'left':
return [left, setLeft]
case 'right':
return [right, setRight]

default:
throw new Error(
'No Props passed. This is a bug in the implementation',
)
}
},
{initialProps: {branch: 'left'}},
)

expect(result.current).toEqual(['left', expect.any(Function)])

rerender({branch: 'right'})

expect(result.current).toEqual(['right', expect.any(Function)])
})

test('allows wrapper components', async () => {
const Context = React.createContext('default')
function Wrapper({children}) {
return <Context.Provider value="provided">{children}</Context.Provider>
}
const {result} = renderHook(
() => {
return React.useContext(Context)
},
{
wrapper: Wrapper,
},
)

expect(result.current).toEqual('provided')
})
30 changes: 29 additions & 1 deletion src/pure.js
Expand Up @@ -218,8 +218,36 @@ function cleanup() {
mountedContainers.clear()
}

function renderHook(renderCallback, options = {}) {
const {initialProps, wrapper} = options
const result = React.createRef()

function TestComponent({renderCallbackProps}) {
const pendingResult = renderCallback(renderCallbackProps)

React.useEffect(() => {
result.current = pendingResult
})

return null
}

const {rerender: baseRerender, unmount} = render(
<TestComponent renderCallbackProps={initialProps} />,
{wrapper},
)

function rerender(rerenderCallbackProps) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
)
}

return {result, rerender, unmount}
}

// just re-export everything from dom-testing-library
export * from '@testing-library/dom'
export {render, cleanup, act, fireEvent}
export {render, renderHook, cleanup, act, fireEvent}

/* eslint func-name-matching:0 */
46 changes: 46 additions & 0 deletions types/index.d.ts
Expand Up @@ -98,6 +98,52 @@ export function render(
options?: Omit<RenderOptions, 'queries'>,
): RenderResult

interface RenderHookResult<Result, Props> {
/**
* Triggers a re-render. The props will be passed to your renderHook callback.
*/
rerender: (props?: Props) => void
/**
* This is a stable reference to the latest value returned by your renderHook
* callback
*/
result: {
/**
* The value returned by your renderHook callback
*/
current: Result
}
/**
* Unmounts the test component. This is useful for when you need to test
* any cleanup your useEffects have.
*/
unmount: () => void
}

interface RenderHookOptions<Props> {
/**
* The argument passed to the renderHook callback. Can be useful if you plan
* to use the rerender utility to change the values passed to your hook.
*/
initialProps?: Props
/**
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
* reusable custom render functions for common data providers. See setup for examples.
*
* @see https://testing-library.com/docs/react-testing-library/api/#wrapper
*/
wrapper?: React.JSXElementConstructor<{children: React.ReactElement}>
}

/**
* Allows you to render a hook within a test React component without having to
* create that component yourself.
*/
export function renderHook<Result, Props>(
render: (initialProps: Props) => Result,
options?: RenderHookOptions<Props>,
): RenderHookResult<Result, Props>

/**
* Unmounts React trees that were mounted with render.
*/
Expand Down
25 changes: 24 additions & 1 deletion types/test.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
import {render, fireEvent, screen, waitFor} from '.'
import {render, fireEvent, screen, waitFor, renderHook} from '.'
import * as pure from './pure'

export async function testRender() {
Expand Down Expand Up @@ -161,6 +161,29 @@ export function testBaseElement() {
)
}

export function testRenderHook() {
const {result, rerender, unmount} = renderHook(() => React.useState(2)[0])

expectType<number, typeof result.current>(result.current)

rerender()

unmount()
}

export function testRenderHookProps() {
const {result, rerender, unmount} = renderHook(
({defaultValue}) => React.useState(defaultValue)[0],
{initialProps: {defaultValue: 2}},
)

expectType<number, typeof result.current>(result.current)

rerender()

unmount()
}

/*
eslint
testing-library/prefer-explicit-assert: "off",
Expand Down

0 comments on commit 9535eff

Please sign in to comment.