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 && ( -
-

{message}

-
- )} - - -
-
-); +}) => { + 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 && ( +
+

{message}

+
+ )} + + {learningContextId && (unitUrl && outlineType !== 'library' ? ( + + ) : ( + + ))} + + + +
+
+ ); +}; 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. +

+ + + + +
+
+`; + +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. +

+
+

+ cUStomMEssagE +

+
+ + + + + +
+
+`; + +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. +

+
+

+ cUStomMEssagE +

+
+ + + + + +
+
+`; + +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. +

+
+

+ cUStomMEssagE +

+
+ + + + + +
+
+`; + +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. +

+
+

+ cUStomMEssagE +

+
+ + + + + +
+
+`; 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;