Skip to content

Commit

Permalink
feat: [AXIMST-52] display unit and xblock render errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor-romaniuk committed Feb 27, 2024
1 parent 68d7533 commit 29e7e13
Show file tree
Hide file tree
Showing 24 changed files with 320 additions and 58 deletions.
87 changes: 48 additions & 39 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DraggableList, ErrorAlert } from '@edx/frontend-lib-content-components'
import { Warning as WarningIcon } from '@edx/paragon/icons';

import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import RenderErrorAlert from '../generic/render-error-alert';
import SubHeader from '../generic/sub-header/SubHeader';
import { RequestStatus } from '../data/constants';
import getPageHeadTitle from '../generic/utils';
Expand Down Expand Up @@ -55,6 +56,7 @@ const CourseUnit = ({ courseId }) => {
courseVerticalChildren,
handleXBlockDragAndDrop,
canPasteComponent,
unitRenderError,
} = useCourseUnit({ courseId, blockId });

const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
Expand Down Expand Up @@ -118,13 +120,15 @@ const CourseUnit = ({ courseId }) => {
/>
)}
/>
<Sequence
courseId={courseId}
sequenceId={sequenceId}
unitId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
showPasteUnit={showPasteUnit}
/>
{unitRenderError ? <RenderErrorAlert errorMessage={unitRenderError} /> : (
<Sequence
courseId={courseId}
sequenceId={sequenceId}
unitId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
showPasteUnit={showPasteUnit}
/>
)}
<Layout
lg={[{ span: 8 }, { span: 4 }]}
md={[{ span: 8 }, { span: 4 }]}
Expand All @@ -144,39 +148,44 @@ const CourseUnit = ({ courseId }) => {
staticFileNotices={staticFileNotices}
courseId={courseId}
/>
<Stack gap={4} className="mb-4 course-unit__xblocks">
<DraggableList
itemList={unitXBlocks}
setState={setUnitXBlocks}
updateOrder={finalizeXBlockOrder}
>
{unitXBlocks.map(({
name, id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
}) => (
<CourseXBlock
id={id}
key={id}
title={name}
type={type}
validationMessages={validationMessages}
shouldScroll={shouldScroll}
unitXBlockActions={unitXBlockActions}
handleConfigureSubmit={handleConfigureSubmit}
data-testid="course-xblock"
userPartitionInfo={userPartitionInfo}
{unitRenderError ? <RenderErrorAlert errorMessage={unitRenderError} /> : (
<>
<Stack gap={4} className="mb-4 course-unit__xblocks">
<DraggableList
itemList={unitXBlocks}
setState={setUnitXBlocks}
updateOrder={finalizeXBlockOrder}
>
{unitXBlocks.map(({
name, id, blockType: type, renderError, shouldScroll, userPartitionInfo, validationMessages,
}) => (
<CourseXBlock
id={id}
key={id}
title={name}
type={type}
renderError={renderError}
validationMessages={validationMessages}
shouldScroll={shouldScroll}
unitXBlockActions={unitXBlockActions}
handleConfigureSubmit={handleConfigureSubmit}
data-testid="course-xblock"
userPartitionInfo={userPartitionInfo}
/>
))}
</DraggableList>
</Stack>
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
{showPasteXBlock && canPasteComponent && (
<PasteComponent
clipboardData={sharedClipboardData}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
))}
</DraggableList>
</Stack>
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
{showPasteXBlock && canPasteComponent && (
<PasteComponent
clipboardData={sharedClipboardData}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
)}
</>
)}
</Layout.Element>
<Layout.Element>
Expand Down
50 changes: 50 additions & 0 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import headerTitleMessages from './header-title/messages';
import courseSequenceMessages from './course-sequence/messages';
import addComponentMessages from './add-component/messages';
import sidebarMessages from './sidebar/messages';
import renderErrorAlertMessages from '../generic/render-error-alert/messages';
import { extractCourseUnitId } from './sidebar/utils';
import courseXBlockMessages from './course-xblock/messages';
import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
Expand Down Expand Up @@ -1379,4 +1380,53 @@ describe('<CourseUnit />', () => {
expect(xBlock1).toBe(xBlock2);
});
});

it('displays a render error message if unit has error', async () => {
const unitRenderErrorText = 'Cannot resolve bundle XXXXXXX';
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
render_error: unitRenderErrorText,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
const {
getByText,
getByRole,
queryByRole,
queryAllByText,
queryAllByTestId,
} = render(<RootWrapper />);

await waitFor(() => {
// check displaying of two error alerts
const errorAlertTitle = renderErrorAlertMessages.alertRenderErrorTitle.defaultMessage;
const errorAlertDescription = renderErrorAlertMessages.alertRenderErrorDescription.defaultMessage;
const errorAlertMessage = renderErrorAlertMessages.alertRenderErrorMessage.defaultMessage
.replace('{message}', unitRenderErrorText);

expect(queryAllByText(errorAlertTitle)).toHaveLength(2);
expect(queryAllByText(errorAlertDescription)).toHaveLength(2);
expect(queryAllByText(errorAlertMessage)).toHaveLength(2);

// check availability of the unit title, breadcrumbs, controls and sidebar
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;

expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();

// check if is the sequence block and items not being displayed
const contentItem = queryAllByTestId('course-xblock');
const addNewUnitBtn = queryByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });

expect(contentItem).toHaveLength(0);
expect(addNewUnitBtn).not.toBeInTheDocument();
});
});
});
13 changes: 10 additions & 3 deletions src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useNavigate } from 'react-router-dom';

