fix: do open editor of new xblock when duplicating (#2017)

* feat: display editors as modals  (#1838)

* fix: do open editor of new xblock when duplicating (#1887)

Fixes bug where after duplicating an xblock, the editor modal of the old xblock is being open instead of the new copied xblock.
This commit is contained in:
Daniel Valenzuela
2025-05-22 11:04:35 -04:00
committed by GitHub
parent dd731a0d19
commit 944d1316ad
29 changed files with 718 additions and 475 deletions

View File

@@ -65,6 +65,7 @@ import xblockContainerIframeMessages from './xblock-container-iframe/messages';
import headerNavigationsMessages from './header-navigations/messages';
import sidebarMessages from './sidebar/messages';
import messages from './messages';
import * as selectors from '../data/selectors';
let axiosMock;
let store;
@@ -166,27 +167,27 @@ describe('<CourseUnit />', () => {
});
it('render CourseUnit component correctly', async () => {
const { getByText, getByRole, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
const unitHeaderTitle = screen.getByTestId('unit-header-title');
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).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();
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
});
});
it('renders the course unit iframe with correct attributes', async () => {
const { getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
expect(iframe).toHaveAttribute('style', 'height: 0px;');
@@ -210,27 +211,27 @@ describe('<CourseUnit />', () => {
});
it('displays an error alert when a studioAjaxError message is received', async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.studioAjaxError, {
error: 'Some error text...',
});
});
expect(getByTestId('saving-error-alert')).toBeInTheDocument();
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument();
});
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
const { getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
const legacyXBlockEditModalIframe = getByTitle(
const legacyXBlockEditModalIframe = screen.getByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
);
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
@@ -248,14 +249,14 @@ describe('<CourseUnit />', () => {
});
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
const { getByTitle, queryByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
const legacyXBlockEditModalIframe = queryByTitle(
const legacyXBlockEditModalIframe = screen.queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
);
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -263,14 +264,14 @@ describe('<CourseUnit />', () => {
});
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
const legacyXBlockEditModalIframe = queryByTitle(
const legacyXBlockEditModalIframe = screen.queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
);
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -285,7 +286,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const courseUnitSidebar = getByTestId('course-unit-sidebar');
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument();
@@ -304,10 +305,10 @@ describe('<CourseUnit />', () => {
});
it('updates course unit sidebar after receiving refreshPositions message', async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.refreshPositions);
});
@@ -321,7 +322,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const courseUnitSidebar = getByTestId('course-unit-sidebar');
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument();
@@ -340,12 +341,10 @@ describe('<CourseUnit />', () => {
});
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
const {
getByTitle, getByText, queryByRole, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(async () => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -356,10 +355,10 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id,
});
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument();
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
const dialog = getByRole('dialog');
const dialog = screen.getByRole('dialog');
expect(dialog).toBeInTheDocument();
// Find the Cancel and Delete buttons within the iframe by their specific classes
@@ -372,7 +371,7 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id,
});
expect(getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toBeInTheDocument();
userEvent.click(deleteButton);
});
@@ -393,14 +392,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
});
axiosMock
@@ -431,28 +430,28 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
);
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
@@ -460,9 +459,7 @@ describe('<CourseUnit />', () => {
});
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
const {
getByTitle, getByRole, getByText, queryByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
id: courseVerticalChildrenMock.children[0].block_id,
@@ -482,8 +479,14 @@ describe('<CourseUnit />', () => {
const updatedCourseVerticalChildren = [
...courseVerticalChildrenMock.children,
{
...courseVerticalChildrenMock.children[0],
name: 'New Cloned XBlock',
block_id: '1234567890',
block_type: 'drag-and-drop-v2',
user_partition_info: {
selectable_partitions: [],
selected_partition_index: -1,
selected_groups_label: '',
},
},
];
@@ -495,9 +498,9 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -526,14 +529,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
});
axiosMock
@@ -542,7 +545,7 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -550,21 +553,21 @@ describe('<CourseUnit />', () => {
);
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
@@ -574,19 +577,19 @@ describe('<CourseUnit />', () => {
it('handles CourseUnit header action buttons', async () => {
const { open } = window;
window.open = jest.fn();
const { getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
const {
draft_preview_link: draftPreviewLink,
published_preview_link: publishedPreviewLink,
} = courseSectionVerticalMock;
await waitFor(() => {
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
userEvent.click(viewLiveButton);
expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
userEvent.click(previewButton);
expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
@@ -596,12 +599,7 @@ describe('<CourseUnit />', () => {
});
it('checks courseUnit title changing when edit query is successfully', async () => {
const {
findByText,
queryByRole,
getByRole,
getByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
let editTitleButton = null;
let titleEditField = null;
const newDisplayName = `${unitDisplayName} new`;
@@ -637,7 +635,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
const unitHeaderTitle = screen.getByTestId('unit-header-title');
editTitleButton = within(unitHeaderTitle)
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
titleEditField = within(unitHeaderTitle)
@@ -645,7 +643,7 @@ describe('<CourseUnit />', () => {
});
expect(titleEditField).not.toBeInTheDocument();
userEvent.click(editTitleButton);
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
await userEvent.clear(titleEditField);
await userEvent.type(titleEditField, newDisplayName);
@@ -653,9 +651,10 @@ describe('<CourseUnit />', () => {
expect(titleEditField).toHaveValue(newDisplayName);
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
expect(titleEditField).not.toBeInTheDocument();
expect(await findByText(newDisplayName)).toBeInTheDocument();
expect(await screen.findByText(newDisplayName)).toBeInTheDocument();
});
it('doesn\'t handle creating xblock and displays an error message', async () => {
@@ -675,15 +674,14 @@ describe('<CourseUnit />', () => {
});
});
it('handle creating Problem xblock and navigate to editor page', async () => {
const { courseKey, locator } = courseCreateXblockMock;
it('handle creating Problem xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
const { getByText, getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
@@ -703,13 +701,18 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
const problemButton = getByRole('button', {
const problemButton = screen.getByRole('button', {
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
hidden: true,
});
userEvent.click(problemButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
});
await waitFor(() => {
expect(screen.getByRole('heading', {
name: new RegExp(`${addComponentMessages.blockEditorModalTitle.defaultMessage}`, 'i'),
})).toBeInTheDocument();
});
axiosMock
@@ -719,66 +722,28 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
});
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
const { getByText, getByRole } = render(<RootWrapper />);
const xblockType = 'text';
axiosMock
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
window.scrollTo(0, 250);
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
await waitFor(() => {
const textButton = screen.getByRole('button', { name: /Text/i });
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
userEvent.click(textButton);
const addXBlockDialog = getByRole('dialog');
expect(addXBlockDialog).toBeInTheDocument();
expect(getByText(
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
)).toBeInTheDocument();
const textRadio = screen.getByRole('radio', { name: /Text/i });
userEvent.click(textRadio);
expect(textRadio).toBeChecked();
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
expect(selectBtn).toBeInTheDocument();
userEvent.click(selectBtn);
});
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
});
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
const { getByRole, getAllByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -788,7 +753,7 @@ describe('<CourseUnit />', () => {
]);
await waitFor(async () => {
units = getAllByTestId('course-unit-btn');
units = screen.getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length);
});
@@ -805,8 +770,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
units = getAllByTestId('course-unit-btn');
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
units = screen.getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -818,7 +783,7 @@ describe('<CourseUnit />', () => {
});
it('the sequence unit is updated after changing the unit header', async () => {
const { getAllByTestId, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
@@ -850,7 +815,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const unitHeaderTitle = getByTestId('unit-header-title');
const unitHeaderTitle = screen.getByTestId('unit-header-title');
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
userEvent.click(editTitleButton);
@@ -862,20 +827,21 @@ describe('<CourseUnit />', () => {
await userEvent.tab();
await waitFor(async () => {
const units = getAllByTestId('course-unit-btn');
const units = screen.getAllByTestId('course-unit-btn');
expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
});
});
it('handles creating Video xblock and navigates to editor page', async () => {
const { courseKey, locator } = courseCreateXblockMock;
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => {
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true });
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
@@ -896,23 +862,23 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
});
userEvent.click(videoButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
});
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
userEvent.click(videoButton);
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
@@ -920,45 +886,124 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
waffleSpy.mockRestore();
});
it('handles creating Video xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
.onPost(getXBlockBaseApiUrl(blockId), {
publish: PUBLISH_TYPES.makePublic,
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
userEvent.click(videoButton);
});
/** TODO -- fix this test.
await waitFor(() => {
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
});
*/
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
});
it('renders course unit details for a draft with unpublished changes', async () => {
const { getByText } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
@@ -966,14 +1011,14 @@ describe('<CourseUnit />', () => {
});
it('renders course unit details in the sidebar', async () => {
const { getByText } = render(<RootWrapper />);
render(<RootWrapper />);
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
await waitFor(() => {
expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitLocationId)).toBeInTheDocument();
expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
});
});
@@ -1011,13 +1056,13 @@ describe('<CourseUnit />', () => {
});
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
const { getByRole, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let draftUnpublishedChangesHeading;
let visibilityCheckbox;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1054,7 +1099,7 @@ describe('<CourseUnit />', () => {
userEvent.click(visibilityCheckbox);
const modalNotification = getByRole('dialog');
const modalNotification = screen.getByRole('dialog');
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
@@ -1086,12 +1131,12 @@ describe('<CourseUnit />', () => {
});
it('should publish course unit after click on the "Publish" button', async () => {
const { getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let publishBtn;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
expect(publishBtn).toBeInTheDocument();
@@ -1125,12 +1170,12 @@ describe('<CourseUnit />', () => {
});
it('should discard changes after click on the "Discard changes" button', async () => {
const { getByTestId, getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let discardChangesBtn;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1140,7 +1185,7 @@ describe('<CourseUnit />', () => {
userEvent.click(discardChangesBtn);
const modalNotification = getByRole('dialog');
const modalNotification = screen.getByRole('dialog');
expect(modalNotification).toBeInTheDocument();
expect(within(modalNotification)
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
@@ -1177,7 +1222,7 @@ describe('<CourseUnit />', () => {
});
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
const { getByRole, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let sidebarVisibilityCheckbox;
let modalVisibilityCheckbox;
@@ -1185,16 +1230,16 @@ describe('<CourseUnit />', () => {
let restrictAccessSelect;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
sidebarVisibilityCheckbox = within(courseUnitSidebar)
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
expect(sidebarVisibilityCheckbox).not.toBeChecked();
const headerConfigureBtn = getByRole('button', { name: /settings/i });
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i });
expect(headerConfigureBtn).toBeInTheDocument();
userEvent.click(headerConfigureBtn);
configureModal = getByTestId('configure-modal');
configureModal = screen.getByTestId('configure-modal');
restrictAccessSelect = within(configureModal)
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
expect(within(configureModal)
@@ -1250,8 +1295,8 @@ describe('<CourseUnit />', () => {
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
});
const { getByText } = render(<RootWrapper />);
await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
render(<RootWrapper />);
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); });
});
it('hides the Tags sidebar when not enabled', async () => {
@@ -1259,15 +1304,13 @@ describe('<CourseUnit />', () => {
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
});
const { queryByText } = render(<RootWrapper />);
await waitFor(() => { expect(queryByText('Unit tags')).not.toBeInTheDocument(); });
render(<RootWrapper />);
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); });
});
describe('Copy paste functionality', () => {
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
const {
getAllByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1279,8 +1322,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
@@ -1291,7 +1334,7 @@ describe('<CourseUnit />', () => {
]);
await waitFor(() => {
units = getAllByTestId('course-unit-btn');
units = screen.getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length);
});
@@ -1307,7 +1350,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
units = getAllByTestId('course-unit-btn');
units = screen.getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -1318,7 +1361,7 @@ describe('<CourseUnit />', () => {
});
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
const { getByRole, getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.copyXBlock, {
id: courseVerticalChildrenMock.children[0].block_id,
@@ -1337,11 +1380,11 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1377,7 +1420,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1387,9 +1430,7 @@ describe('<CourseUnit />', () => {
});
it('displays a notification about new files after pasting a component', async () => {
const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1401,8 +1442,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1421,7 +1462,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const newFilesAlert = getByTestId('has-new-files-alert');
const newFilesAlert = screen.getByTestId('has-new-files-alert');
expect(within(newFilesAlert)
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
@@ -1435,13 +1476,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
expect(queryByTestId('has-new-files-alert')).toBeNull();
expect(screen.queryByTestId('has-new-files-alert')).toBeNull();
});
it('displays a notification about conflicting errors after pasting a component', async () => {
const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1453,8 +1492,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1475,7 +1514,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
const conflictingErrorsAlert = screen.getByTestId('has-conflicting-errors-alert');
expect(within(conflictingErrorsAlert)
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1489,13 +1528,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull();
});
it('displays a notification about error files after pasting a component', async () => {
const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1507,8 +1544,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1529,7 +1566,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const errorFilesAlert = getByTestId('has-error-files-alert');
const errorFilesAlert = screen.getByTestId('has-error-files-alert');
expect(within(errorFilesAlert)
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1538,11 +1575,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
expect(queryByTestId('has-error-files')).toBeNull();
expect(screen.queryByTestId('has-error-files')).toBeNull();
});
it('should hide the "Paste component" block if canPasteComponent is false', async () => {
const { queryByText, queryByRole } = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId))
@@ -1553,10 +1590,10 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
expect(queryByRole('button', {
expect(screen.queryByRole('button', {
name: messages.pasteButtonText.defaultMessage,
})).not.toBeInTheDocument();
expect(queryByText(
expect(screen.queryByText(
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
)).not.toBeInTheDocument();
});
@@ -1590,9 +1627,7 @@ describe('<CourseUnit />', () => {
});
it('should display "Move Modal" on receive trigger message', async () => {
const {
getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
await screen.findByText(unitDisplayName);
@@ -1606,15 +1641,12 @@ describe('<CourseUnit />', () => {
await screen.findByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
);
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
});
it('should navigates to xBlock current unit', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
await screen.findByText(unitDisplayName);
@@ -1630,7 +1662,7 @@ describe('<CourseUnit />', () => {
);
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
const currentSectionItemBtn = screen.getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1638,7 +1670,7 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
const currentSubsection = currentSection.child_info.children[0];
const currentSubsectionItemBtn = getByRole('button', {
const currentSubsectionItemBtn = screen.getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1646,7 +1678,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const currentComponentLocationText = getByText(
const currentComponentLocationText = screen.getByText(
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
);
expect(currentComponentLocationText).toBeInTheDocument();
@@ -1654,9 +1686,7 @@ describe('<CourseUnit />', () => {
});
it('should allow move operation and handles it successfully', async () => {
const {
getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
@@ -1680,7 +1710,7 @@ describe('<CourseUnit />', () => {
);
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
const currentSectionItemBtn = screen.getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1688,7 +1718,7 @@ describe('<CourseUnit />', () => {
const currentSubsection = currentSection.child_info.children[1];
await waitFor(() => {
const currentSubsectionItemBtn = getByRole('button', {
const currentSubsectionItemBtn = screen.getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1697,14 +1727,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
const currentUnit = currentSubsection.child_info.children[0];
const currentUnitItemBtn = getByRole('button', {
const currentUnitItemBtn = screen.getByRole('button', {
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentUnitItemBtn).toBeInTheDocument();
userEvent.click(currentUnitItemBtn);
});
const moveModalBtn = getByRole('button', {
const moveModalBtn = screen.getByRole('button', {
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
});
expect(moveModalBtn).toBeInTheDocument();
@@ -1718,10 +1748,7 @@ describe('<CourseUnit />', () => {
});
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
const {
queryByRole,
getByText,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
@@ -1738,18 +1765,18 @@ describe('<CourseUnit />', () => {
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
const dismissButton = queryByRole('button', {
const dismissButton = screen.queryByRole('button', {
name: /dismiss/i, hidden: true,
});
const undoButton = queryByRole('button', {
const undoButton = screen.queryByRole('button', {
name: messages.undoMoveButton.defaultMessage, hidden: true,
});
const newLocationButton = queryByRole('button', {
const newLocationButton = screen.queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
expect(undoButton).toBeInTheDocument();
expect(newLocationButton).toBeInTheDocument();
@@ -1757,9 +1784,9 @@ describe('<CourseUnit />', () => {
userEvent.click(undoButton);
await waitFor(() => {
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
});
expect(getByText(
expect(screen.getByText(
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
@@ -1768,9 +1795,7 @@ describe('<CourseUnit />', () => {
});
it('should navigate to new location by button click', async () => {
const {
queryByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
@@ -1785,7 +1810,7 @@ describe('<CourseUnit />', () => {
callbackFn: requestData.callbackFn,
}), store.dispatch);
const newLocationButton = queryByRole('button', {
const newLocationButton = screen.queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
userEvent.click(newLocationButton);
@@ -1798,16 +1823,14 @@ describe('<CourseUnit />', () => {
describe('XBlock restrict access', () => {
it('opens xblock restrict access modal successfully', async () => {
const {
getByTitle, getByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const usageId = courseVerticalChildrenMock.children[0].block_id;
expect(iframe).toBeInTheDocument();
@@ -1817,7 +1840,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const configureModal = getByTestId('configure-modal');
const configureModal = screen.getByTestId('configure-modal');
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
@@ -1826,12 +1849,10 @@ describe('<CourseUnit />', () => {
});
it('closes xblock restrict access modal when cancel button is clicked', async () => {
const {
getByTitle, queryByTestId, getByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
usageId: courseVerticalChildrenMock.children[0].block_id,
@@ -1839,7 +1860,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const configureModal = getByTestId('configure-modal');
const configureModal = screen.getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument();
userEvent.click(within(configureModal).getByRole('button', {
name: configureModalMessages.cancelButton.defaultMessage,
@@ -1847,7 +1868,7 @@ describe('<CourseUnit />', () => {
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
});
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
});
it('handles submit xblock restrict access data when save button is clicked', async () => {
@@ -1858,15 +1879,13 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
const {
getByTitle, getByRole, getByTestId, queryByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
});
@@ -1876,13 +1895,13 @@ describe('<CourseUnit />', () => {
});
});
const configureModal = await waitFor(() => getByTestId('configure-modal'));
const configureModal = await waitFor(() => screen.getByTestId('configure-modal'));
expect(configureModal).toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
const restrictAccessSelect = getByRole('combobox', {
const restrictAccessSelect = screen.getByRole('combobox', {
name: configureModalMessages.restrictAccessTo.defaultMessage,
});
@@ -1912,17 +1931,17 @@ describe('<CourseUnit />', () => {
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
});
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
});
});
const checkLegacyEditModalOnEditMessage = async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const editButton = getByTestId('header-edit-button');
const editButton = screen.getByTestId('header-edit-button');
expect(editButton).toBeInTheDocument();
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
userEvent.click(editButton);
});
@@ -2109,46 +2128,65 @@ describe('<CourseUnit />', () => {
});
it('should render split test content page correctly', async () => {
const {
getByText,
getByRole,
queryByRole,
getByTestId,
queryByText,
} = render(<RootWrapper />);
render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
const unitHeaderTitle = screen.getByTestId('unit-header-title');
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
// Sidebar
const sidebarContent = [
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage },
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage },
{ query: queryByRole, type: 'link', name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage },
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage
.replaceAll('{bold_tag}', ''),
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'link',
name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
},
];
sidebarContent.forEach(({ query, type, name }) => {
@@ -2156,7 +2194,7 @@ describe('<CourseUnit />', () => {
});
expect(
queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
).toHaveAttribute('href', helpLinkUrl);
});
});
@@ -2169,7 +2207,7 @@ describe('<CourseUnit />', () => {
});
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
const { getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
@@ -2196,7 +2234,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.currentXBlockId, {
id: targetBlockId,

View File

@@ -1,13 +1,14 @@
import { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, StandardModal, useToggle,
} from '@openedx/paragon';
import { getCourseSectionVertical } from '../data/selectors';
import { getWaffleFlags } from '../../data/selectors';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn';
@@ -16,6 +17,8 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useEventListener } from '../../generic/hooks';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
const AddComponent = ({
parentLocator,
@@ -24,7 +27,6 @@ const AddComponent = ({
addComponentTemplateData,
handleCreateNewCourseXBlock,
}) => {
const navigate = useNavigate();
const intl = useIntl();
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
@@ -32,10 +34,17 @@ const AddComponent = ({
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState(null);
const [courseId, setCourseId] = useState(null);
const [newBlockId, setNewBlockId] = useState(null);
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]);
const [usageId, setUsageId] = useState(null);
const { sendMessageToIframe } = useIframe();
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
const receiveMessage = useCallback(({ data: { type, payload } }) => {
if (type === messageTypes.showMultipleComponentPicker) {
@@ -54,6 +63,12 @@ const AddComponent = ({
closeSelectLibraryContentModal();
}, [selectedComponents]);
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
const handleLibraryV2Selection = useCallback((selection) => {
handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2,
@@ -71,12 +86,28 @@ const AddComponent = ({
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break;
case COMPONENT_TYPES.problem:
case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
showXBlockEditorModal();
});
break;
case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock(
{ type, parentLocator: blockId },
/* istanbul ignore next */ ({ courseKey, locator }) => {
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
if (useVideoGalleryFlow) {
showVideoSelectorModal();
} else {
showXBlockEditorModal();
}
},
);
break;
// TODO: The library functional will be a bit different of current legacy (CMS)
// behaviour and this ticket is on hold (blocked by other development team).
case COMPONENT_TYPES.library:
@@ -99,9 +130,11 @@ const AddComponent = ({
type,
boilerplate: moduleName,
parentLocator: blockId,
}, ({ courseKey, locator }) => {
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
navigate(`/course/${courseKey}/editor/html/${locator}`);
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
showXBlockEditorModal();
});
break;
default:
@@ -201,6 +234,43 @@ const AddComponent = ({
onChangeComponentSelection={setSelectedComponents}
/>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.videoPickerModalTitle)}
isOpen={isVideoSelectorModalOpen}
onClose={closeVideoSelectorModal}
isOverflowVisible={false}
size="xl"
>
<div className="selector-page">
<VideoSelectorPage
blockId={newBlockId}
courseId={courseId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onCancel={closeVideoSelectorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.blockEditorModalTitle)}
isOpen={isXBlockEditorModalOpen}
onClose={closeXBlockEditorModal}
isOverflowVisible={false}
size="xl"
>
<div className="editor-page">
<EditorPage
courseId={courseId}
blockType={blockType}
blockId={newBlockId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={closeXBlockEditorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
</div>
);
}

View File

@@ -31,6 +31,16 @@ const messages = defineMessages({
defaultMessage: 'Add selected components',
description: 'Problem bank component add button text.',
},
videoPickerModalTitle: {
id: 'course-authoring.course-unit.modal.video-title.text',
defaultMessage: 'Select video',
description: 'Video picker modal title.',
},
blockEditorModalTitle: {
id: 'course-authoring.course-unit.modal.block-editor-title.text',
defaultMessage: 'Edit component',
description: 'Block editor modal title.',
},
modalContainerTitle: {
id: 'course-authoring.course-unit.modal.container.title',
defaultMessage: 'Add {componentTitle} component',

View File

@@ -1,11 +1,11 @@
export type UseMessageHandlersTypes = {
courseId: string;
navigate: (path: string) => void;
dispatch: (action: any) => void;
setIframeOffset: (height: number) => void;
handleDeleteXBlock: (usageId: string) => void;
handleScrollToXBlock: (scrollOffset: number) => void;
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
handleDuplicateXBlock: (usageId: string) => void;
handleEditXBlock: (blockType: string, usageId: string) => void;
handleManageXBlockAccess: (usageId: string) => void;
handleShowLegacyEditXBlockModal: (id: string) => void;
handleCloseLegacyEditorXBlockModal: () => void;
@@ -14,7 +14,6 @@ export type UseMessageHandlersTypes = {
handleOpenManageTagsModal: (id: string) => void;
handleShowProcessingNotification: (variant: string) => void;
handleHideProcessingNotification: () => void;
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
};
export type MessageHandlersTypes = Record<string, (payload: any) => void>;

View File

@@ -16,7 +16,6 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
*/
export const useMessageHandlers = ({
courseId,
navigate,
dispatch,
setIframeOffset,
handleDeleteXBlock,
@@ -30,15 +29,15 @@ export const useMessageHandlers = ({
handleOpenManageTagsModal,
handleShowProcessingNotification,
handleHideProcessingNotification,
handleRedirectToXBlockEditPage,
handleEditXBlock,
}: UseMessageHandlersTypes): MessageHandlersTypes => {
const { copyToClipboard } = useClipboard();
return useMemo(() => ({
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId),
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
[messageTypes.toggleCourseXBlockDropdown]: ({
@@ -52,9 +51,14 @@ export const useMessageHandlers = ({
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
[messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
[messageTypes.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification(
NOTIFICATION_MESSAGES.copying,
),
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock(
payload.type,
payload.locator,
),
}), [
courseId,
handleDeleteXBlock,

View File

@@ -1,10 +1,10 @@
import { getConfig } from '@edx/frontend-platform';
import {
FC, useEffect, useState, useMemo, useCallback,
} from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle, Sheet } from '@openedx/paragon';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import {
hideProcessingNotification,
@@ -13,9 +13,9 @@ import {
import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import ModalIframe from '../../generic/modal-iframe';
import { getWaffleFlags } from '../../data/selectors';
import { IFRAME_FEATURE_POLICY } from '../../constants';
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
import supportedEditors from '../../editors/supportedEditors';
import { useIframe } from '../../generic/hooks/context/hooks';
import {
fetchCourseSectionVerticalData,
@@ -35,16 +35,22 @@ import messages from './messages';
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
import { useIframeContent } from '../../generic/hooks/useIframeContent';
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
}) => {
const intl = useIntl();
const dispatch = useDispatch();
const navigate = useNavigate();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState<string>('');
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
const [newBlockId, setNewBlockId] = useState<string>('');
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
const [iframeOffset, setIframeOffset] = useState(0);
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
@@ -64,14 +70,27 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
setIframeRef(iframeRef);
}, [setIframeRef]);
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
const handleEditXBlock = useCallback((type: string, id: string) => {
setBlockType(type);
setNewBlockId(id);
if (type === 'video' && useVideoGalleryFlow) {
showVideoSelectorModal();
} else {
showXBlockEditorModal();
}
}, [showVideoSelectorModal, showXBlockEditorModal]);
const handleDuplicateXBlock = useCallback(
(blockType: string, usageId: string) => {
(usageId: string) => {
unitXBlockActions.handleDuplicate(usageId);
if (supportedEditors[blockType]) {
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
}
},
[unitXBlockActions, courseId, navigate],
[unitXBlockActions, courseId],
);
const handleDeleteXBlock = (usageId: string) => {
@@ -147,13 +166,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
dispatch(hideProcessingNotification());
};
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
};
const messageHandlers = useMessageHandlers({
courseId,
navigate,
dispatch,
setIframeOffset,
handleDeleteXBlock,
@@ -167,7 +181,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
handleOpenManageTagsModal,
handleShowProcessingNotification,
handleHideProcessingNotification,
handleRedirectToXBlockEditPage,
handleEditXBlock,
});
useIframeMessages(messageHandlers);
@@ -186,6 +200,43 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
close={closeDeleteModal}
onDeleteSubmit={onDeleteSubmit}
/>
<StandardModal
title={intl.formatMessage(messages.videoPickerModalTitle)}
isOpen={isVideoSelectorModalOpen}
onClose={closeVideoSelectorModal}
isOverflowVisible={false}
size="xl"
>
<div className="selector-page">
<VideoSelectorPage
blockId={newBlockId}
courseId={courseId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onCancel={closeVideoSelectorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.blockEditorModalTitle)}
isOpen={isXBlockEditorModalOpen}
onClose={closeXBlockEditorModal}
isOverflowVisible={false}
size="xl"
>
<div className="editor-page">
<EditorPage
courseId={courseId}
blockType={blockType}
blockId={newBlockId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={closeXBlockEditorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
{Object.keys(accessManagedXBlockData).length ? (
<ConfigureModal
isXBlockComponent

View File

@@ -15,6 +15,15 @@ const messages = defineMessages({
id: 'course-authoring.course-unit.xblock.iframe.label',
defaultMessage: '{xblockCount} xBlocks inside the frame',
},
videoPickerModalTitle: {
id: 'course-authoring.course-unit.xblock.video-editor.title',
defaultMessage: 'Select video',
},
blockEditorModalTitle: {
id: 'course-authoring.course-unit.xblock.editor.title',
defaultMessage: 'Edit component',
description: 'Block editor modal title.',
},
});
export default messages;

View File

@@ -26,6 +26,7 @@ const slice = createSlice({
useNewCertificatesPage: true,
useNewTextbooksPage: true,
useNewGroupConfigurationsPage: true,
useVideoGalleryFlow: false,
},
},
reducers: {

View File

@@ -7,7 +7,6 @@ import * as hooks from './hooks';
import supportedEditors from './supportedEditors';
import type { EditorComponent } from './EditorComponent';
import { useEditorContext } from './EditorContext';
import AdvancedEditor from './AdvancedEditor';
export interface Props extends EditorComponent {
@@ -17,7 +16,6 @@ export interface Props extends EditorComponent {
learningContextId: string | null;
lmsEndpointUrl: string | null;
studioEndpointUrl: string | null;
fullScreen?: boolean; // eslint-disable-line react/no-unused-prop-types
}
const Editor: React.FC<Props> = ({
@@ -42,7 +40,6 @@ const Editor: React.FC<Props> = ({
studioEndpointUrl,
},
});
const { fullScreen } = useEditorContext();
const EditorComponent = supportedEditors[blockType];
@@ -60,24 +57,7 @@ const Editor: React.FC<Props> = ({
);
}
const innerEditor = <EditorComponent {...{ onClose, returnFunction }} />;
if (fullScreen) {
return (
<div
className="d-flex flex-column"
>
<div
className="pgn__modal-fullscreen h-100"
role="dialog"
aria-label={blockType}
>
{innerEditor}
</div>
</div>
);
}
return innerEditor;
return <EditorComponent {...{ onClose, returnFunction }} />;
};
export default Editor;

View File

@@ -7,14 +7,6 @@ import React from 'react';
*/
export interface EditorContext {
learningContextId: string;
/**
* When editing components in the libraries part of the Authoring MFE, we show
* the editors in a modal (fullScreen = false). This is the preferred approach
* so that authors can see context behind the modal.
* However, when making edits from the legacy course view, we display the
* editors in a fullscreen view. This approach is deprecated.
*/
fullScreen: boolean;
}
const context = React.createContext<EditorContext | undefined>(undefined);
@@ -32,7 +24,6 @@ export function useEditorContext() {
export const EditorContextProvider: React.FC<{
children: React.ReactNode,
learningContextId: string;
fullScreen: boolean;
}> = ({ children, ...contextData }) => {
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
return <context.Provider value={ctx}>{children}</context.Provider>;

View File

@@ -37,7 +37,6 @@ const defaultPropsHtml = {
lmsEndpointUrl: 'http://lms.test.none/',
studioEndpointUrl: 'http://cms.test.none/',
onClose: jest.fn(),
fullScreen: false,
};
const fieldsHtml = {
displayName: 'Introduction to Testing',
@@ -66,22 +65,6 @@ describe('EditorPage', () => {
expect(modalElement.classList).not.toContain('pgn__modal-fullscreen');
});
test('it can display the Text (html) editor as a full page (when coming from the legacy UI)', async () => {
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
{ status: 200, data: snakeCaseObject(fieldsHtml) }
));
render(<EditorPage {...defaultPropsHtml} fullScreen />);
// Then the editor should open
expect(await screen.findByRole('heading', { name: /Introduction to Testing/ })).toBeInTheDocument();
const modalElement = screen.getByRole('dialog');
expect(modalElement.classList).toContain('pgn__modal-fullscreen');
expect(modalElement.classList).not.toContain('pgn__modal');
expect(modalElement.classList).not.toContain('pgn__modal-xl');
});
test('it shows the Advanced Editor if there is no corresponding editor', async () => {
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line
{ status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } }

View File

@@ -14,7 +14,6 @@ interface Props extends EditorComponent {
isMarkdownEditorEnabledForCourse?: boolean;
lmsEndpointUrl?: string;
studioEndpointUrl?: string;
fullScreen?: boolean;
children?: never;
}
@@ -31,7 +30,6 @@ const EditorPage: React.FC<Props> = ({
studioEndpointUrl = null,
onClose = null,
returnFunction = null,
fullScreen = true,
}) => (
<Provider store={store}>
<ErrorBoundary
@@ -40,7 +38,7 @@ const EditorPage: React.FC<Props> = ({
studioEndpointUrl,
}}
>
<EditorContextProvider fullScreen={fullScreen} learningContextId={courseId}>
<EditorContextProvider learningContextId={courseId}>
<Editor
{...{
onClose,

View File

@@ -9,6 +9,8 @@ const VideoSelector = ({
learningContextId,
lmsEndpointUrl,
studioEndpointUrl,
returnFunction,
onCancel,
}) => {
const dispatch = useDispatch();
const loading = hooks.useInitializeApp({
@@ -26,7 +28,7 @@ const VideoSelector = ({
return null;
}
return (
<VideoGallery />
<VideoGallery returnFunction={returnFunction} onCancel={onCancel} />
);
};
@@ -35,6 +37,8 @@ VideoSelector.propTypes = {
learningContextId: PropTypes.string.isRequired,
lmsEndpointUrl: PropTypes.string.isRequired,
studioEndpointUrl: PropTypes.string.isRequired,
returnFunction: PropTypes.func,
onCancel: PropTypes.func,
};
export default VideoSelector;

View File

@@ -10,6 +10,8 @@ const VideoSelectorPage = ({
courseId,
lmsEndpointUrl,
studioEndpointUrl,
returnFunction,
onCancel,
}) => (
<Provider store={store}>
<ErrorBoundary
@@ -24,6 +26,8 @@ const VideoSelectorPage = ({
learningContextId: courseId,
lmsEndpointUrl,
studioEndpointUrl,
returnFunction,
onCancel,
}}
/>
</ErrorBoundary>
@@ -42,6 +46,8 @@ VideoSelectorPage.propTypes = {
courseId: PropTypes.string,
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
returnFunction: PropTypes.func,
onCancel: PropTypes.func,
};
export default VideoSelectorPage;

View File

@@ -32,7 +32,6 @@ const defaultPropsHtml = {
lmsEndpointUrl: 'http://lms.test.none/',
studioEndpointUrl: 'http://cms.test.none/',
onClose: jest.fn(),
fullScreen: false,
};
const fieldsHtml = {
displayName: 'Introduction to Testing',

View File

@@ -14,7 +14,6 @@ import { Close } from '@openedx/paragon/icons';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { EditorComponent } from '../../EditorComponent';
import { useEditorContext } from '../../EditorContext';
import TitleHeader from './components/TitleHeader';
import * as hooks from './hooks';
import messages from './messages';
@@ -30,37 +29,18 @@ interface WrapperProps {
}
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
const { fullScreen } = useEditorContext();
const intl = useIntl();
if (fullScreen) {
return (
<div
className="editor-container d-flex flex-column position-relative zindex-0"
style={{ minHeight: '100%' }}
>
{children}
</div>
);
}
const title = intl.formatMessage(messages.modalTitle);
return (
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog>
);
};
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => {
const { fullScreen } = useEditorContext();
return <ModalDialog.Body className={fullScreen ? 'pb-6' : 'pb-0'}>{ children }</ModalDialog.Body>;
};
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => <ModalDialog.Body className="pb-0">{ children }</ModalDialog.Body>;
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => {
const { fullScreen } = useEditorContext();
if (fullScreen) {
return <div className="editor-footer fixed-bottom">{children}</div>;
}
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{ children }</>;
};
// eslint-disable-next-line react/jsx-no-useless-fragment
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{ children }</>;
interface Props extends EditorComponent {
children: React.ReactNode;

View File

@@ -20,7 +20,7 @@ describe('SelectTypeModal', () => {
jest.spyOn(hooks, 'onSelect').mockImplementation(mockSelect);
// This is a new-style test, unlike most of the old snapshot-based editor tests.
render(
<EditorContextProvider fullScreen={false} learningContextId="course-v1:Org+COURSE+RUN">
<EditorContextProvider learningContextId="course-v1:Org+COURSE+RUN">
<Provider store={editorStore}>
<SelectTypeModal onClose={mockClose} />
</Provider>

View File

@@ -18,6 +18,7 @@ exports[`VideoEditor snapshots renders as expected with default behavior 1`] = `
"useSelector": [MockFunction],
}
}
onClose={[MockFunction props.onClose]}
/>
</div>
</EditorContainer>

View File

@@ -7,7 +7,9 @@ import VideoSettingsModal from './VideoSettingsModal';
import { RequestKeys } from '../../../data/constants/requests';
interface Props {
onReturn?: (() => void);
isLibrary: boolean;
onClose?: (() => void) | null;
}
export const {
@@ -27,13 +29,15 @@ export const hooks = {
const VideoEditorModal: React.FC<Props> = ({
isLibrary,
onClose,
onReturn,
}) => {
const dispatch = useDispatch();
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const selectedVideoId = searchParams.get('selectedVideoId');
const selectedVideoUrl = searchParams.get('selectedVideoUrl');
const onReturn = hooks.useReturnToGallery();
const onSettingsReturn = onReturn || hooks.useReturnToGallery();
const isLoaded = useSelector(
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
);
@@ -44,8 +48,9 @@ const VideoEditorModal: React.FC<Props> = ({
return (
<VideoSettingsModal {...{
onReturn,
onReturn: onSettingsReturn,
isLibrary,
onClose,
}}
/>
);

View File

@@ -20,11 +20,13 @@ import messages from '../../messages';
interface Props {
onReturn: () => void;
isLibrary: boolean;
onClose?: (() => void) | null;
}
const VideoSettingsModal: React.FC<Props> = ({
onReturn,
isLibrary,
onClose,
}) => (
<>
{!isLibrary && (
@@ -32,7 +34,7 @@ const VideoSettingsModal: React.FC<Props> = ({
variant="link"
className="text-primary-500"
size="sm"
onClick={onReturn}
onClick={onClose || onReturn}
style={{
textDecoration: 'none',
marginLeft: '3px',

View File

@@ -39,7 +39,7 @@ const VideoEditor: React.FC<EditorComponent> = ({
>
{(isCreateWorkflow || studioViewFinished) ? (
<div className="video-editor">
<VideoEditorModal {...{ isLibrary }} />
<VideoEditorModal {...{ isLibrary, onClose, returnFunction }} />
</div>
) : (
<div style={{

View File

@@ -86,6 +86,7 @@ export const filterList = ({
export const useVideoListProps = ({
searchSortProps,
videos,
returnFunction,
}) => {
const [highlighted, setHighlighted] = React.useState(null);
const [
@@ -128,7 +129,10 @@ export const useVideoListProps = ({
},
selectBtnProps: {
onClick: () => {
if (highlighted) {
/* istanbul ignore next */
if (returnFunction) {
returnFunction()();
} else if (highlighted) {
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
} else {
setShowSelectVideoError(true);
@@ -138,10 +142,15 @@ export const useVideoListProps = ({
};
};
export const useVideoUploadHandler = ({ replace }) => {
export const useVideoUploadHandler = ({ replace, uploadHandler }) => {
const learningContextId = useSelector(selectors.app.learningContextId);
const blockId = useSelector(selectors.app.blockId);
const path = `/course/${learningContextId}/editor/video_upload/${blockId}`;
if (uploadHandler) {
return () => {
uploadHandler();
};
}
if (replace) {
return () => window.location.replace(path);
}
@@ -191,11 +200,12 @@ export const getstatusBadgeVariant = ({ status }) => {
export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status);
export const useVideoProps = ({ videos }) => {
export const useVideoProps = ({ videos, uploadHandler, returnFunction }) => {
const searchSortProps = useSearchAndSortProps();
const videoList = useVideoListProps({
searchSortProps,
videos,
returnFunction,
});
const {
galleryError,
@@ -203,7 +213,7 @@ export const useVideoProps = ({ videos }) => {
inputError,
selectBtnProps,
} = videoList;
const fileInput = { click: useVideoUploadHandler({ replace: false }) };
const fileInput = { click: useVideoUploadHandler({ replace: false, uploadHandler }) };
return {
galleryError,

View File

@@ -1,5 +1,10 @@
import React, { useEffect } from 'react';
import { Image } from '@openedx/paragon';
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Image, useToggle, StandardModal,
} from '@openedx/paragon';
import { useSearchParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { selectors } from '../../data/redux';
import * as hooks from './hooks';
@@ -8,8 +13,11 @@ import { acceptedImgKeys } from './utils';
import messages from './messages';
import { RequestKeys } from '../../data/constants/requests';
import videoThumbnail from '../../data/images/videoThumbnail.svg';
import VideoUploadEditor from '../VideoUploadEditor';
import VideoEditor from '../VideoEditor';
const VideoGallery = () => {
const VideoGallery = ({ returnFunction, onCancel }) => {
const intl = useIntl();
const rawVideos = useSelector(selectors.app.videos);
const isLoaded = useSelector(
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
@@ -21,14 +29,27 @@ const VideoGallery = () => {
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
);
const videos = hooks.buildVideos({ rawVideos });
const handleVideoUpload = hooks.useVideoUploadHandler({ replace: true });
const [isVideoUploadModalOpen, showVideoUploadModal, closeVideoUploadModal] = useToggle();
const [isVideoEditorModalOpen, showVideoEditorModal, closeVideoEditorModal] = useToggle();
const setSearchParams = useSearchParams()[1];
useEffect(() => {
// If no videos exists redirects to the video upload screen
// If no videos exists opens to the video upload modal
if (isLoaded && videos.length === 0) {
handleVideoUpload();
showVideoUploadModal();
}
}, [isLoaded]);
const onVideoUpload = useCallback((videoUrl) => {
closeVideoUploadModal();
showVideoEditorModal();
setSearchParams({ selectedVideoUrl: videoUrl });
}, [closeVideoUploadModal, showVideoEditorModal, setSearchParams]);
const uploadHandler = useCallback(() => {
showVideoUploadModal();
});
const {
galleryError,
inputError,
@@ -36,7 +57,7 @@ const VideoGallery = () => {
galleryProps,
searchSortProps,
selectBtnProps,
} = hooks.useVideoProps({ videos });
} = hooks.useVideoProps({ videos, uploadHandler, returnFunction });
const handleCancel = hooks.useCancelHandler();
const modalMessages = {
@@ -60,8 +81,8 @@ const VideoGallery = () => {
<SelectionModal
{...{
isOpen: true,
close: handleCancel,
size: 'fullscreen',
close: onCancel || handleCancel,
size: 'xl',
isFullscreenScroll: false,
galleryError,
inputError,
@@ -79,10 +100,34 @@ const VideoGallery = () => {
isFetchError,
}}
/>
<StandardModal
title={intl.formatMessage(messages.videoUploadModalTitle)}
isOpen={isVideoUploadModalOpen}
onClose={closeVideoUploadModal}
isOverflowVisible={false}
size="xl"
hasCloseButton={false}
>
<div className="editor-page">
<VideoUploadEditor
onUpload={onVideoUpload}
onClose={closeVideoUploadModal}
/>
</div>
</StandardModal>
{isVideoEditorModalOpen && (
<VideoEditor
onClose={closeVideoEditorModal}
returnFunction={returnFunction}
/>
)}
</div>
);
};
VideoGallery.propTypes = {};
VideoGallery.propTypes = {
onCancel: PropTypes.func,
returnFunction: PropTypes.func,
};
export default VideoGallery;

View File

@@ -6,6 +6,8 @@ import React from 'react';
import {
act, fireEvent, render, screen,
} from '@testing-library/react';
import * as reactRouterDom from 'react-router-dom';
import * as reduxThunks from '../../data/redux';
import VideoGallery from './index';
@@ -120,11 +122,10 @@ describe('VideoGallery', () => {
expect(screen.getByText(video.client_video_id)).toBeInTheDocument()
));
});
it('navigates to video upload page when there are no videos', async () => {
expect(window.location.replace).not.toHaveBeenCalled();
it('renders video upload modal when there are no videos', async () => {
updateState({ videos: [] });
await renderComponent();
expect(window.location.replace).toHaveBeenCalled();
expect(screen.getByRole('heading', { name: /upload or embed a new video/i })).toBeInTheDocument();
});
it.each([
[/newest/i, [2, 1, 3]],
@@ -191,5 +192,36 @@ describe('VideoGallery', () => {
expect(screen.queryByText('client_id_1')).not.toBeInTheDocument();
expect(screen.queryByText('client_id_3')).not.toBeInTheDocument();
});
it('calls onVideoUpload correctly when a video is uploaded', async () => {
// Mock useSearchParams
const setSearchParams = jest.fn();
jest.spyOn(reactRouterDom, 'useSearchParams').mockReturnValue([{}, setSearchParams]);
// Mock the uploadVideo thunk to immediately call postUploadRedirect
jest.spyOn(reduxThunks.thunkActions.video, 'uploadVideo').mockImplementation(
({ postUploadRedirect }) => () => {
if (postUploadRedirect) {
postUploadRedirect('http://test.video/url.mp4');
}
return { type: 'MOCK_UPLOAD_VIDEO' };
},
);
await renderComponent();
// Open the upload modal by clicking the button
const openModalButton = screen.getByRole('button', { name: /upload or embed a new video/i });
fireEvent.click(openModalButton);
// Wait for the input to appear in the modal
const urlInput = await screen.findByPlaceholderText('Paste your video ID or URL');
fireEvent.change(urlInput, { target: { value: 'http://test.video/url.mp4' } });
const submitButton = screen.getByRole('button', { name: /submit/i });
fireEvent.click(submitButton);
expect(setSearchParams).toHaveBeenCalledWith({ selectedVideoUrl: 'http://test.video/url.mp4' });
});
});
});

View File

@@ -21,7 +21,16 @@ const messages = {
defaultMessage: 'Upload or embed a new video',
description: 'Label for upload button',
},
videoUploadModalTitle: {
id: 'authoring.selectvideomodal.upload.title',
defaultMessage: 'Upload or embed a new video',
description: 'Label for upload modal',
},
videoEditorModalTitle: {
id: 'authoring.selectvideomodal.edit.title',
defaultMessage: 'Edit selected video',
description: 'Label for editor modal',
},
// Sort Dropdown
sortByDateNewest: {
id: 'authoring.selectvideomodal.sort.datenewest.label',

View File

@@ -10,9 +10,9 @@ import { thunkActions } from '../../data/redux';
import * as hooks from './hooks';
import messages from './messages';
const URLUploader = () => {
const URLUploader = ({ onUpload }) => {
const [textInputValue, setTextInputValue] = React.useState('');
const onURLUpload = hooks.onVideoUpload('selectedVideoUrl');
const onURLUpload = hooks.onVideoUpload('selectedVideoUrl', onUpload);
const intl = useIntl();
return (
<div className="d-flex flex-column">
@@ -58,16 +58,16 @@ const URLUploader = () => {
);
};
export const VideoUploader = ({ setLoading }) => {
export const VideoUploader = ({ setLoading, onUpload, onClose }) => {
const dispatch = useDispatch();
const intl = useIntl();
const goBack = hooks.useHistoryGoBack();
const goBack = onClose || hooks.useHistoryGoBack();
const handleProcessUpload = ({ fileData }) => {
dispatch(thunkActions.video.uploadVideo({
supportedFiles: [fileData],
setLoadSpinner: setLoading,
postUploadRedirect: hooks.onVideoUpload('selectedVideoId'),
postUploadRedirect: hooks.onVideoUpload('selectedVideoId', onUpload),
}));
};
@@ -85,14 +85,20 @@ export const VideoUploader = ({ setLoading }) => {
<Dropzone
accept={{ 'video/*': ['.mp4', '.mov'] }}
onProcessUpload={handleProcessUpload}
inputComponent={<URLUploader />}
inputComponent={<URLUploader onUpload={onUpload} />}
/>
</div>
);
};
URLUploader.propTypes = {
onUpload: PropTypes.func,
};
VideoUploader.propTypes = {
setLoading: PropTypes.func.isRequired,
onUpload: PropTypes.func,
onClose: PropTypes.func,
};
export default VideoUploader;

View File

@@ -11,15 +11,20 @@ export const {
navigateTo,
} = appHooks;
export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl') => {
export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl', onUpload = null) => {
const learningContextId = selectors.app.learningContextId(storeState);
const blockId = selectors.app.blockId(storeState);
if (onUpload) {
return (videoUrl) => {
onUpload(videoUrl, learningContextId, blockId);
};
}
return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?${uploadType}=${videoUrl}`);
};
export const onVideoUpload = (uploadType) => {
export const onVideoUpload = (uploadType, onUpload) => {
const storeState = store.getState();
return module.postUploadRedirect(storeState, uploadType);
return module.postUploadRedirect(storeState, uploadType, onUpload);
};
export const useUploadVideo = async ({

View File

@@ -1,17 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Spinner } from '@openedx/paragon';
import './index.scss';
import messages from './messages';
import { VideoUploader } from './VideoUploader';
const VideoUploadEditor = () => {
const VideoUploadEditor = ({ onUpload, onClose }) => {
const [loading, setLoading] = React.useState(false);
const intl = useIntl();
return (!loading) ? (
<div className="d-flex marked-area flex-column p-3">
<VideoUploader setLoading={setLoading} />
<VideoUploader onUpload={onUpload} onClose={onClose} setLoading={setLoading} />
</div>
) : (
<div style={{
@@ -30,4 +31,9 @@ const VideoUploadEditor = () => {
);
};
VideoUploadEditor.propTypes = {
onUpload: PropTypes.func,
onClose: PropTypes.func,
};
export default VideoUploadEditor;

View File

@@ -41,7 +41,6 @@ export const ComponentEditorModal: React.FC<Record<never, never>> = () => {
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={onClose}
returnFunction={() => onClose}
fullScreen={false}
/>
);
};