@@ -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,
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
62
src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx
Normal file
62
src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx
Normal 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;
|
||||
46
src/editors/sharedComponents/ErrorBoundary/index.jsx
Normal file
46
src/editors/sharedComponents/ErrorBoundary/index.jsx
Normal 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,
|
||||
};
|
||||
39
src/editors/sharedComponents/ErrorBoundary/index.test.jsx
Normal file
39
src/editors/sharedComponents/ErrorBoundary/index.test.jsx
Normal 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' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user