feat: add error boundary (#246)

* feat: add error boundary
This commit is contained in:
connorhaugh
2023-02-14 15:21:43 -05:00
committed by GitHub
parent 529ec8ddf2
commit 61c99b9b40
5 changed files with 204 additions and 50 deletions

View File

@@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import store from './data/store';
import Editor from './Editor';
import ErrorBoundary from './sharedComponents/ErrorBoundary';
export const EditorPage = ({
courseId,
@@ -13,18 +14,20 @@ export const EditorPage = ({
studioEndpointUrl,
onClose,
}) => (
<Provider store={store}>
<Editor
{...{
onClose,
learningContextId: courseId,
blockType,
blockId,
lmsEndpointUrl,
studioEndpointUrl,
}}
/>
</Provider>
<ErrorBoundary>
<Provider store={store}>
<Editor
{...{
onClose,
learningContextId: courseId,
blockType,
blockId,
lmsEndpointUrl,
studioEndpointUrl,
}}
/>
</Provider>
</ErrorBoundary>
);
EditorPage.defaultProps = {
blockId: null,

View File

@@ -1,47 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Editor Page snapshots props besides blockType default to null 1`] = `
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
<ErrorBoundary>
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
}
>
<Editor
blockId={null}
blockType="html"
learningContextId={null}
lmsEndpointUrl={null}
onClose={null}
studioEndpointUrl={null}
/>
</Provider>
>
<Editor
blockId={null}
blockType="html"
learningContextId={null}
lmsEndpointUrl={null}
onClose={null}
studioEndpointUrl={null}
/>
</Provider>
</ErrorBoundary>
`;
exports[`Editor Page snapshots rendering correctly with expected Input 1`] = `
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
<ErrorBoundary>
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
}
>
<Editor
blockId="block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
blockType="html"
learningContextId="course-v1:edX+DemoX+Demo_Course"
lmsEndpointUrl="evenfakerurl.com"
onClose={[MockFunction props.onClose]}
studioEndpointUrl="fakeurl.com"
/>
</Provider>
>
<Editor
blockId="block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
blockType="html"
learningContextId="course-v1:edX+DemoX+Demo_Course"
lmsEndpointUrl="evenfakerurl.com"
onClose={[MockFunction props.onClose]}
studioEndpointUrl="fakeurl.com"
/>
</Provider>
</ErrorBoundary>
`;

View File

@@ -0,0 +1,62 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Button, Container, Row, Col,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
/**
* An error page that displays a generic message for unexpected errors. Also contains a "Try
* Again" button to refresh the page.
*
* @memberof module:React
* @extends {Component}
*/
class ErrorPage extends Component {
/* istanbul ignore next */
reload() {
global.location.reload();
}
render() {
const { message } = this.props;
return (
<Container fluid className="py-5 justify-content-center align-items-start text-center">
<Row>
<Col>
<p className="text-muted">
<FormattedMessage
id="unexpected.error.message.text"
defaultMessage="An unexpected error occurred. Please click the button below to refresh the page."
description="error message when an unexpected error occurs"
/>
</p>
{message && (
<div role="alert" className="my-4">
<p>{message}</p>
</div>
)}
<Button onClick={this.reload}>
<FormattedMessage
id="unexpected.error.button.text"
defaultMessage="Try again"
description="text for button that tries to reload the app by refreshing the page"
/>
</Button>
</Col>
</Row>
</Container>
);
}
}
ErrorPage.propTypes = {
message: PropTypes.string,
};
ErrorPage.defaultProps = {
message: null,
};
export default ErrorPage;

View File

@@ -0,0 +1,46 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
logError,
} from '@edx/frontend-platform/logging';
import ErrorPage from './ErrorPage';
/**
* Error boundary component used to log caught errors and display the error page.
*
* @memberof module:React
* @extends {Component}
*/
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
logError(error, { stack: info.componentStack });
}
render() {
if (this.state.hasError) {
return <ErrorPage />;
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node,
};
ErrorBoundary.defaultProps = {
children: null,
};

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { mount } from 'enzyme';
import {
logError,
} from '@edx/frontend-platform/logging';
import ErrorBoundary from './index';
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));
describe('ErrorBoundary', () => {
it('should render children if no error', () => {
const component = (
<ErrorBoundary>
<div>Yay</div>
</ErrorBoundary>
);
const wrapper = mount(component);
const element = wrapper.find('div');
expect(element.text()).toEqual('Yay');
});
it('should render ErrorPage if it has an error', () => {
const ExplodingComponent = () => {
throw new Error('booyah');
};
const component = (
<ErrorBoundary>
<ExplodingComponent />
</ErrorBoundary>
);
mount(component);
expect(logError).toHaveBeenCalledTimes(1);
expect(logError).toHaveBeenCalledWith(new Error('booyah'), { stack: '\n in ExplodingComponent\n in ErrorBoundary (created by WrapperComponent)\n in WrapperComponent' });
});
});