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']