From c49779a2938697303bc4c5f02a3abf3492aaa789 Mon Sep 17 00:00:00 2001
From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
Date: Wed, 3 May 2023 10:35:02 -0400
Subject: [PATCH] feat: add a button to return to studio (#322)
---
src/editors/EditorPage.jsx | 13 +-
src/editors/EditorPage.test.jsx | 5 +
src/editors/VideoSelectorPage.jsx | 15 +-
src/editors/VideoSelectorPage.test.jsx | 5 +
.../__snapshots__/EditorPage.test.jsx.snap | 54 ++---
.../VideoSelectorPage.test.jsx.snap | 58 +++---
.../ErrorBoundary/ErrorPage.jsx | 81 ++++++--
.../ErrorBoundary/ErrorPage.test.jsx | 76 +++++++
.../__snapshots__/ErrorPage.test.jsx.snap | 196 ++++++++++++++++++
.../sharedComponents/ErrorBoundary/index.jsx | 15 +-
.../ErrorBoundary/index.test.jsx | 9 +-
.../ErrorBoundary/messages.js | 10 +
12 files changed, 453 insertions(+), 84 deletions(-)
create mode 100644 src/editors/sharedComponents/ErrorBoundary/ErrorPage.test.jsx
create mode 100644 src/editors/sharedComponents/ErrorBoundary/__snapshots__/ErrorPage.test.jsx.snap
diff --git a/src/editors/EditorPage.jsx b/src/editors/EditorPage.jsx
index 1bd030ca1..098e404ba 100644
--- a/src/editors/EditorPage.jsx
+++ b/src/editors/EditorPage.jsx
@@ -14,8 +14,13 @@ export const EditorPage = ({
studioEndpointUrl,
onClose,
}) => (
-
-
+
+
-
-
+
+
);
EditorPage.defaultProps = {
blockId: null,
diff --git a/src/editors/EditorPage.test.jsx b/src/editors/EditorPage.test.jsx
index 57a8482d2..8c9348cde 100644
--- a/src/editors/EditorPage.test.jsx
+++ b/src/editors/EditorPage.test.jsx
@@ -12,6 +12,11 @@ const props = {
};
jest.mock('react-redux', () => ({
Provider: 'Provider',
+ connect: (mapStateToProps, mapDispatchToProps) => (component) => ({
+ mapStateToProps,
+ mapDispatchToProps,
+ component,
+ }),
}));
jest.mock('./Editor', () => 'Editor');
diff --git a/src/editors/VideoSelectorPage.jsx b/src/editors/VideoSelectorPage.jsx
index 371cfa5e4..5f754b46c 100644
--- a/src/editors/VideoSelectorPage.jsx
+++ b/src/editors/VideoSelectorPage.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import ErrorBoundary from './sharedComponents/ErrorBoundary';
-import { VideoSelector } from './VideoSelector';
+import VideoSelector from './VideoSelector';
import store from './data/store';
const VideoSelectorPage = ({
@@ -10,8 +10,13 @@ const VideoSelectorPage = ({
lmsEndpointUrl,
studioEndpointUrl,
}) => (
-
-
+
+
-
-
+
+
);
VideoSelectorPage.defaultProps = {
diff --git a/src/editors/VideoSelectorPage.test.jsx b/src/editors/VideoSelectorPage.test.jsx
index 8cab51d13..1e550f75a 100644
--- a/src/editors/VideoSelectorPage.test.jsx
+++ b/src/editors/VideoSelectorPage.test.jsx
@@ -10,6 +10,11 @@ const props = {
jest.mock('react-redux', () => ({
Provider: 'Provider',
+ connect: (mapStateToProps, mapDispatchToProps) => (component) => ({
+ mapStateToProps,
+ mapDispatchToProps,
+ component,
+ }),
}));
jest.mock('./VideoSelector', () => 'VideoSelector');
diff --git a/src/editors/__snapshots__/EditorPage.test.jsx.snap b/src/editors/__snapshots__/EditorPage.test.jsx.snap
index 256b94d4c..49bcd6fd2 100644
--- a/src/editors/__snapshots__/EditorPage.test.jsx.snap
+++ b/src/editors/__snapshots__/EditorPage.test.jsx.snap
@@ -1,17 +1,20 @@
// 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/__snapshots__/VideoSelectorPage.test.jsx.snap b/src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap
index 390794300..ed75497f2 100644
--- a/src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap
+++ b/src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap
@@ -1,45 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Video Selector Page snapshots rendering correctly with expected Input 1`] = `
-
-
+
-
-
-
+
+
`;
exports[`Video Selector Page snapshots rendering with props to null 1`] = `
-
-
+
-
-
-
+
+
`;
diff --git a/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx b/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx
index 49621fef9..85d2d67a8 100644
--- a/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx
+++ b/src/editors/sharedComponents/ErrorBoundary/ErrorPage.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Button, Container, Row, Col,
@@ -6,6 +7,8 @@ import {
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
+import { navigateTo } from '../../hooks';
+import { selectors } from '../../data/redux';
/**
* An error page that displays a generic message for unexpected errors. Also contains a "Try
@@ -13,36 +16,74 @@ import messages from './messages';
*/
export const ErrorPage = ({
message,
+ studioEndpointUrl,
+ learningContextId,
+ // redux
+ unitData,
// injected
intl,
-}) => (
-
-
-
-
- {intl.formatMessage(messages.unexpectedError)}
-
- {message && (
-
- )}
-
- {intl.formatMessage(messages.unexpectedErrorButtonLabel)}
-
-
-
-
-);
+}) => {
+ const outlineType = learningContextId?.startsWith('library-v1') ? 'library' : 'course';
+ const outlineUrl = `${studioEndpointUrl}/${outlineType}/${learningContextId}`;
+ const unitUrl = unitData?.data ? `${studioEndpointUrl}/container/${unitData?.data.ancestors[0].id}` : null;
+
+ return (
+
+
+
+
+ {intl.formatMessage(messages.unexpectedError)}
+
+ {message && (
+
+ )}
+
+ {learningContextId && (unitUrl && outlineType !== 'library' ? (
+ navigateTo(unitUrl)}>
+ {intl.formatMessage(messages.returnToUnitPageLabel)}
+
+ ) : (
+ navigateTo(outlineUrl)}>
+ {intl.formatMessage(messages.returnToOutlineLabel, { outlineType })}
+
+ ))}
+ global.location.reload()}>
+ {intl.formatMessage(messages.unexpectedErrorButtonLabel)}
+
+
+
+
+
+ );
+};
ErrorPage.propTypes = {
message: PropTypes.string,
+ learningContextId: PropTypes.string.isRequired,
+ studioEndpointUrl: PropTypes.string.isRequired,
+ // redux
+ unitData: PropTypes.shape({
+ data: PropTypes.shape({
+ ancestors: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ }),
+ ),
+ }),
+ }),
// injected
intl: intlShape.isRequired,
};
ErrorPage.defaultProps = {
message: null,
+ unitData: null,
};
-export default injectIntl(ErrorPage);
+export const mapStateToProps = (state) => ({
+ unitData: selectors.app.unitUrl(state),
+});
+
+export default injectIntl(connect(mapStateToProps)(ErrorPage));
diff --git a/src/editors/sharedComponents/ErrorBoundary/ErrorPage.test.jsx b/src/editors/sharedComponents/ErrorBoundary/ErrorPage.test.jsx
new file mode 100644
index 000000000..da0979f8a
--- /dev/null
+++ b/src/editors/sharedComponents/ErrorBoundary/ErrorPage.test.jsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { selectors } from '../../data/redux';
+import { formatMessage } from '../../../testUtils';
+import { ErrorPage, mapStateToProps } from './ErrorPage';
+
+jest.mock('../../data/redux', () => ({
+ selectors: {
+ app: {
+ unitUrl: jest.fn(state => ({ unitUrl: state })),
+ },
+ },
+}));
+
+describe('Editor Page', () => {
+ const emptyProps = {
+ learningContextId: null,
+ studioEndpointUrl: null,
+ intl: { formatMessage },
+ };
+ const passedProps = {
+ learningContextId: 'course-v1:edX+DemoX+Demo_Course',
+ studioEndpointUrl: 'fakeurl.com',
+ message: 'cUStomMEssagE',
+ intl: { formatMessage },
+ };
+ const unitData = {
+ data: {
+ ancestors: [{ id: 'SomeID' }],
+ },
+ };
+
+ describe('rendered with empty props', () => {
+ it('should only have one button (try again)', () => {
+ const wrapper = shallow( );
+ const buttonText = wrapper.find('Button').text();
+ expect(wrapper).toMatchSnapshot();
+ expect(buttonText).toEqual('Try again');
+ });
+ });
+
+ describe('rendered with pass through props defined', () => {
+ const wrapper = shallow( );
+ describe('shows two buttons', () => {
+ it('the first button should correspond to returning to the course outline', () => {
+ const firstButtonText = wrapper.find('Button').at(0).text();
+ const secondButtonText = wrapper.find('Button').at(1).text();
+ expect(wrapper).toMatchSnapshot();
+ expect(firstButtonText).toEqual('Return to course outline');
+ expect(secondButtonText).toEqual('Try again');
+ });
+ it('the first button should correspond to returning to the unit page', () => {
+ const returnToUnitPageWrapper = shallow( );
+ expect(returnToUnitPageWrapper).toMatchSnapshot();
+ const firstButtonText = returnToUnitPageWrapper.find('Button').at(0).text();
+ const secondButtonText = returnToUnitPageWrapper.find('Button').at(1).text();
+ expect(returnToUnitPageWrapper).toMatchSnapshot();
+ expect(firstButtonText).toEqual('Return to unit page');
+ expect(secondButtonText).toEqual('Try again');
+ });
+ });
+ it('should have custom message', () => {
+ const customMessageText = wrapper.find('div').children().at(0).text();
+ expect(wrapper).toMatchSnapshot();
+ expect(customMessageText).toEqual('cUStomMEssagE');
+ });
+ });
+ describe('mapStateToProps() function', () => {
+ const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
+ test('unitData should equal unitUrl from app.unitUrl', () => {
+ expect(
+ mapStateToProps(testState).unitData,
+ ).toEqual(selectors.app.unitUrl(testState));
+ });
+ });
+});
diff --git a/src/editors/sharedComponents/ErrorBoundary/__snapshots__/ErrorPage.test.jsx.snap b/src/editors/sharedComponents/ErrorBoundary/__snapshots__/ErrorPage.test.jsx.snap
new file mode 100644
index 000000000..8b93543e6
--- /dev/null
+++ b/src/editors/sharedComponents/ErrorBoundary/__snapshots__/ErrorPage.test.jsx.snap
@@ -0,0 +1,196 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Editor Page rendered with empty props should only have one button (try again) 1`] = `
+
+
+
+
+ An unexpected error occurred. Please click the button below to refresh the page.
+
+
+
+ Try again
+
+
+
+
+
+`;
+
+exports[`Editor Page rendered with pass through props defined should have custom message 1`] = `
+
+
+
+
+ An unexpected error occurred. Please click the button below to refresh the page.
+
+
+
+
+ Return to course outline
+
+
+ Try again
+
+
+
+
+
+`;
+
+exports[`Editor Page rendered with pass through props defined shows two buttons the first button should correspond to returning to the course outline 1`] = `
+
+
+
+
+ An unexpected error occurred. Please click the button below to refresh the page.
+
+
+
+
+ Return to course outline
+
+
+ Try again
+
+
+
+
+
+`;
+
+exports[`Editor Page rendered with pass through props defined shows two buttons the first button should correspond to returning to the unit page 1`] = `
+
+
+
+
+ An unexpected error occurred. Please click the button below to refresh the page.
+
+
+
+
+ Return to unit page
+
+
+ Try again
+
+
+
+
+
+`;
+
+exports[`Editor Page rendered with pass through props defined shows two buttons the first button should correspond to returning to the unit page 2`] = `
+
+
+
+
+ An unexpected error occurred. Please click the button below to refresh the page.
+
+
+
+
+ Return to unit page
+
+
+ Try again
+
+
+
+
+
+`;
diff --git a/src/editors/sharedComponents/ErrorBoundary/index.jsx b/src/editors/sharedComponents/ErrorBoundary/index.jsx
index ecdbe8c08..2298825b8 100644
--- a/src/editors/sharedComponents/ErrorBoundary/index.jsx
+++ b/src/editors/sharedComponents/ErrorBoundary/index.jsx
@@ -16,7 +16,9 @@ import ErrorPage from './ErrorPage';
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
- this.state = { hasError: false };
+ this.state = {
+ hasError: false,
+ };
}
static getDerivedStateFromError() {
@@ -30,7 +32,12 @@ export default class ErrorBoundary extends Component {
render() {
if (this.state.hasError) {
- return ;
+ return (
+
+ );
}
return this.props.children;
@@ -39,8 +46,12 @@ export default class ErrorBoundary extends Component {
ErrorBoundary.propTypes = {
children: PropTypes.node,
+ learningContextId: PropTypes.string,
+ studioEndpointUrl: PropTypes.string,
};
ErrorBoundary.defaultProps = {
children: null,
+ learningContextId: null,
+ studioEndpointUrl: null,
};
diff --git a/src/editors/sharedComponents/ErrorBoundary/index.test.jsx b/src/editors/sharedComponents/ErrorBoundary/index.test.jsx
index 5e4f10a64..a937c8f95 100644
--- a/src/editors/sharedComponents/ErrorBoundary/index.test.jsx
+++ b/src/editors/sharedComponents/ErrorBoundary/index.test.jsx
@@ -11,7 +11,7 @@ jest.mock('@edx/frontend-platform/logging', () => ({
}));
// stubbing this to avoid needing to inject a stubbed intl into an internal component
-jest.mock('./ErrorPage', () => () =>
);
+jest.mock('./ErrorPage', () => () => Error Page
);
describe('ErrorBoundary', () => {
it('should render children if no error', () => {
@@ -21,8 +21,9 @@ describe('ErrorBoundary', () => {
);
const wrapper = mount(component);
-
const element = wrapper.find('div');
+
+ expect(logError).toHaveBeenCalledTimes(0);
expect(element.text()).toEqual('Yay');
});
@@ -35,8 +36,10 @@ describe('ErrorBoundary', () => {
);
- mount(component);
+ const wrapper = mount(component);
+ const element = wrapper.find('p');
expect(logError).toHaveBeenCalledTimes(1);
expect(logError).toHaveBeenCalledWith(new Error('booyah'), { stack: '\n in ExplodingComponent\n in ErrorBoundary (created by WrapperComponent)\n in WrapperComponent' });
+ expect(element.text()).toEqual('Error Page');
});
});
diff --git a/src/editors/sharedComponents/ErrorBoundary/messages.js b/src/editors/sharedComponents/ErrorBoundary/messages.js
index 6ad6a7502..8aa3e4216 100644
--- a/src/editors/sharedComponents/ErrorBoundary/messages.js
+++ b/src/editors/sharedComponents/ErrorBoundary/messages.js
@@ -11,6 +11,16 @@ const messages = defineMessages({
defaultMessage: 'Try again',
description: 'text for button that tries to reload the app by refreshing the page',
},
+ returnToUnitPageLabel: {
+ id: 'unexpected.error.returnToUnitPage.button.text',
+ defaultMessage: 'Return to unit page',
+ description: 'Text for button that navigates back to the unit page',
+ },
+ returnToOutlineLabel: {
+ id: 'unexpected.error.returnToCourseOutline.button.text',
+ defaultMessage: 'Return to {outlineType} outline',
+ description: 'Text for button that navigates back to the course outline',
+ },
});
export default messages;