From 61c99b9b4051f0269114555655b4e28ee71e41cf Mon Sep 17 00:00:00 2001 From: connorhaugh <49422820+connorhaugh@users.noreply.github.com> Date: Tue, 14 Feb 2023 15:21:43 -0500 Subject: [PATCH] feat: add error boundary (#246) * feat: add error boundary --- src/editors/EditorPage.jsx | 27 ++++--- .../__snapshots__/EditorPage.test.jsx.snap | 80 ++++++++++--------- .../ErrorBoundary/ErrorPage.jsx | 62 ++++++++++++++ .../sharedComponents/ErrorBoundary/index.jsx | 46 +++++++++++ .../ErrorBoundary/index.test.jsx | 39 +++++++++ 5 files changed, 204 insertions(+), 50 deletions(-) create mode 100644 src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx create mode 100644 src/editors/sharedComponents/ErrorBoundary/index.jsx create mode 100644 src/editors/sharedComponents/ErrorBoundary/index.test.jsx diff --git a/src/editors/EditorPage.jsx b/src/editors/EditorPage.jsx index 1ea1533db..1bd030ca1 100644 --- a/src/editors/EditorPage.jsx +++ b/src/editors/EditorPage.jsx @@ -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, }) => ( - - - + + + + + ); EditorPage.defaultProps = { blockId: null, diff --git a/src/editors/__snapshots__/EditorPage.test.jsx.snap b/src/editors/__snapshots__/EditorPage.test.jsx.snap index 0d67afe3b..256b94d4c 100644 --- a/src/editors/__snapshots__/EditorPage.test.jsx.snap +++ b/src/editors/__snapshots__/EditorPage.test.jsx.snap @@ -1,47 +1,51 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Editor Page snapshots props besides blockType default to null 1`] = ` - + - - + > + + + `; exports[`Editor Page snapshots rendering correctly with expected Input 1`] = ` - + - - + > + + + `; diff --git a/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx b/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx new file mode 100644 index 000000000..c67e6fa56 --- /dev/null +++ b/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx @@ -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 ( + + + +

+ +

+ {message && ( +
+

{message}

+
+ )} + + +
+
+ ); + } +} + +ErrorPage.propTypes = { + message: PropTypes.string, +}; + +ErrorPage.defaultProps = { + message: null, +}; + +export default ErrorPage; diff --git a/src/editors/sharedComponents/ErrorBoundary/index.jsx b/src/editors/sharedComponents/ErrorBoundary/index.jsx new file mode 100644 index 000000000..ecdbe8c08 --- /dev/null +++ b/src/editors/sharedComponents/ErrorBoundary/index.jsx @@ -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 ; + } + + return this.props.children; + } +} + +ErrorBoundary.propTypes = { + children: PropTypes.node, +}; + +ErrorBoundary.defaultProps = { + children: null, +}; diff --git a/src/editors/sharedComponents/ErrorBoundary/index.test.jsx b/src/editors/sharedComponents/ErrorBoundary/index.test.jsx new file mode 100644 index 000000000..ed4c8c674 --- /dev/null +++ b/src/editors/sharedComponents/ErrorBoundary/index.test.jsx @@ -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 = ( + +
Yay
+
+ ); + 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 = ( + + + + ); + 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' }); + }); +});