diff --git a/src/index.js b/src/index.js index f1c396de..aa6417cc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,37 @@ -import React from 'react' +import React, { Suspense } from 'react' import { render, cleanup, act } from 'react-testing-library' function TestHook({ callback, hookProps, children }) { - try { - children(callback(hookProps)) - } catch (e) { - children(undefined, e) + children(callback(hookProps)) + return null +} + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError() { + return { hasError: true } } + + componentDidCatch(error) { + this.props.onError(error) + } + + componentDidUpdate(prevProps) { + if (this.props != prevProps && this.state.hasError) { + this.setState({ hasError: false }) + } + } + + render() { + return !this.state.hasError && this.props.children + } +} + +function Fallback() { return null } @@ -27,27 +52,34 @@ function resultContainer() { } } + const updateResult = (val, err) => { + value = val + error = err + resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) + } + return { result, addResolver: (resolver) => { resolvers.push(resolver) }, - updateResult: (val, err) => { - value = val - error = err - resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) - } + setValue: (val) => updateResult(val), + setError: (err) => updateResult(undefined, err) } } function renderHook(callback, { initialProps, ...options } = {}) { - const { result, updateResult, addResolver } = resultContainer() + const { result, setValue, setError, addResolver } = resultContainer() const hookProps = { current: initialProps } const toRender = () => ( - - {updateResult} - + + }> + + {setValue} + + + ) const { unmount, rerender: rerenderComponent } = render(toRender(), options) diff --git a/test/suspenseHook.test.js b/test/suspenseHook.test.js new file mode 100644 index 00000000..f613664f --- /dev/null +++ b/test/suspenseHook.test.js @@ -0,0 +1,51 @@ +import { renderHook, cleanup } from 'src' + +describe('suspense hook tests', () => { + const cache = {} + const fetchName = (isSuccessful) => { + if (!cache.value) { + cache.value = new Promise((resolve, reject) => { + setTimeout(() => { + if (isSuccessful) { + resolve('Bob') + } else { + reject(new Error('Failed to fetch name')) + } + }, 50) + }) + .then((value) => (cache.value = value)) + .catch((e) => (cache.value = e)) + } + return cache.value + } + + const useFetchName = (isSuccessful = true) => { + const name = fetchName(isSuccessful) + if (typeof name.then === 'function' || name instanceof Error) { + throw name + } + return name + } + + beforeEach(() => { + delete cache.value + }) + + afterEach(cleanup) + + test('should allow rendering to be suspended', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFetchName(true)) + + await waitForNextUpdate() + + expect(result.current).toBe('Bob') + }) + + test('should set error if suspense promise rejects', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFetchName(false)) + + await waitForNextUpdate() + + expect(result.error).toEqual(new Error('Failed to fetch name')) + }) +})