diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js new file mode 100644 index 0000000..8777d43 --- /dev/null +++ b/src/__tests__/renderHook.js @@ -0,0 +1,62 @@ +import { createContext, h } from 'preact' +import { useState, useContext, useEffect } from 'preact/hooks' +import { renderHook } from '../pure' + +test('gives comitted result', () => { + const { result } = renderHook(() => { + const [state, setState] = useState(1) + + 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] = useState('left') + const [right, setRight] = useState('right') + + 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 = createContext('default') + function Wrapper ({ children }) { + return {children} + } + const { result } = renderHook( + () => { + return useContext(Context) + }, + { + wrapper: Wrapper + } + ) + + expect(result.current).toEqual('provided') +}) diff --git a/src/pure.js b/src/pure.js index 8f594aa..e2d8468 100644 --- a/src/pure.js +++ b/src/pure.js @@ -1,5 +1,6 @@ import { getQueriesForElement, prettyDOM, configure as configureDTL } from '@testing-library/dom' -import { h, hydrate as preactHydrate, render as preactRender } from 'preact' +import { h, hydrate as preactHydrate, render as preactRender, createRef } from 'preact' +import { useEffect } from 'preact/hooks' import { act } from 'preact/test-utils' import { fireEvent } from './fire-event' @@ -107,7 +108,35 @@ function cleanup () { mountedContainers.forEach(cleanupAtContainer) } +function renderHook (renderCallback, options) { + const { initialProps, wrapper } = (options || {}) + const result = createRef() + + function TestComponent ({ renderCallbackProps }) { + const pendingResult = renderCallback(renderCallbackProps) + + useEffect(() => { + result.current = pendingResult + }) + + return null + } + + const { rerender: baseRerender, unmount } = render( + , + { wrapper } + ) + + function rerender (rerenderCallbackProps) { + return baseRerender( + + ) + } + + return { result, rerender, unmount } +} + // eslint-disable-next-line import/export export * from '@testing-library/dom' // eslint-disable-next-line import/export -export { render, cleanup, act, fireEvent } +export { render, cleanup, act, fireEvent, renderHook } diff --git a/types/index.d.ts b/types/index.d.ts index 4a04c35..2480d09 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,6 @@ import { queries, Queries, BoundFunction } from '@testing-library/dom' import { act as preactAct } from 'preact/test-utils' -import { ComponentChild } from 'preact' +import { ComponentChild, ComponentType, Element } from 'preact' export * from '@testing-library/dom' @@ -46,3 +46,49 @@ export function cleanup(): void export const act: typeof preactAct extends undefined ? (callback: () => void) => void : typeof preactAct + +export interface RenderHookResult { + /** + * 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 +} + +export interface RenderHookOptions { + /** + * 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?: ComponentType<{ children: Element }> +} + +/** + * Allows you to render a hook within a test React component without having to + * create that component yourself. + */ +export function renderHook( + render: (initialProps: Props) => Result, + options?: RenderHookOptions, +): RenderHookResult