import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import RenderErrorAlert from '../../generic/render-error-alert';
import ConditionalSortableElement from '../../generic/drag-helper/ConditionalSortableElement';
import { scrollToElement } from '../../course-outline/utils';
import { COURSE_BLOCK_NAMES } from '../../constants';
Expand All @@ -23,7 +24,7 @@ import messages from './messages';

const CourseXBlock = ({
id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo,
handleConfigureSubmit, validationMessages, ...props
handleConfigureSubmit, validationMessages, renderError, ...props
}) => {
const courseXBlockElementRef = useRef(null);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
Expand Down Expand Up @@ -131,8 +132,12 @@ const CourseXBlock = ({
)}
/>
<Card.Section>
<XBlockMessages validationMessages={validationMessages} />
<XBlockContent id={id} title={title} elementId={id} iframeUrl={iframeUrl} />
{renderError ? <RenderErrorAlert errorMessage={renderError} /> : (
<>
<XBlockMessages validationMessages={validationMessages} />
<XBlockContent id={id} title={title} elementId={id} iframeUrl={iframeUrl} />
</>
)}
</Card.Section>
</Card>
</div>
Expand All @@ -142,12 +147,14 @@ const CourseXBlock = ({
CourseXBlock.defaultProps = {
validationMessages: [],
shouldScroll: false,
renderError: '',
};

CourseXBlock.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
renderError: PropTypes.string,
shouldScroll: PropTypes.bool,
validationMessages: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.string,
Expand Down
31 changes: 31 additions & 0 deletions src/course-unit/course-xblock/CourseXBlock.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import configureModalMessages from '../../generic/configure-modal/messages';
import deleteModalMessages from '../../generic/delete-modal/messages';
import renderErrorAlertMessages from '../../generic/render-error-alert/messages';
import initializeStore from '../../store';
import { getCourseSectionVerticalApiUrl, getXBlockBaseApiUrl } from '../data/api';
import { fetchCourseSectionVerticalData } from '../data/thunk';
Expand Down Expand Up @@ -309,4 +310,34 @@ describe('<CourseXBlock />', () => {
expect(getByText(visibilityMessage)).toBeInTheDocument();
});
});

it('displays a render error message if item has error', async () => {
const renderErrorMessage = 'Some error message';
const { getByText, getByLabelText, queryByTestId } = renderComponent(
{
renderError: renderErrorMessage,
},
);

await waitFor(() => {
const errorAlertTitle = renderErrorAlertMessages.alertRenderErrorTitle.defaultMessage;
const errorAlertDescription = renderErrorAlertMessages.alertRenderErrorDescription.defaultMessage;
const errorAlertMessage = renderErrorAlertMessages.alertRenderErrorMessage.defaultMessage
.replace('{message}', renderErrorMessage);
const contentIframe = queryByTestId('content-iframe-test-id');

// check displaying of the error alert
expect(getByText(errorAlertTitle)).toBeInTheDocument();
expect(getByText(errorAlertDescription)).toBeInTheDocument();
expect(getByText(errorAlertMessage)).toBeInTheDocument();

// check availability of the item title and controls
expect(getByText(name)).toBeInTheDocument();
expect(getByLabelText(messages.blockAltButtonEdit.defaultMessage)).toBeInTheDocument();
expect(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage)).toBeInTheDocument();

// check if is the content of the element not being displayed
expect(contentIframe).not.toBeInTheDocument();
});
});
});
2 changes: 2 additions & 0 deletions src/course-unit/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {

const unitTitle = courseUnit.metadata?.displayName || '';
const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id;
const unitRenderError = courseUnit.renderError || '';

const headerNavigationsActions = {
handleViewLive: () => {
Expand Down Expand Up @@ -149,5 +150,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
courseVerticalChildren,
handleXBlockDragAndDrop,
canPasteComponent,
unitRenderError,
};
};
2 changes: 1 addition & 1 deletion src/generic/alert-message/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
const AlertMessage = ({ title, description, ...props }) => (
<Alert {...props}>
<Alert.Heading>{title}</Alert.Heading>
<span>{description}</span>
<div>{description}</div>
</Alert>
);

Expand Down
56 changes: 56 additions & 0 deletions src/generic/render-error-alert/RenderErrorAlert.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { render } from '@testing-library/react';
import { CheckCircle as CheckCircleIcon } from '@edx/paragon/icons';
import { IntlProvider } from '@edx/frontend-platform/i18n';

import RenderErrorAlert from '.';
import messages from './messages';

const defaultTitle = messages.alertRenderErrorTitle.defaultMessage;
const defaultDescription = messages.alertRenderErrorDescription.defaultMessage;
const defaultErrorFullMessage = messages.alertRenderErrorMessage.defaultMessage;
const defaultErrorMessage = 'default error message';
const customClassName = 'some-class';
const customErrorMessage = 'custom error message';

const RootWrapper = (props) => (
<IntlProvider locale="en">
<RenderErrorAlert
errorMessage={defaultErrorMessage}
{...props}
/>
</IntlProvider>
);

describe('<RenderErrorAlert />', () => {
it('renders default values when no props are provided', () => {
const { getByText } = render(<RootWrapper />);
const titleElement = getByText(defaultTitle);
const descriptionElement = getByText(defaultDescription);
expect(titleElement).toBeInTheDocument();
expect(descriptionElement).toBeInTheDocument();
expect(getByText(defaultErrorFullMessage.replace('{message}', defaultErrorMessage))).toBeInTheDocument();
});

it('renders provided props correctly', () => {
const customProps = {
variant: 'success',
icon: CheckCircleIcon,
title: 'Custom Title',
description: 'Custom Description',
errorMessage: customErrorMessage,
};
const { getByText } = render(<RootWrapper {...customProps} />);

expect(getByText(customProps.title)).toBeInTheDocument();
expect(getByText(customProps.description)).toBeInTheDocument();
});

it('renders the alert with additional props', () => {
const { getByRole } = render(<RootWrapper className={customClassName} />);
const alertElement = getByRole('alert');
const classNameExists = alertElement.classList.contains(customClassName);
expect(alertElement).toBeInTheDocument();
expect(classNameExists).toBe(true);
});
});
44 changes: 44 additions & 0 deletions src/generic/render-error-alert/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import { Info as InfoIcon } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';

import AlertMessage from '../alert-message';
import messages from './messages';

const RenderErrorAlert = ({
variant, icon, title, description, errorMessage, ...props
}) => {
const intl = useIntl();

return (
<AlertMessage
variant={variant}
icon={icon}
title={title || intl.formatMessage(messages.alertRenderErrorTitle)}
description={description || (
<>
<p className="mt-4 mb-1">{intl.formatMessage(messages.alertRenderErrorDescription)}</p>
<p className="mb-0">{intl.formatMessage(messages.alertRenderErrorMessage, { message: errorMessage })}</p>
</>
)}
{...props}
/>
);
};

RenderErrorAlert.defaultProps = {
icon: InfoIcon,
variant: 'danger',
title: '',
description: '',
};

RenderErrorAlert.propTypes = {
variant: 'danger',
icon: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
errorMessage: PropTypes.string.isRequired,
};

export default RenderErrorAlert;
Loading

0 comments on commit 29e7e13

Please sign in to comment.