Subject Under Test(sut):
An error fallback component to be passed to ErrorBoundary
component from react-error-boundary
.
Behaviours:
In development mode, it shows error track trace
In production mode, it hides error track trace
It shows a
Reset
button, and clicking it calls resetErrorBoundary action passed by parentErrorBoundary
component.
Code
import { render } from '@testing-library/react'; | |
import { ErrorBoundary } from 'react-error-boundary'; | |
import { ErrorFallBack } from './ErrorFallBack'; | |
import userEvent from '@testing-library/user-event'; | |
import { environment as mockEnvironment } from '@epic/environments'; | |
describe('Error Fallback', function () { | |
jest.mock('@epic/environments'); | |
function Bomb({ explode }: { explode: boolean }) { | |
if (explode) { | |
throw new Error('Boom!'); | |
} | |
return null; | |
} | |
beforeAll(() => { | |
jest.spyOn(console, 'error').mockImplementation(() => {}); | |
}); | |
afterAll(() => { | |
jest.restoreAllMocks(); | |
}); | |
afterEach(() => { | |
jest.resetAllMocks(); | |
}); | |
function renderInErrorBoundary({ explode = false }: { explode?: boolean }) { | |
return render( | |
<ErrorBoundary FallbackComponent={ErrorFallBack}> | |
<Bomb explode={explode} /> | |
</ErrorBoundary> | |
); | |
} | |
function renderStandalone() { | |
const reset = jest.fn(); | |
const queries = render(<ErrorFallBack error={new Error('error')} resetErrorBoundary={reset} />); | |
return { | |
reset, | |
...queries, | |
}; | |
} | |
it('should show errors in dev', function () { | |
const { getByText, getByTestId } = renderInErrorBoundary({ explode: true }); | |
expect(getByText('Boom!')).toBeInTheDocument(); | |
expect(getByText(/reset/i)).toBeInTheDocument(); | |
expect(getByTestId('error-stack-trace')).toBeInTheDocument(); | |
}); | |
it('should not show error stack trace in production', function () { | |
mockEnvironment.production = true; | |
const { queryByTestId } = renderInErrorBoundary({ explode: true }); | |
expect(queryByTestId('error-stack-trace')).toBeNull(); | |
}); | |
it('should show "Reset" button and user click should trigger reset action', function () { | |
const { getByText, reset } = renderStandalone(); | |
const button = getByText(/reset/i); | |
userEvent.click(button); | |
expect(reset).toHaveBeenCalledTimes(1); | |
}); | |
}); |
import React, { PropsWithChildren } from 'react'; | |
import { | |
Button, | |
Card, | |
CardActions, | |
CardContent, | |
CardHeader, | |
Container, | |
Divider, | |
Typography, | |
} from '@mui/material'; | |
import { FallbackProps } from 'react-error-boundary'; | |
import { environment } from '@epic/environments'; | |
import { formatException } from './stackTraceFormatter'; | |
export function ErrorFallBack({ | |
children, | |
error, | |
resetErrorBoundary, | |
}: PropsWithChildren<FallbackProps>) { | |
const stack = formatException(error.stack || ''); | |
const errorMessage = error.message || 'Unknown error'; | |
return ( | |
<Container maxWidth={'md'}> | |
<Card | |
style={{ | |
margin: '24px 0', | |
padding: '24px', | |
borderLeft: '5px solid #ffe564', | |
backgroundColor: 'rgba(255,229,100,0.2)', | |
}} | |
> | |
<CardHeader | |
title={ | |
'Oops, there is an error and its our fault. We are very sorry, please contact the team.' | |
} | |
/> | |
<Divider /> | |
<CardContent style={{ color: 'red' }}> | |
<Typography variant={'h6'} style={{ paddingBottom: '20px' }}> | |
{errorMessage} | |
</Typography> | |
{environment.production ? null : ( | |
<Typography | |
display={'block'} | |
data-testid={'error-stack-trace'} | |
style={{ | |
whiteSpace: 'pre-line', | |
}} | |
> | |
{stack} | |
</Typography> | |
)} | |
{children} | |
</CardContent> | |
<CardActions> | |
<Button variant={'contained'} color={'primary'} onClick={resetErrorBoundary}> | |
Reset | |
</Button> | |
</CardActions> | |
</Card> | |
</Container> | |
); | |
} |
Notes
- The
renderInErrorBoundary
helper method renders a bomb component inside an actualErrorBoundary
, no mocks. It shows how the sut should be used. -
getByText
itself implicitly asserts if the element is in the document because if not found, it will throw an error. However, I asserted anyway for clarity -
queryByTestId
is used to assert if the error stack trace is rendered in the DOM under production, it returns null if not found, contrary togetByTestId
, which would throw an error when the element is not found. -
@epic/environments
is mocked out to override the value to test production mode behaviour. - The
renderStandalone
helper method renders the component as any other React component, for the sole purpose of testing the behaviour of theReset
button. Because theresetErrorBoundary
is implicitly passed to the sut, and I don’t know a way to mock a React component prop yet. The test shouldn’t care about it anyway.
Top comments (0)