Skip to content

Commit 5f35090

Browse files
authored
feat: add render hook (#56)
* feat: add render hook * improvement: add renderHook types * fix: add effect import
1 parent 71a466e commit 5f35090

File tree

3 files changed

+140
-3
lines changed

3 files changed

+140
-3
lines changed

src/__tests__/renderHook.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { createContext, h } from 'preact'
2+
import { useState, useContext, useEffect } from 'preact/hooks'
3+
import { renderHook } from '../pure'
4+
5+
test('gives comitted result', () => {
6+
const { result } = renderHook(() => {
7+
const [state, setState] = useState(1)
8+
9+
useEffect(() => {
10+
setState(2)
11+
}, [])
12+
13+
return [state, setState]
14+
})
15+
16+
expect(result.current).toEqual([2, expect.any(Function)])
17+
})
18+
19+
test('allows rerendering', () => {
20+
const { result, rerender } = renderHook(
21+
({ branch }) => {
22+
const [left, setLeft] = useState('left')
23+
const [right, setRight] = useState('right')
24+
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 = createContext('default')
49+
function Wrapper ({ children }) {
50+
return <Context.Provider value="provided">{children}</Context.Provider>
51+
}
52+
const { result } = renderHook(
53+
() => {
54+
return useContext(Context)
55+
},
56+
{
57+
wrapper: Wrapper
58+
}
59+
)
60+
61+
expect(result.current).toEqual('provided')
62+
})

src/pure.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getQueriesForElement, prettyDOM, configure as configureDTL } from '@testing-library/dom'
2-
import { h, hydrate as preactHydrate, render as preactRender } from 'preact'
2+
import { h, hydrate as preactHydrate, render as preactRender, createRef } from 'preact'
3+
import { useEffect } from 'preact/hooks'
34
import { act } from 'preact/test-utils'
45
import { fireEvent } from './fire-event'
56

@@ -107,7 +108,35 @@ function cleanup () {
107108
mountedContainers.forEach(cleanupAtContainer)
108109
}
109110

111+
function renderHook (renderCallback, options) {
112+
const { initialProps, wrapper } = (options || {})
113+
const result = createRef()
114+
115+
function TestComponent ({ renderCallbackProps }) {
116+
const pendingResult = renderCallback(renderCallbackProps)
117+
118+
useEffect(() => {
119+
result.current = pendingResult
120+
})
121+
122+
return null
123+
}
124+
125+
const { rerender: baseRerender, unmount } = render(
126+
<TestComponent renderCallbackProps={initialProps} />,
127+
{ wrapper }
128+
)
129+
130+
function rerender (rerenderCallbackProps) {
131+
return baseRerender(
132+
<TestComponent renderCallbackProps={rerenderCallbackProps} />
133+
)
134+
}
135+
136+
return { result, rerender, unmount }
137+
}
138+
110139
// eslint-disable-next-line import/export
111140
export * from '@testing-library/dom'
112141
// eslint-disable-next-line import/export
113-
export { render, cleanup, act, fireEvent }
142+
export { render, cleanup, act, fireEvent, renderHook }

types/index.d.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { queries, Queries, BoundFunction } from '@testing-library/dom'
22
import { act as preactAct } from 'preact/test-utils'
3-
import { ComponentChild } from 'preact'
3+
import { ComponentChild, ComponentType, Element } from 'preact'
44

55
export * from '@testing-library/dom'
66

@@ -46,3 +46,49 @@ export function cleanup(): void
4646
export const act: typeof preactAct extends undefined
4747
? (callback: () => void) => void
4848
: typeof preactAct
49+
50+
export interface RenderHookResult<Result, Props> {
51+
/**
52+
* Triggers a re-render. The props will be passed to your renderHook callback.
53+
*/
54+
rerender: (props?: Props) => void
55+
/**
56+
* This is a stable reference to the latest value returned by your renderHook
57+
* callback
58+
*/
59+
result: {
60+
/**
61+
* The value returned by your renderHook callback
62+
*/
63+
current: Result
64+
}
65+
/**
66+
* Unmounts the test component. This is useful for when you need to test
67+
* any cleanup your useEffects have.
68+
*/
69+
unmount: () => void
70+
}
71+
72+
export interface RenderHookOptions<Props> {
73+
/**
74+
* The argument passed to the renderHook callback. Can be useful if you plan
75+
* to use the rerender utility to change the values passed to your hook.
76+
*/
77+
initialProps?: Props
78+
/**
79+
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
80+
* reusable custom render functions for common data providers. See setup for examples.
81+
*
82+
* @see https://testing-library.com/docs/react-testing-library/api/#wrapper
83+
*/
84+
wrapper?: ComponentType<{ children: Element }>
85+
}
86+
87+
/**
88+
* Allows you to render a hook within a test React component without having to
89+
* create that component yourself.
90+
*/
91+
export function renderHook<Result, Props>(
92+
render: (initialProps: Props) => Result,
93+
options?: RenderHookOptions<Props>,
94+
): RenderHookResult<Result, Props>

0 commit comments

Comments
 (0)