diff --git a/README.md b/README.md index d15485f9..811297da 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Renders a test component that will call the provided `callback`, including any h - `result` (`object`) - `current` (`any`) - the return value of the `callback` function + - `error` (`Error`) - the error that was thrown if the `callback` function threw an error during rendering - `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. - `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. - `unmount` (`function()`) - function to unmount the test component, commonly used to trigger cleanup effects for `useEffect` hooks. diff --git a/src/index.js b/src/index.js index 59aff861..f1c396de 100644 --- a/src/index.js +++ b/src/index.js @@ -2,25 +2,51 @@ import React from 'react' import { render, cleanup, act } from 'react-testing-library' function TestHook({ callback, hookProps, children }) { - children(callback(hookProps)) + try { + children(callback(hookProps)) + } catch (e) { + children(undefined, e) + } return null } +function resultContainer() { + let value = null + let error = null + const resolvers = [] + + const result = { + get current() { + if (error) { + throw error + } + return value + }, + get error() { + return error + } + } + + return { + result, + addResolver: (resolver) => { + resolvers.push(resolver) + }, + updateResult: (val, err) => { + value = val + error = err + resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) + } + } +} + function renderHook(callback, { initialProps, ...options } = {}) { - const result = { current: null } + const { result, updateResult, addResolver } = resultContainer() const hookProps = { current: initialProps } - const resolvers = [] - const waitForNextUpdate = () => - new Promise((resolve) => { - resolvers.push(resolve) - }) const toRender = () => ( - {(res) => { - result.current = res - resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) - }} + {updateResult} ) @@ -28,12 +54,12 @@ function renderHook(callback, { initialProps, ...options } = {}) { return { result, - waitForNextUpdate, - unmount, + waitForNextUpdate: () => new Promise((resolve) => addResolver(resolve)), rerender: (newProps = hookProps.current) => { hookProps.current = newProps rerenderComponent(toRender()) - } + }, + unmount } } diff --git a/test/errorHook.test.js b/test/errorHook.test.js new file mode 100644 index 00000000..e70c27b6 --- /dev/null +++ b/test/errorHook.test.js @@ -0,0 +1,108 @@ +import { useState, useEffect } from 'react' +import { renderHook } from 'src' + +describe('error hook tests', () => { + function useError(throwError) { + if (throwError) { + throw new Error('expected') + } + return true + } + + const somePromise = () => Promise.resolve() + + function useAsyncError(throwError) { + const [value, setValue] = useState() + useEffect(() => { + somePromise().then(() => { + setValue(throwError) + }) + }, [throwError]) + return useError(value) + } + + describe('synchronous', () => { + test('should raise error', () => { + const { result } = renderHook(() => useError(true)) + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture error', () => { + const { result } = renderHook(() => useError(true)) + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture error', () => { + const { result } = renderHook(() => useError(false)) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset error', () => { + const { result, rerender } = renderHook((throwError) => useError(throwError), { + initialProps: true + }) + + expect(result.error).not.toBe(undefined) + + rerender(false) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + describe('asynchronous', () => { + test('should raise async error', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true)) + + await waitForNextUpdate() + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture async error', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true)) + + await waitForNextUpdate() + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture async error', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncError(false)) + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset async error', async () => { + const { result, waitForNextUpdate, rerender } = renderHook( + (throwError) => useAsyncError(throwError), + { + initialProps: true + } + ) + + await waitForNextUpdate() + + expect(result.error).not.toBe(undefined) + + rerender(false) + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) +}) diff --git a/test/typescript/renderHook.ts b/test/typescript/renderHook.ts index b5e31e5b..f5dd92f5 100644 --- a/test/typescript/renderHook.ts +++ b/test/typescript/renderHook.ts @@ -65,6 +65,15 @@ function checkTypesWhenHookReturnsVoid() { const _rerender: () => void = rerender } +function checkTypesWithError() { + const { result } = renderHook(() => useCounter()) + + // check types + const _result: { + error: Error + } = result +} + async function checkTypesForWaitForNextUpdate() { const { waitForNextUpdate } = renderHook(() => {}) diff --git a/typings/index.d.ts b/typings/index.d.ts index 2eec5415..204d2579 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -7,7 +7,8 @@ export function renderHook( } & RenderOptions ): { readonly result: { - current: R + readonly current: R, + readonly error: Error } readonly waitForNextUpdate: () => Promise readonly unmount: RenderResult['unmount']