Skip to content

Commit

Permalink
feat: [FC-0044] Unit page - make xblock edit functional (#912)
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor-romaniuk authored Apr 2, 2024
1 parent dd13ed4 commit 5247ec5
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 11 deletions.
5 changes: 4 additions & 1 deletion src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,14 @@ const CourseUnit = ({ courseId }) => {
/>
)}
<Stack gap={4} className="mb-4">
{courseVerticalChildren.children.map(({ name, blockId: id, shouldScroll }) => (
{courseVerticalChildren.children.map(({
name, blockId: id, blockType: type, shouldScroll,
}) => (
<CourseXBlock
id={id}
key={id}
title={name}
type={type}
shouldScroll={shouldScroll}
unitXBlockActions={unitXBlockActions}
data-testid="course-xblock"
Expand Down
22 changes: 20 additions & 2 deletions src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,40 @@ import {
} from '@openedx/paragon';
import { EditOutline as EditIcon, MoreVert as MoveVertIcon } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';

import DeleteModal from '../../generic/delete-modal/DeleteModal';
import { scrollToElement } from '../../course-outline/utils';
import { getCourseId } from '../data/selectors';
import { COMPONENT_ICON_TYPES } from '../constants';
import messages from './messages';

const CourseXBlock = ({
id, title, unitXBlockActions, shouldScroll, ...props
id, title, type, unitXBlockActions, shouldScroll, ...props
}) => {
const courseXBlockElementRef = useRef(null);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const navigate = useNavigate();
const courseId = useSelector(getCourseId);
const intl = useIntl();

const onXBlockDelete = () => {
unitXBlockActions.handleDelete(id);
closeDeleteModal();
};

const handleEdit = () => {
switch (type) {
case COMPONENT_ICON_TYPES.html:
case COMPONENT_ICON_TYPES.problem:
case COMPONENT_ICON_TYPES.video:
navigate(`/course/${courseId}/editor/${type}/${id}`);
break;
default:
}
};

useEffect(() => {
// if this item has been newly added, scroll to it.
if (courseXBlockElementRef.current && shouldScroll) {
Expand All @@ -40,7 +57,7 @@ const CourseXBlock = ({
alt={intl.formatMessage(messages.blockAltButtonEdit)}
iconAs={EditIcon}
size="md"
onClick={() => {}}
onClick={handleEdit}
/>
<Dropdown>
<Dropdown.Toggle
Expand Down Expand Up @@ -94,6 +111,7 @@ CourseXBlock.defaultProps = {
CourseXBlock.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
shouldScroll: PropTypes.bool,
unitXBlockActions: PropTypes.shape({
handleDelete: PropTypes.func,
Expand Down
94 changes: 86 additions & 8 deletions src/course-unit/course-xblock/CourseXBlock.test.jsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,71 @@
import { render, waitFor } from '@testing-library/react';
import { useSelector } from 'react-redux';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';

import { getCourseId } from '../data/selectors';
import { COMPONENT_ICON_TYPES } from '../constants';
import { courseVerticalChildrenMock } from '../__mocks__';
import CourseXBlock from './CourseXBlock';

import deleteModalMessages from '../../generic/delete-modal/messages';
import messages from './messages';

let store;
const courseId = '1234';
const blockId = '567890';
const handleDeleteMock = jest.fn();
const handleDuplicateMock = jest.fn();
const xblockData = courseVerticalChildrenMock.children[0];
const handleConfigureSubmitMock = jest.fn();
const mockedUsedNavigate = jest.fn();
const {
name,
block_id: id,
block_type: type,
user_partition_info: userPartitionInfo,
} = courseVerticalChildrenMock.children[0];
const unitXBlockActionsMock = {
handleDelete: handleDeleteMock,
handleDuplicate: handleDuplicateMock,
};

const renderComponent = () => render(
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedUsedNavigate,
}));

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));

const renderComponent = (props) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseXBlock
id={xblockData.block_id}
title={xblockData.block_id}
id={id}
title={name}
type={type}
blockId={blockId}
unitXBlockActions={unitXBlockActionsMock}
userPartitionInfo={camelCaseObject(userPartitionInfo)}
shouldScroll={false}
handleConfigureSubmit={handleConfigureSubmitMock}
{...props}
/>
</IntlProvider>
</AppProvider>,
);

useSelector.mockImplementation((selector) => {
if (selector === getCourseId) {
return courseId;
}
return null;
});

describe('<CourseXBlock />', () => {
beforeEach(async () => {
initializeMockApp({
Expand All @@ -48,7 +82,7 @@ describe('<CourseXBlock />', () => {
const { getByText, getByLabelText } = renderComponent();

await waitFor(() => {
expect(getByText(xblockData.block_id)).toBeInTheDocument();
expect(getByText(name)).toBeInTheDocument();
expect(getByLabelText(messages.blockAltButtonEdit.defaultMessage)).toBeInTheDocument();
expect(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage)).toBeInTheDocument();
});
Expand Down Expand Up @@ -76,7 +110,7 @@ describe('<CourseXBlock />', () => {

userEvent.click(duplicateBtn);
expect(handleDuplicateMock).toHaveBeenCalledTimes(1);
expect(handleDuplicateMock).toHaveBeenCalledWith(xblockData.block_id);
expect(handleDuplicateMock).toHaveBeenCalledWith(id);
});
});

Expand All @@ -102,7 +136,51 @@ describe('<CourseXBlock />', () => {
userEvent.click(deleteBtn);
userEvent.click(getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage }));
expect(handleDeleteMock).toHaveBeenCalled();
expect(handleDeleteMock).toHaveBeenCalledWith(xblockData.block_id);
expect(handleDeleteMock).toHaveBeenCalledWith(id);
});
});

describe('edit', () => {
it('navigates to editor page on edit HTML xblock', () => {
const { getByText, getByRole } = renderComponent({
type: COMPONENT_ICON_TYPES.html,
});

const editButton = getByRole('button', { name: messages.blockAltButtonEdit.defaultMessage });
expect(getByText(name)).toBeInTheDocument();
expect(editButton).toBeInTheDocument();

userEvent.click(editButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/editor/html/${id}`);
});

it('navigates to editor page on edit Video xblock', () => {
const { getByText, getByRole } = renderComponent({
type: COMPONENT_ICON_TYPES.video,
});

const editButton = getByRole('button', { name: messages.blockAltButtonEdit.defaultMessage });
expect(getByText(name)).toBeInTheDocument();
expect(editButton).toBeInTheDocument();

userEvent.click(editButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/editor/video/${id}`);
});

it('navigates to editor page on edit Problem xblock', () => {
const { getByText, getByRole } = renderComponent({
type: COMPONENT_ICON_TYPES.problem,
});

const editButton = getByRole('button', { name: messages.blockAltButtonEdit.defaultMessage });
expect(getByText(name)).toBeInTheDocument();
expect(editButton).toBeInTheDocument();

userEvent.click(editButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/editor/problem/${id}`);
});
});
});

0 comments on commit 5247ec5

Please sign in to comment.