Files
frontend-app-authoring/src/course-outline/CourseOutline.test.jsx
Chris Chávez c3df0b0692 feat: Show toast when exporting course tags (#995)
Show in  in-progress toast when exporting course tags
2024-05-24 15:54:59 +05:30

2290 lines
98 KiB
JavaScript

import {
act, render, waitFor, fireEvent, within, screen,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cloneDeep } from 'lodash';
import { closestCorners } from '@dnd-kit/core';
import { useLocation } from 'react-router-dom';
import {
getCourseBestPracticesApiUrl,
getCourseLaunchApiUrl,
getCourseOutlineIndexApiUrl,
getCourseReindexApiUrl,
getXBlockApiUrl,
getCourseBlockApiUrl,
getCourseItemApiUrl,
getXBlockBaseApiUrl,
exportTags,
} from './data/api';
import { RequestStatus } from '../data/constants';
import {
fetchCourseBestPracticesQuery,
fetchCourseLaunchQuery,
fetchCourseOutlineIndexQuery,
updateCourseSectionHighlightsQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
courseOutlineIndexMock,
courseOutlineIndexWithoutSections,
courseBestPracticesMock,
courseLaunchMock,
courseSectionMock,
courseSubsectionMock,
} from './__mocks__';
import { clipboardUnit } from '../__mocks__';
import { executeThunk } from '../utils';
import { COURSE_BLOCK_NAMES, VIDEO_SHARING_OPTIONS } from './constants';
import CourseOutline from './CourseOutline';
import configureModalMessages from '../generic/configure-modal/messages';
import pasteButtonMessages from '../generic/clipboard/paste-component/messages';
import messages from './messages';
import { getClipboardUrl } from '../generic/data/api';
import headerMessages from './header-navigations/messages';
import cardHeaderMessages from './card-header/messages';
import enableHighlightsModalMessages from './enable-highlights-modal/messages';
import statusBarMessages from './status-bar/messages';
import subsectionMessages from './subsection-card/messages';
import pageAlertMessages from './page-alerts/messages';
import {
moveSubsectionOver,
moveUnitOver,
moveSubsection,
moveUnit,
} from '../generic/drag-helper/utils';
let axiosMock;
let store;
const mockPathname = '/foo-bar';
const courseId = '123';
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
close: jest.fn(),
};
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
jest.mock('../help-urls/hooks', () => ({
useHelpUrls: () => ({
contentHighlights: 'some',
visibility: 'some',
grading: 'some',
outline: 'some',
}),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage: (message) => message.defaultMessage,
}),
}));
jest.mock('./data/api', () => ({
...jest.requireActual('./data/api'),
getTagsCount: () => jest.fn().mockResolvedValue({}),
}));
const queryClient = new QueryClient();
jest.mock('@dnd-kit/core', () => ({
...jest.requireActual('@dnd-kit/core'),
// Since jsdom (used by jest) does not support getBoundingClientRect function
// which is required for drag-n-drop calculations, we mock closestCorners fn
// from dnd-kit to return collided elements as per the test. This allows us to
// test all drag-n-drop handlers.
closestCorners: jest.fn(),
}));
// eslint-disable-next-line no-promise-executor-return
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const RootWrapper = () => (
<AppProvider store={store}>
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<CourseOutline courseId={courseId} />
</IntlProvider>
</QueryClientProvider>
</AppProvider>
);
describe('<CourseOutline />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
useLocation.mockReturnValue({
pathname: mockPathname,
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, courseOutlineIndexMock);
axiosMock
.onGet(getCourseBestPracticesApiUrl({
courseId, excludeGraded: true, all: true,
}))
.reply(200, courseBestPracticesMock);
axiosMock
.onGet(getCourseLaunchApiUrl({
courseId, gradedOnly: true, validateOras: true, all: true,
}))
.reply(200, courseLaunchMock);
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
});
it('render CourseOutline component correctly', async () => {
const { getByText } = render(<RootWrapper />);
await waitFor(() => {
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
});
});
it('handles course outline fetch api errors', async () => {
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(500, 'some internal error');
const { findByText, queryByRole } = render(<RootWrapper />);
expect(await findByText('"some internal error"')).toBeInTheDocument();
// check errors in store
expect(store.getState().courseOutline.errors).toEqual({
courseLaunchApi: null,
outlineIndexApi: {
data: '"some internal error"',
dismissible: false,
status: 500,
type: 'serverError',
},
reindexApi: null,
sectionLoadingApi: null,
});
expect(queryByRole('button', { name: 'Dismiss' })).not.toBeInTheDocument();
});
it('check reindex and render success alert is correctly', async () => {
const { findByText, findByTestId } = render(<RootWrapper />);
axiosMock
.onGet(getCourseReindexApiUrl(courseOutlineIndexMock.reindexLink))
.reply(200);
const reindexButton = await findByTestId('course-reindex');
fireEvent.click(reindexButton);
expect(await findByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument();
});
it('check video sharing option udpates correctly', async () => {
const { findByLabelText } = render(<RootWrapper />);
axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
metadata: {
video_sharing_options: VIDEO_SHARING_OPTIONS.allOff,
},
})
.reply(200);
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
await act(
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
);
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
metadata: {
video_sharing_options: VIDEO_SHARING_OPTIONS.allOff,
},
}));
});
it('check video sharing option shows error on failure', async () => {
const { findByLabelText, queryByRole } = render(<RootWrapper />);
axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
metadata: {
video_sharing_options: VIDEO_SHARING_OPTIONS.allOff,
},
})
.reply(500);
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
await act(
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
);
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
metadata: {
video_sharing_options: VIDEO_SHARING_OPTIONS.allOff,
},
}));
const alertElement = queryByRole('alert');
expect(alertElement).toHaveTextContent(
pageAlertMessages.alertFailedGeneric.defaultMessage,
);
});
it('render error alert after failed reindex correctly', async () => {
const { findByText, findByTestId } = render(<RootWrapper />);
axiosMock
.onGet(getCourseReindexApiUrl(courseOutlineIndexMock.reindexLink))
.reply(500);
const reindexButton = await findByTestId('course-reindex');
await act(async () => fireEvent.click(reindexButton));
expect(await findByText('Request failed with status code 500')).toBeInTheDocument();
});
it('check that new section list is saved when dragged', async () => {
const { findAllByRole } = render(<RootWrapper />);
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = sectionsDraggers[6];
axiosMock
.onPut(getCourseBlockApiUrl(courseBlockId))
.reply(200, { dummy: 'value' });
const section1 = store.getState().courseOutline.sectionsList[0].id;
closestCorners.mockReturnValue([{ id: section1 }]);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await sleep(1);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await waitFor(async () => {
const saveStatus = store.getState().courseOutline.savingStatus;
expect(saveStatus).toEqual(RequestStatus.SUCCESSFUL);
});
const section2 = store.getState().courseOutline.sectionsList[1].id;
expect(section1).toBe(section2);
});
it('check section list is restored to original order when API call fails', async () => {
const { findAllByRole } = render(<RootWrapper />);
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = sectionsDraggers[6];
axiosMock
.onPut(getCourseBlockApiUrl(courseBlockId))
.reply(500);
const section1 = store.getState().courseOutline.sectionsList[0].id;
closestCorners.mockReturnValue([{ id: section1 }]);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await sleep(1);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await waitFor(async () => {
const saveStatus = store.getState().courseOutline.savingStatus;
expect(saveStatus).toEqual(RequestStatus.FAILED);
});
const section1New = store.getState().courseOutline.sectionsList[0].id;
expect(section1).toBe(section1New);
});
it('adds new section correctly', async () => {
const { findAllByTestId, findByTestId } = render(<RootWrapper />);
let elements = await findAllByTestId('section-card');
window.HTMLElement.prototype.getBoundingClientRect = jest.fn(() => ({
top: 0,
bottom: 4000,
}));
expect(elements.length).toBe(4);
axiosMock
.onPost(getXBlockBaseApiUrl())
.reply(200, {
locator: courseSectionMock.id,
});
axiosMock
.onGet(getXBlockApiUrl(courseSectionMock.id))
.reply(200, courseSectionMock);
const newSectionButton = await findByTestId('new-section-button');
await act(async () => fireEvent.click(newSectionButton));
elements = await findAllByTestId('section-card');
expect(elements.length).toBe(5);
expect(window.HTMLElement.prototype.scrollIntoView).toBeCalled();
});
it('adds new subsection correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const [section] = await findAllByTestId('section-card');
let subsections = await within(section).findAllByTestId('subsection-card');
expect(subsections.length).toBe(2);
window.HTMLElement.prototype.getBoundingClientRect = jest.fn(() => ({
top: 0,
bottom: 4000,
}));
axiosMock
.onPost(getXBlockBaseApiUrl())
.reply(200, {
locator: courseSubsectionMock.id,
});
axiosMock
.onGet(getXBlockApiUrl(courseSubsectionMock.id))
.reply(200, courseSubsectionMock);
const newSubsectionButton = await within(section).findByTestId('new-subsection-button');
await act(async () => {
fireEvent.click(newSubsectionButton);
});
subsections = await within(section).findAllByTestId('subsection-card');
expect(subsections.length).toBe(3);
expect(window.HTMLElement.prototype.scrollIntoView).toBeCalled();
});
it('adds new unit correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const units = await within(subsectionElement).findAllByTestId('unit-card');
expect(units.length).toBe(1);
axiosMock
.onPost(getXBlockBaseApiUrl())
.reply(200, {
locator: 'some',
});
const newUnitButton = await within(subsectionElement).findByTestId('new-unit-button');
await act(async () => fireEvent.click(newUnitButton));
expect(axiosMock.history.post.length).toBe(1);
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [subsection] = section.childInfo.children;
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
parent_locator: subsection.id,
category: COURSE_BLOCK_NAMES.vertical.id,
display_name: COURSE_BLOCK_NAMES.vertical.name,
}));
});
it('render checklist value correctly', async () => {
const { getByText } = render(<RootWrapper />);
await executeThunk(fetchCourseLaunchQuery({
courseId, gradedOnly: true, validateOras: true, all: true,
}), store.dispatch);
await executeThunk(fetchCourseBestPracticesQuery({
courseId, excludeGraded: true, all: true,
}), store.dispatch);
expect(getByText('4/9 completed')).toBeInTheDocument();
});
it('render alerts if checklist api fails', async () => {
axiosMock
.onGet(getCourseLaunchApiUrl({
courseId, gradedOnly: true, validateOras: true, all: true,
}))
.reply(500);
const { findByText, findByRole } = render(<RootWrapper />);
await executeThunk(fetchCourseLaunchQuery({
courseId, gradedOnly: true, validateOras: true, all: true,
}), store.dispatch);
expect(await findByText('Request failed with status code 500')).toBeInTheDocument();
// check errors in store
expect(store.getState().courseOutline.errors).toEqual({
courseLaunchApi: {
data: 'Request failed with status code 500',
type: 'unknown',
dismissible: true,
},
outlineIndexApi: null,
reindexApi: null,
sectionLoadingApi: null,
});
const dismissBtn = await findByRole('button', { name: 'Dismiss' });
fireEvent.click(dismissBtn);
expect(store.getState().courseOutline.errors).toEqual({
courseLaunchApi: null,
outlineIndexApi: null,
reindexApi: null,
sectionLoadingApi: null,
});
});
it('check highlights are enabled after enable highlights query is successful', async () => {
const { findByTestId, findByText } = render(<RootWrapper />);
axiosMock.reset();
axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true,
},
})
.reply(200);
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, {
...courseOutlineIndexMock,
courseStructure: {
...courseOutlineIndexMock.courseStructure,
highlightsEnabledForMessaging: true,
},
});
const enableButton = await findByTestId('highlights-enable-button');
fireEvent.click(enableButton);
const saveButton = await findByText(enableHighlightsModalMessages.submitButton.defaultMessage);
await act(async () => fireEvent.click(saveButton));
expect(await findByTestId('highlights-enabled-span')).toBeInTheDocument();
});
it('should expand and collapse subsections, after click on subheader buttons', async () => {
const { queryAllByTestId, findByText } = render(<RootWrapper />);
const collapseBtn = await findByText(headerMessages.collapseAllButton.defaultMessage);
expect(collapseBtn).toBeInTheDocument();
fireEvent.click(collapseBtn);
const expandBtn = await findByText(headerMessages.expandAllButton.defaultMessage);
expect(expandBtn).toBeInTheDocument();
fireEvent.click(expandBtn);
await waitFor(() => {
const cardSubsections = queryAllByTestId('section-card__subsections');
cardSubsections.forEach(element => expect(element).toBeVisible());
fireEvent.click(collapseBtn);
cardSubsections.forEach(element => expect(element).not.toBeVisible());
});
});
it('render CourseOutline component without sections correctly', async () => {
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, courseOutlineIndexWithoutSections);
const { getByTestId } = render(<RootWrapper />);
await waitFor(() => {
expect(getByTestId('empty-placeholder')).toBeInTheDocument();
});
});
it('render configuration alerts and check dismiss query', async () => {
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, {
...courseOutlineIndexMock,
notificationDismissUrl: '/some/url',
});
const { findByRole } = render(<RootWrapper />);
expect(await findByRole('alert')).toBeInTheDocument();
const dismissBtn = await findByRole('button', { name: 'Dismiss' });
axiosMock
.onDelete('/some/url')
.reply(204);
fireEvent.click(dismissBtn);
expect(axiosMock.history.delete.length).toBe(1);
});
it('check edit title works for section, subsection and unit', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const checkEditTitle = async (section, element, item, newName, elementName) => {
axiosMock.reset();
axiosMock
.onPost(getCourseItemApiUrl(item.id, {
metadata: {
display_name: newName,
},
}))
.reply(200, { dummy: 'value' });
// mock section, subsection and unit name and check within the elements.
// this is done to avoid adding conditions to this mock.
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, {
...section,
display_name: newName,
childInfo: {
children: [
{
...section.childInfo.children[0],
display_name: newName,
childInfo: {
children: [
{
...section.childInfo.children[0].childInfo.children[0],
display_name: newName,
},
],
},
},
],
},
});
const editButton = await within(element).findByTestId(`${elementName}-edit-button`);
fireEvent.click(editButton);
const editField = await within(element).findByTestId(`${elementName}-edit-field`);
fireEvent.change(editField, { target: { value: newName } });
await act(async () => fireEvent.blur(editField));
expect(
axiosMock.history.post[axiosMock.history.post.length - 1].data,
`Failed for ${elementName}!`,
).toBe(JSON.stringify({
metadata: {
display_name: newName,
},
}));
const results = await within(element).findAllByText(newName);
expect(results.length, `Failed for ${elementName}!`).toBeGreaterThan(0);
};
// check section
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
await checkEditTitle(section, sectionElement, section, 'New section name', 'section');
// check subsection
const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection');
// check unit
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit');
});
it('check whether section, subsection and unit is deleted when corresponding delete button is clicked', async () => {
const { findAllByTestId, findByTestId, queryByText } = render(<RootWrapper />);
// get section, subsection and unit
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
const checkDeleteBtn = async (item, element, elementName) => {
await waitFor(() => {
expect(queryByText(item.displayName), `Failed for ${elementName}!`).toBeInTheDocument();
});
axiosMock.onDelete(getCourseItemApiUrl(item.id)).reply(200);
const menu = await within(element).findByTestId(`${elementName}-card-header__menu-button`);
fireEvent.click(menu);
const deleteButton = await within(element).findByTestId(`${elementName}-card-header__menu-delete-button`);
fireEvent.click(deleteButton);
const confirmButton = await findByTestId('delete-confirm-button');
await act(async () => fireEvent.click(confirmButton));
await waitFor(() => {
expect(queryByText(item.displayName), `Failed for ${elementName}!`).not.toBeInTheDocument();
});
};
// delete unit, subsection and then section in order.
// check unit
await checkDeleteBtn(unit, unitElement, 'unit');
// check subsection
await checkDeleteBtn(subsection, subsectionElement, 'subsection');
// check section
await checkDeleteBtn(section, sectionElement, 'section');
});
it('check whether section, subsection and unit is duplicated successfully', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get section, subsection and unit
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
const checkDuplicateBtn = async (item, parentElement, element, elementName, expectedLength) => {
// baseline
if (parentElement) {
expect(
await within(parentElement).findAllByTestId(`${elementName}-card`),
`Failed for ${elementName}!`,
).toHaveLength(expectedLength - 1);
} else {
expect(
await findAllByTestId(`${elementName}-card`),
`Failed for ${elementName}!`,
).toHaveLength(expectedLength - 1);
}
const duplicatedItemId = item.id + elementName;
axiosMock
.onPost(getXBlockBaseApiUrl())
.reply(200, {
locator: duplicatedItemId,
});
if (elementName === 'section') {
section.id = duplicatedItemId;
} else if (elementName === 'subsection') {
section.childInfo.children = [...section.childInfo.children, { ...subsection, id: duplicatedItemId }];
} else if (elementName === 'unit') {
subsection.childInfo.children = [...subsection.childInfo.children, { ...unit, id: duplicatedItemId }];
section.childInfo.children = [subsection, ...section.childInfo.children.slice(1)];
}
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, {
...section,
});
const menu = await within(element).findByTestId(`${elementName}-card-header__menu-button`);
fireEvent.click(menu);
const duplicateButton = await within(element).findByTestId(`${elementName}-card-header__menu-duplicate-button`);
await act(async () => fireEvent.click(duplicateButton));
if (parentElement) {
expect(
await within(parentElement).findAllByTestId(`${elementName}-card`),
`Failed for ${elementName}!`,
).toHaveLength(expectedLength);
} else {
expect(
await findAllByTestId(`${elementName}-card`),
`Failed for ${elementName}!`,
).toHaveLength(expectedLength);
}
};
// duplicate unit, subsection and then section in order.
// check unit
await checkDuplicateBtn(unit, subsectionElement, unitElement, 'unit', 2);
// check subsection
await checkDuplicateBtn(subsection, sectionElement, subsectionElement, 'subsection', 3);
// check section
await checkDuplicateBtn(section, null, sectionElement, 'section', 5);
});
it('check section, subsection & unit is published when publish button is clicked', async () => {
const { findAllByTestId, findByTestId } = render(<RootWrapper />);
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
const checkPublishBtn = async (item, element, elementName) => {
expect(
(await within(element).getAllByRole('status'))[0],
`Failed for ${elementName}!`,
).toHaveTextContent(cardHeaderMessages.statusBadgeDraft.defaultMessage);
axiosMock
.onPost(getCourseItemApiUrl(item.id), {
publish: 'make_public',
})
.reply(200, { dummy: 'value' });
let mockReturnValue = {
...section,
childInfo: {
children: [
{
...section.childInfo.children[0],
published: true,
visibilityState: 'live',
},
...section.childInfo.children.slice(1),
],
},
};
if (elementName === 'unit') {
mockReturnValue = {
...section,
childInfo: {
children: [
{
...section.childInfo.children[0],
childInfo: {
children: [
{
...section.childInfo.children[0].childInfo.children[0],
published: true,
visibilityState: 'live',
},
...section.childInfo.children[0].childInfo.children.slice(1),
],
},
},
...section.childInfo.children.slice(1),
],
},
};
}
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, mockReturnValue);
const menu = await within(element).findByTestId(`${elementName}-card-header__menu-button`);
fireEvent.click(menu);
const publishButton = await within(element).findByTestId(`${elementName}-card-header__menu-publish-button`);
await act(async () => fireEvent.click(publishButton));
const confirmButton = await findByTestId('publish-confirm-button');
await act(async () => fireEvent.click(confirmButton));
expect(
(await within(element).getAllByRole('status'))[0],
`Failed for ${elementName}!`,
).toHaveTextContent(cardHeaderMessages.statusBadgeLive.defaultMessage);
};
// publish unit, subsection and then section in order.
// check unit
await checkPublishBtn(unit, unitElement, 'unit');
// check subsection
await checkPublishBtn(subsection, subsectionElement, 'subsection');
// section doesn't display badges
});
it('check configure modal for section', async () => {
const { findByTestId, findAllByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const newReleaseDateIso = '2025-09-10T22:00:00Z';
const newReleaseDate = '09/10/2025';
axiosMock
.onPost(getCourseItemApiUrl(section.id), {
publish: 'republish',
metadata: {
visible_to_staff_only: true,
start: newReleaseDateIso,
},
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, {
...section,
start: newReleaseDateIso,
});
const [firstSection] = await findAllByTestId('section-card');
const sectionDropdownButton = await within(firstSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(sectionDropdownButton));
const configureBtn = await within(firstSection).findByTestId('section-card-header__menu-configure-button');
await act(async () => fireEvent.click(configureBtn));
let releaseDateStack = await findByTestId('release-date-stack');
let releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
expect(releaseDatePicker).toHaveValue('08/10/2023');
await act(async () => fireEvent.change(releaseDatePicker, { target: { value: newReleaseDate } }));
expect(releaseDatePicker).toHaveValue(newReleaseDate);
const saveButton = await findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
publish: 'republish',
metadata: {
visible_to_staff_only: true,
start: newReleaseDateIso,
},
}));
await act(async () => fireEvent.click(sectionDropdownButton));
await act(async () => fireEvent.click(configureBtn));
releaseDateStack = await findByTestId('release-date-stack');
releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
expect(releaseDatePicker).toHaveValue(newReleaseDate);
});
it('check configure modal for subsection', async () => {
const {
findAllByTestId,
findByTestId,
} = render(<RootWrapper />);
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
const [subsection] = section.childInfo.children;
const expectedRequestData = {
publish: 'republish',
graderType: 'Homework',
isPrereq: false,
prereqMinScore: 100,
prereqMinCompletion: 100,
metadata: {
visible_to_staff_only: null,
due: '2025-09-10T05:00:00Z',
hide_after_due: true,
show_correctness: 'always',
is_practice_exam: false,
is_time_limited: true,
is_proctored_enabled: false,
exam_review_rules: '',
default_time_limit_minutes: 3270,
is_onboarding_exam: false,
start: '2025-08-10T00:00:00Z',
},
};
axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });
const [currentSection] = await findAllByTestId('section-card');
const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
subsection.start = expectedRequestData.metadata.start;
subsection.due = expectedRequestData.metadata.due;
subsection.format = expectedRequestData.graderType;
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
subsection.hideAfterDue = expectedRequestData.metadata.hideAfterDue;
section.childInfo.children[0] = subsection;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
fireEvent.click(subsectionDropdownButton);
const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button');
fireEvent.click(configureBtn);
// update fields
let configureModal = await findByTestId('configure-modal');
expect(await within(configureModal).findByText(expectedRequestData.graderType)).toBeInTheDocument();
let releaseDateStack = await within(configureModal).findByTestId('release-date-stack');
let releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
fireEvent.change(releaseDatePicker, { target: { value: '08/10/2025' } });
let releaseDateTimePicker = await within(releaseDateStack).findByPlaceholderText('HH:MM');
fireEvent.change(releaseDateTimePicker, { target: { value: '00:00' } });
let dueDateStack = await within(configureModal).findByTestId('due-date-stack');
let dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY');
fireEvent.change(dueDatePicker, { target: { value: '09/10/2025' } });
let dueDateTimePicker = await within(dueDateStack).findByPlaceholderText('HH:MM');
fireEvent.change(dueDateTimePicker, { target: { value: '05:00' } });
let graderTypeDropdown = await within(configureModal).findByTestId('grader-type-select');
fireEvent.change(graderTypeDropdown, { target: { value: expectedRequestData.graderType } });
// visibility tab
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
fireEvent.click(visibilityTab);
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(visibilityRadioButtons[1]);
let advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
fireEvent.click(advancedTab);
let radioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(radioButtons[1]);
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
fireEvent.change(hours, { target: { value: '54:30' } });
const saveButton = await within(configureModal).findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
// verify request
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
// reopen modal and check values
await act(async () => fireEvent.click(subsectionDropdownButton));
await act(async () => fireEvent.click(configureBtn));
configureModal = await findByTestId('configure-modal');
releaseDateStack = await within(configureModal).findByTestId('release-date-stack');
releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
expect(releaseDatePicker).toHaveValue('08/10/2025');
releaseDateTimePicker = await within(releaseDateStack).findByPlaceholderText('HH:MM');
expect(releaseDateTimePicker).toHaveValue('00:00');
dueDateStack = await await within(configureModal).findByTestId('due-date-stack');
dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY');
expect(dueDatePicker).toHaveValue('09/10/2025');
dueDateTimePicker = await within(dueDateStack).findByPlaceholderText('HH:MM');
expect(dueDateTimePicker).toHaveValue('05:00');
graderTypeDropdown = await within(configureModal).findByTestId('grader-type-select');
expect(graderTypeDropdown).toHaveValue(expectedRequestData.graderType);
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
fireEvent.click(advancedTab);
radioButtons = await within(configureModal).findAllByRole('radio');
expect(radioButtons[0]).toHaveProperty('checked', false);
expect(radioButtons[1]).toHaveProperty('checked', true);
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
expect(hours).toHaveValue('54:30');
});
it('check prereq and proctoring settings in configure modal for subsection', async () => {
const {
findAllByTestId,
findByTestId,
} = render(<RootWrapper />);
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
const [subsection, secondSubsection] = section.childInfo.children;
const expectedRequestData = {
publish: 'republish',
graderType: 'notgraded',
isPrereq: true,
prereqUsageKey: secondSubsection.id,
prereqMinScore: 80,
prereqMinCompletion: 90,
metadata: {
visible_to_staff_only: true,
due: '',
hide_after_due: false,
show_correctness: 'always',
is_practice_exam: false,
is_time_limited: true,
is_proctored_enabled: true,
exam_review_rules: 'some rules for proctored exams',
default_time_limit_minutes: 30,
is_onboarding_exam: false,
start: '1970-01-01T05:00:00Z',
},
};
axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });
const [currentSection] = await findAllByTestId('section-card');
const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
subsection.isPrereq = expectedRequestData.isPrereq;
subsection.prereq = expectedRequestData.prereqUsageKey;
subsection.prereqMinScore = expectedRequestData.prereqMinScore;
subsection.prereqMinCompletion = expectedRequestData.prereqMinCompletion;
section.childInfo.children[0] = subsection;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
fireEvent.click(subsectionDropdownButton);
const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button');
fireEvent.click(configureBtn);
// update fields
let configureModal = await findByTestId('configure-modal');
let advancedTab = await within(configureModal).findByRole(
'tab',
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
);
// visibility tab
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
fireEvent.click(visibilityTab);
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(visibilityRadioButtons[2]);
fireEvent.click(advancedTab);
let radioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(radioButtons[2]);
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
fireEvent.change(hours, { target: { value: '00:30' } });
// select a prerequisite
const prereqSelect = await within(configureModal).findByRole('combobox');
fireEvent.change(prereqSelect, { target: { value: expectedRequestData.prereqUsageKey } });
// update minimum score and completion percentage
let prereqMinScoreInput = await within(configureModal).findByLabelText(
configureModalMessages.minScoreLabel.defaultMessage,
);
fireEvent.change(prereqMinScoreInput, { target: { value: expectedRequestData.prereqMinScore } });
let prereqMinCompletionInput = await within(configureModal).findByLabelText(
configureModalMessages.minCompletionLabel.defaultMessage,
);
fireEvent.change(prereqMinCompletionInput, { target: { value: expectedRequestData.prereqMinCompletion } });
// enable this subsection to be used as prerequisite by other subsections
let prereqCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.prereqCheckboxLabel.defaultMessage,
);
fireEvent.click(prereqCheckbox);
// fill some rules for proctored exams
let examsRulesInput = await within(configureModal).findByLabelText(
configureModalMessages.reviewRulesLabel.defaultMessage,
);
fireEvent.change(examsRulesInput, { target: { value: expectedRequestData.metadata.exam_review_rules } });
const saveButton = await within(configureModal).findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
// verify request
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
// reopen modal and check values
await act(async () => fireEvent.click(subsectionDropdownButton));
await act(async () => fireEvent.click(configureBtn));
configureModal = await findByTestId('configure-modal');
advancedTab = await within(configureModal).findByRole('tab', {
name: configureModalMessages.advancedTabTitle.defaultMessage,
});
fireEvent.click(advancedTab);
radioButtons = await within(configureModal).findAllByRole('radio');
expect(radioButtons[0]).toHaveProperty('checked', false);
expect(radioButtons[1]).toHaveProperty('checked', false);
expect(radioButtons[2]).toHaveProperty('checked', true);
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
expect(hours).toHaveValue('00:30');
prereqCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.prereqCheckboxLabel.defaultMessage,
);
expect(prereqCheckbox).toBeChecked();
const prereqSelectOption = await within(configureModal).findByRole('option', { selected: true });
expect(prereqSelectOption).toHaveAttribute('value', expectedRequestData.prereqUsageKey);
examsRulesInput = await within(configureModal).findByLabelText(
configureModalMessages.reviewRulesLabel.defaultMessage,
);
expect(examsRulesInput).toHaveTextContent(expectedRequestData.metadata.exam_review_rules);
prereqMinScoreInput = await within(configureModal).findByLabelText(
configureModalMessages.minScoreLabel.defaultMessage,
);
expect(prereqMinScoreInput).toHaveAttribute('value', `${expectedRequestData.prereqMinScore}`);
prereqMinCompletionInput = await within(configureModal).findByLabelText(
configureModalMessages.minCompletionLabel.defaultMessage,
);
expect(prereqMinCompletionInput).toHaveAttribute('value', `${expectedRequestData.prereqMinCompletion}`);
});
it('check practice proctoring settings in configure modal', async () => {
const {
findAllByTestId,
findByTestId,
} = render(<RootWrapper />);
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
const [subsection] = section.childInfo.children;
const expectedRequestData = {
publish: 'republish',
graderType: 'notgraded',
isPrereq: false,
prereqMinScore: 100,
prereqMinCompletion: 100,
metadata: {
visible_to_staff_only: null,
due: '',
hide_after_due: false,
show_correctness: 'never',
is_practice_exam: true,
is_time_limited: true,
is_proctored_enabled: true,
exam_review_rules: '',
default_time_limit_minutes: 30,
is_onboarding_exam: false,
start: '1970-01-01T05:00:00Z',
},
};
axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });
const [currentSection] = await findAllByTestId('section-card');
const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
section.childInfo.children[0] = subsection;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
fireEvent.click(subsectionDropdownButton);
const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button');
fireEvent.click(configureBtn);
// update fields
let configureModal = await findByTestId('configure-modal');
let advancedTab = await within(configureModal).findByRole(
'tab',
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
);
// visibility tab
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
fireEvent.click(visibilityTab);
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(visibilityRadioButtons[4]);
// advancedTab
fireEvent.click(advancedTab);
let radioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(radioButtons[3]);
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
fireEvent.change(hours, { target: { value: '00:30' } });
// rules box should not be visible
expect(within(configureModal).queryByLabelText(
configureModalMessages.reviewRulesLabel.defaultMessage,
)).not.toBeInTheDocument();
const saveButton = await within(configureModal).findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
// verify request
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
// reopen modal and check values
await act(async () => fireEvent.click(subsectionDropdownButton));
await act(async () => fireEvent.click(configureBtn));
configureModal = await findByTestId('configure-modal');
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
fireEvent.click(advancedTab);
radioButtons = await within(configureModal).findAllByRole('radio');
expect(radioButtons[0]).toHaveProperty('checked', false);
expect(radioButtons[1]).toHaveProperty('checked', false);
expect(radioButtons[2]).toHaveProperty('checked', false);
expect(radioButtons[3]).toHaveProperty('checked', true);
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
expect(hours).toHaveValue('00:30');
});
it('check onboarding proctoring settings in configure modal', async () => {
const {
findAllByTestId,
findByTestId,
} = render(<RootWrapper />);
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
const [, subsection] = section.childInfo.children;
const expectedRequestData = {
publish: 'republish',
graderType: 'notgraded',
isPrereq: true,
prereqMinScore: 100,
prereqMinCompletion: 100,
metadata: {
visible_to_staff_only: null,
due: '',
hide_after_due: false,
show_correctness: 'past_due',
is_practice_exam: false,
is_time_limited: true,
is_proctored_enabled: true,
exam_review_rules: '',
default_time_limit_minutes: 30,
is_onboarding_exam: true,
start: '2013-02-05T05:00:00Z',
},
};
axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });
const [currentSection] = await findAllByTestId('section-card');
const [, secondSubsection] = await within(currentSection).findAllByTestId('subsection-card');
const subsectionDropdownButton = await within(secondSubsection).findByTestId('subsection-card-header__menu-button');
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
section.childInfo.children[1] = subsection;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
fireEvent.click(subsectionDropdownButton);
const configureBtn = await within(secondSubsection).findByTestId('subsection-card-header__menu-configure-button');
fireEvent.click(configureBtn);
// update fields
let configureModal = await findByTestId('configure-modal');
// visibility tab
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
fireEvent.click(visibilityTab);
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(visibilityRadioButtons[5]);
// advancedTab
let advancedTab = await within(configureModal).findByRole(
'tab',
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
);
fireEvent.click(advancedTab);
let radioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(radioButtons[3]);
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
fireEvent.change(hours, { target: { value: '00:30' } });
// rules box should not be visible
expect(within(configureModal).queryByLabelText(
configureModalMessages.reviewRulesLabel.defaultMessage,
)).not.toBeInTheDocument();
const saveButton = await within(configureModal).findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
// verify request
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
// reopen modal and check values
await act(async () => fireEvent.click(subsectionDropdownButton));
await act(async () => fireEvent.click(configureBtn));
configureModal = await findByTestId('configure-modal');
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
fireEvent.click(advancedTab);
radioButtons = await within(configureModal).findAllByRole('radio');
expect(radioButtons[0]).toHaveProperty('checked', false);
expect(radioButtons[1]).toHaveProperty('checked', false);
expect(radioButtons[2]).toHaveProperty('checked', false);
expect(radioButtons[3]).toHaveProperty('checked', true);
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
expect(hours).toHaveValue('00:30');
});
it('check no special exam setting in configure modal', async () => {
const {
findAllByTestId,
findByTestId,
} = render(<RootWrapper />);
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[1]);
const [subsection] = section.childInfo.children;
const expectedRequestData = {
publish: 'republish',
graderType: 'notgraded',
prereqMinScore: 100,
prereqMinCompletion: 100,
metadata: {
visible_to_staff_only: null,
due: '',
hide_after_due: false,
show_correctness: 'always',
is_practice_exam: false,
is_time_limited: false,
is_proctored_enabled: false,
exam_review_rules: '',
default_time_limit_minutes: 0,
is_onboarding_exam: false,
start: '1970-01-01T05:00:00Z',
},
};
axiosMock
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
.reply(200, { dummy: 'value' });
const [, currentSection] = await findAllByTestId('section-card');
const [subsectionElement] = await within(currentSection).findAllByTestId('subsection-card');
const subsectionDropdownButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-button');
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
section.childInfo.children[0] = subsection;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
fireEvent.click(subsectionDropdownButton);
const configureBtn = await within(subsectionElement).findByTestId('subsection-card-header__menu-configure-button');
fireEvent.click(configureBtn);
// update fields
let configureModal = await findByTestId('configure-modal');
// advancedTab
let advancedTab = await within(configureModal).findByRole(
'tab',
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
);
fireEvent.click(advancedTab);
let radioButtons = await within(configureModal).findAllByRole('radio');
fireEvent.click(radioButtons[0]);
// time box should not be visible
expect(within(configureModal).queryByLabelText(
configureModalMessages.timeAllotted.defaultMessage,
)).not.toBeInTheDocument();
// rules box should not be visible
expect(within(configureModal).queryByLabelText(
configureModalMessages.reviewRulesLabel.defaultMessage,
)).not.toBeInTheDocument();
const saveButton = await within(configureModal).findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
// verify request
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
// reopen modal and check values
await act(async () => fireEvent.click(subsectionDropdownButton));
await act(async () => fireEvent.click(configureBtn));
configureModal = await findByTestId('configure-modal');
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
fireEvent.click(advancedTab);
radioButtons = await within(configureModal).findAllByRole('radio');
expect(radioButtons[0]).toHaveProperty('checked', true);
expect(radioButtons[1]).toHaveProperty('checked', false);
expect(radioButtons[2]).toHaveProperty('checked', false);
expect(radioButtons[3]).toHaveProperty('checked', false);
});
it('check configure modal for unit', async () => {
const { findAllByTestId, findByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const [subsection] = section.childInfo.children;
const [unit] = subsection.childInfo.children;
// Enrollment Track Groups : Audit
const newGroupAccess = { 50: [1] };
const isVisibleToStaffOnly = true;
axiosMock
.onPost(getCourseItemApiUrl(unit.id), {
publish: 'republish',
metadata: {
visible_to_staff_only: isVisibleToStaffOnly,
discussion_enabled: false,
group_access: newGroupAccess,
},
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
const [firstSection] = await findAllByTestId('section-card');
const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card');
const subsectionExpandButton = await within(firstSubsection).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(subsectionExpandButton);
const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card');
const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button');
// after configuraiton response
unit.visibilityState = 'staff_only';
unit.discussion_enabled = false;
unit.userPartitionInfo = {
selectablePartitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: true,
deleted: false,
},
],
},
],
selectedPartitionIndex: 0,
selectedGroupsLabel: '',
};
subsection.childInfo.children[0] = unit;
section.childInfo.children[0] = subsection;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
fireEvent.click(unitDropdownButton);
const configureBtn = await within(firstUnit).getByTestId('unit-card-header__menu-configure-button');
fireEvent.click(configureBtn);
let configureModal = await findByTestId('configure-modal');
expect(await within(configureModal).findByText(
configureModalMessages.unitVisibility.defaultMessage,
)).toBeInTheDocument();
let visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
await act(async () => fireEvent.click(visibilityCheckbox));
let discussionCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
);
expect(discussionCheckbox).toBeChecked();
await act(async () => fireEvent.click(discussionCheckbox));
let groupeType = await within(configureModal).findByTestId('group-type-select');
fireEvent.change(groupeType, { target: { value: '0' } });
let checkboxes = await within(await within(configureModal).findByTestId('group-checkboxes')).findAllByRole('checkbox');
fireEvent.click(checkboxes[1]);
const saveButton = await within(configureModal).findByTestId('configure-save-button');
await act(async () => fireEvent.click(saveButton));
// reopen modal and check values
await act(async () => fireEvent.click(unitDropdownButton));
await act(async () => fireEvent.click(configureBtn));
configureModal = await findByTestId('configure-modal');
visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
expect(visibilityCheckbox).toBeChecked();
discussionCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
);
expect(discussionCheckbox).not.toBeChecked();
groupeType = await within(configureModal).findByTestId('group-type-select');
expect(groupeType).toHaveValue('0');
checkboxes = await within(await within(configureModal).findByTestId('group-checkboxes')).findAllByRole('checkbox');
expect(checkboxes[0]).not.toBeChecked();
expect(checkboxes[1]).toBeChecked();
});
it('check update highlights when update highlights query is successfully', async () => {
const { getByRole } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const highlights = [
'New Highlight 1',
'New Highlight 2',
'New Highlight 3',
'New Highlight 4',
'New Highlight 5',
];
axiosMock
.onPost(getCourseItemApiUrl(section.id), {
publish: 'republish',
metadata: {
highlights,
},
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, {
...section,
highlights,
});
await executeThunk(updateCourseSectionHighlightsQuery(section.id, highlights), store.dispatch);
await waitFor(() => {
expect(getByRole('button', { name: '5 Section highlights' })).toBeInTheDocument();
});
});
it('check whether section move up and down options work correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section element
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
const [, secondSection] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
// mock api call
axiosMock
.onPut(getCourseBlockApiUrl(courseBlockId))
.reply(200, { dummy: 'value' });
// find menu button and click on it to open menu
const menu = await within(sectionElement).findByTestId('section-card-header__menu-button');
fireEvent.click(menu);
// move second section to first position to test move up option
const moveUpButton = await within(sectionElement).findByTestId('section-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSectionId = store.getState().courseOutline.sectionsList[0].id;
expect(secondSection.id).toBe(firstSectionId);
// move first section back to second position to test move down option
const moveDownButton = await within(sectionElement).findByTestId('section-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const newSecondSectionId = store.getState().courseOutline.sectionsList[1].id;
expect(secondSection.id).toBe(newSecondSectionId);
});
it('check whether section move up & down option is rendered correctly based on index', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get first, second and last section element
const {
0: firstSection, 1: secondSection, length, [length - 1]: lastSection,
} = await findAllByTestId('section-card');
// find menu button and click on it to open menu in first section
const firstMenu = await within(firstSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(firstMenu));
// move down option should be enabled in first element
expect(
await within(firstSection).findByTestId('section-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
// move up option should not be enabled in first element
expect(
await within(firstSection).findByTestId('section-card-header__menu-move-up-button'),
).toHaveAttribute('aria-disabled', 'true');
// find menu button and click on it to open menu in second section
const secondMenu = await within(secondSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(secondMenu));
// both move down & up option should be enabled in second element
expect(
await within(secondSection).findByTestId('section-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
expect(
await within(secondSection).findByTestId('section-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
// find menu button and click on it to open menu in last section
const lastMenu = await within(lastSection).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(lastMenu));
// move down option should not be enabled in last element
expect(
await within(lastSection).findByTestId('section-card-header__menu-move-down-button'),
).toHaveAttribute('aria-disabled', 'true');
// move up option should be enabled in last element
expect(
await within(lastSection).findByTestId('section-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
});
it('check whether subsection move up and down options work correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section element
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [, secondSubsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(store.getState().courseOutline.sectionsList[0].id))
.reply(200, { dummy: 'value' });
const expectedSection = moveSubsection([
...courseOutlineIndexMock.courseStructure.childInfo.children,
], 0, 0, 1)[0][0];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSection);
// find menu button and click on it to open menu
const menu = await within(subsectionElement).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move second subsection to first position to test move up option
const moveUpButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSubsectionId = store.getState().courseOutline.sectionsList[0].childInfo.children[0].id;
expect(secondSubsection.id).toBe(firstSubsectionId);
// move first section back to second position to test move down option
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
const moveDownButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const secondSubsectionId = store.getState().courseOutline.sectionsList[0].childInfo.children[1].id;
expect(secondSubsection.id).toBe(secondSubsectionId);
});
it('check whether subsection move up to prev section if it is on top of its parent section', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const [firstSection, section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(firstSection.id))
.reply(200, { dummy: 'value' });
const expectedSections = moveSubsectionOver([
...courseOutlineIndexMock.courseStructure.childInfo.children,
], 1, 0, 0, firstSection.childInfo.children.length + 1)[0];
axiosMock
.onGet(getXBlockApiUrl(firstSection.id))
.reply(200, expectedSections[0]);
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSections[1]);
// find menu button and click on it to open menu
const menu = await within(subsectionElement).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first subsection in second section to last position of prev section
const moveUpButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSectionSubsections = store.getState().courseOutline.sectionsList[0].childInfo.children;
expect(firstSectionSubsections.length).toBe(firstSection.childInfo.children.length + 1);
const lastSubsectionFirstSection = firstSectionSubsections[firstSectionSubsections.length - 1].id;
expect(subsection.id).toBe(lastSubsectionFirstSection);
const subsectionsSecondSection = store.getState().courseOutline.sectionsList[1].childInfo.children;
expect(subsectionsSecondSection.length).toBe(section.childInfo.children.length - 1);
});
it('check whether subsection move down to next section if it is in bottom position of its parent section', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const [section, secondSection] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const lastSubsectionIdx = section.childInfo.children.length - 1;
const subsection = section.childInfo.children[lastSubsectionIdx];
const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubsectionIdx];
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(secondSection.id))
.reply(200, { dummy: 'value' });
const expectedSections = moveSubsectionOver([
...courseOutlineIndexMock.courseStructure.childInfo.children,
], 0, lastSubsectionIdx, 1, 0)[0];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSections[0]);
axiosMock
.onGet(getXBlockApiUrl(secondSection.id))
.reply(200, expectedSections[1]);
// find menu button and click on it to open menu
const menu = await within(subsectionElement).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first subsection in second section to last position of prev section
const moveDownBtn = await within(subsectionElement).findByTestId('subsection-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownBtn));
const firstSectionSubsections = store.getState().courseOutline.sectionsList[0].childInfo.children;
expect(firstSectionSubsections.length).toBe(section.childInfo.children.length - 1);
const subsectionsSecondSection = store.getState().courseOutline.sectionsList[1].childInfo.children;
expect(subsectionsSecondSection.length).toBe(secondSection.childInfo.children.length + 1);
const firstSubSecondSection = subsectionsSecondSection[0].id;
expect(subsection.id).toBe(firstSubSecondSection);
});
it('check whether subsection move up & down option is rendered correctly based on index', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// using first section
const sectionElements = await findAllByTestId('section-card');
const firstSectionElement = sectionElements[0];
// get first, second and last subsection element
const [
firstSubsection,
secondSubsection,
] = await within(firstSectionElement).findAllByTestId('subsection-card');
// find menu button and click on it to open menu in first section
const firstMenu = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(firstMenu));
// move down option should be enabled in first element
expect(
await within(firstSubsection).findByTestId('subsection-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
// move up option should not be enabled in first element
expect(
await within(firstSubsection).findByTestId('subsection-card-header__menu-move-up-button'),
).toHaveAttribute('aria-disabled', 'true');
// find menu button and click on it to open menu in second section
const secondMenu = await within(secondSubsection).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(secondMenu));
// both move down & up option should be enabled in second element
expect(
await within(secondSubsection).findByTestId('subsection-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
expect(
await within(secondSubsection).findByTestId('subsection-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
const lastSectionElement = sectionElements[sectionElements.length - 1];
// get first, second and last subsection element
const [lastSubsection] = await within(lastSectionElement).findAllByTestId('subsection-card');
// find menu button and click on it to open menu in last section
const lastMenu = await within(lastSubsection).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(lastMenu));
// move down option should not be enabled in last subsection of last section element
expect(
await within(lastSubsection).findByTestId('subsection-card-header__menu-move-down-button'),
).toHaveAttribute('aria-disabled', 'true');
// move up option should be enabled in last element
expect(
await within(lastSubsection).findByTestId('subsection-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
});
it('check whether unit move up and down options work correctly', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section -> second subsection -> second unit element
const [, section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const [, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [, secondUnit] = subsection.childInfo.children;
const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(store.getState().courseOutline.sectionsList[1].childInfo.children[1].id))
.reply(200, { dummy: 'value' });
const expectedSection = moveUnit([...courseOutlineIndexMock.courseStructure.childInfo.children], 1, 1, 0, 1)[0][1];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSection);
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move second unit to first position to test move up option
const moveUpButton = await within(unitElement).findByTestId('unit-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstUnitId = store.getState().courseOutline.sectionsList[1].childInfo.children[1].childInfo.children[0].id;
expect(secondUnit.id).toBe(firstUnitId);
// move first unit back to second position to test move down option
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, section);
const moveDownButton = await within(subsectionElement).findByTestId('unit-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const secondUnitId = store.getState().courseOutline.sectionsList[1].childInfo.children[1].childInfo.children[1].id;
expect(secondUnit.id).toBe(secondUnitId);
});
it('check whether unit moves up to previous subsection if it is in top position in parent subsection', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section -> second subsection -> first unit element
const [, section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const [firstSubsection, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(firstSubsection.id))
.reply(200, { dummy: 'value' });
const expectedSections = moveUnitOver([
...courseOutlineIndexMock.courseStructure.childInfo.children,
], 1, 1, 0, 1, 0, firstSubsection.childInfo.children.length)[0];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSections[1]);
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first unit to last position of prev subsection
const moveUpButton = await within(unitElement).findByTestId('unit-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSubUnits = store.getState().courseOutline.sectionsList[1].childInfo.children[0].childInfo.children;
expect(firstSubUnits[firstSubUnits.length - 1].id).toBe(unit.id);
const secondSubUnits = store.getState().courseOutline.sectionsList[1].childInfo.children[1].childInfo.children;
expect(secondSubUnits.length).toBe(subsection.childInfo.children.length - 1);
});
it('check whether unit moves up to previous subsection of prev section if it is in top position in parent subsection & section', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section -> second subsection -> first unit element
const [firstSection, secondSection] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const [subsection] = secondSection.childInfo.children;
const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1];
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(firstSectionLastSubsection.id))
.reply(200, { dummy: 'value' });
const expectedSections = moveUnitOver(
[...courseOutlineIndexMock.courseStructure.childInfo.children],
1,
0,
0,
0,
firstSection.childInfo.children.length - 1,
firstSectionLastSubsection.childInfo.children.length,
)[0];
axiosMock
.onGet(getXBlockApiUrl(firstSection.id))
.reply(200, expectedSections[0]);
axiosMock
.onGet(getXBlockApiUrl(secondSection.id))
.reply(200, expectedSections[1]);
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first unit to last position of prev subsection
const moveUpButton = await within(unitElement).findByTestId('unit-card-header__menu-move-up-button');
await act(async () => fireEvent.click(moveUpButton));
const firstSectionSubStore = store.getState().courseOutline.sectionsList[0].childInfo.children;
const firstSectionLastSubUnits = firstSectionSubStore[firstSectionSubStore.length - 1].childInfo.children;
expect(firstSectionLastSubUnits[firstSectionLastSubUnits.length - 1].id).toBe(unit.id);
const secondSubUnits = store.getState().courseOutline.sectionsList[1].childInfo.children[0].childInfo.children;
expect(secondSubUnits.length).toBe(subsection.childInfo.children.length - 1);
});
it('check whether unit moves down to next subsection if it is in last position in parent subsection', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section -> second subsection -> first unit element
const [, section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const [firstSubsection, subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const lastUnitIdx = firstSubsection.childInfo.children.length - 1;
const unit = firstSubsection.childInfo.children[lastUnitIdx];
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(subsection.id))
.reply(200, { dummy: 'value' });
const expectedSections = moveUnitOver([
...courseOutlineIndexMock.courseStructure.childInfo.children,
], 1, 0, lastUnitIdx, 1, 1, 0)[0];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSections[1]);
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first unit to last position of prev subsection
const moveDownButton = await within(unitElement).findByTestId('unit-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const firstSubUnits = store.getState().courseOutline.sectionsList[1].childInfo.children[0].childInfo.children;
expect(firstSubUnits.length).toBe(firstSubsection.childInfo.children.length - 1);
const secondSubUnits = store.getState().courseOutline.sectionsList[1].childInfo.children[1].childInfo.children;
expect(secondSubUnits[0].id).toBe(unit.id);
});
it('check whether unit moves down to next subsection of next section if it is in last position in parent subsection & section', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get second section -> second subsection -> first unit element
const [, secondSection, thirdSection] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [, sectionElement] = await findAllByTestId('section-card');
const lastSubIndex = secondSection.childInfo.children.length - 1;
const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex];
const thirdSectionFirstSubsection = thirdSection.childInfo.children[0];
const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex];
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1;
const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx];
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
// mock api call
axiosMock
.onPut(getCourseItemApiUrl(thirdSectionFirstSubsection.id))
.reply(200, { dummy: 'value' });
const expectedSections = moveUnitOver(
[...courseOutlineIndexMock.courseStructure.childInfo.children],
1,
lastSubIndex,
lastUnitIdx,
2,
0,
0,
)[0];
axiosMock
.onGet(getXBlockApiUrl(secondSection.id))
.reply(200, expectedSections[1]);
axiosMock
.onGet(getXBlockApiUrl(thirdSection.id))
.reply(200, expectedSections[2]);
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first unit to last position of prev subsection
const moveDownButton = await within(unitElement).findByTestId('unit-card-header__menu-move-down-button');
await act(async () => fireEvent.click(moveDownButton));
const secondSectionSubStore = store.getState().courseOutline.sectionsList[1].childInfo.children;
const secondSectionLastSubUnits = secondSectionSubStore[secondSectionSubStore.length - 1].childInfo.children;
expect(secondSectionLastSubUnits.length).toBe(secondSectionLastSubsection.childInfo.children.length - 1);
const thirdSubUnits = store.getState().courseOutline.sectionsList[2].childInfo.children[0].childInfo.children;
expect(thirdSubUnits[0].id).toBe(unit.id);
});
it('check whether unit move up & down option is rendered correctly based on index', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// using first section -> first subsection -> first unit
const sections = await findAllByTestId('section-card');
const [sectionElement] = sections;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
// get first and only unit in the subsection
const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card');
// find menu button and click on it to open menu in first section
const firstMenu = await within(firstUnit).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(firstMenu));
// move down option should be enabled in first element as it can move down to next subsection
expect(
await within(firstUnit).findByTestId('unit-card-header__menu-move-down-button'),
).not.toHaveAttribute('aria-disabled');
// move up option should not be enabled in first element as we have no subsections or sections above
expect(
await within(firstUnit).findByTestId('unit-card-header__menu-move-up-button'),
).toHaveAttribute('aria-disabled', 'true');
// using last section -> last subsection -> last unit
const lastSection = sections[sections.length - 1];
// it has only one subsection
const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card');
const lastExpandBtn = await within(lastSubsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(lastExpandBtn));
// get last and the only unit in the subsection
const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card');
// find menu button and click on it to open menu in first section
const lastMenu = await within(lastUnit).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(lastMenu));
// move down option should not be enabled in last element as we have no subsections or sections below
expect(
await within(lastUnit).findByTestId('unit-card-header__menu-move-down-button'),
).toHaveAttribute('aria-disabled', 'true');
// move down option should be enabled in last element as it can move up to prev section's last subsection
expect(
await within(lastUnit).findByTestId('unit-card-header__menu-move-up-button'),
).not.toHaveAttribute('aria-disabled');
});
it('check that new subsection list is saved when dragged', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card');
const [section] = store.getState().courseOutline.sectionsList;
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = subsectionsDraggers[1];
const subsection1 = section.childInfo.children[0].id;
closestCorners.mockReturnValue([{ id: subsection1 }]);
axiosMock
.onPut(getCourseItemApiUrl(section.id))
.reply(200, { dummy: 'value' });
const expectedSection = moveSubsection([
...courseOutlineIndexMock.courseStructure.childInfo.children,
], 0, 1, 0)[0][0];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSection);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await sleep(1);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await waitFor(async () => {
const saveStatus = store.getState().courseOutline.savingStatus;
expect(saveStatus).toEqual(RequestStatus.SUCCESSFUL);
});
const subsection2 = store.getState().courseOutline.sectionsList[0].childInfo.children[1].id;
expect(subsection1).toBe(subsection2);
});
it('check that new subsection list is restored to original order when API call fails', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card');
const [section] = store.getState().courseOutline.sectionsList;
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = subsectionsDraggers[1];
const subsection1 = section.childInfo.children[0].id;
closestCorners.mockReturnValue([{ id: subsection1 }]);
axiosMock
.onPut(getCourseItemApiUrl(section.id))
.reply(500);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await sleep(1);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await waitFor(async () => {
const saveStatus = store.getState().courseOutline.savingStatus;
expect(saveStatus).toEqual(RequestStatus.FAILED);
});
const subsection1New = store.getState().courseOutline.sectionsList[0].childInfo.children[0].id;
expect(subsection1).toBe(subsection1New);
});
it('check that new unit list is saved when dragged', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get third section
const [, , sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const section = store.getState().courseOutline.sectionsList[2];
const [subsection] = section.childInfo.children;
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = unitDraggers[1];
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
const unit1 = subsection.childInfo.children[0].id;
closestCorners.mockReturnValue([{ id: unit1 }]);
axiosMock
.onPut(getCourseItemApiUrl(subsection.id))
.reply(200, { dummy: 'value' });
const expectedSection = moveUnit([...sections], 2, 0, 1, 0)[0][2];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSection);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await sleep(1);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await waitFor(async () => {
const saveStatus = store.getState().courseOutline.savingStatus;
expect(saveStatus).toEqual(RequestStatus.SUCCESSFUL);
});
const unit2 = store.getState().courseOutline.sectionsList[2].childInfo.children[0].childInfo.children[1].id;
expect(unit1).toBe(unit2);
});
it('check that new unit list is restored to original order when API call fails', async () => {
const { findAllByTestId } = render(<RootWrapper />);
// get third section
const [, , sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const section = store.getState().courseOutline.sectionsList[2];
const [subsection] = section.childInfo.children;
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = unitDraggers[1];
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
const unit1 = subsection.childInfo.children[0].id;
closestCorners.mockReturnValue([{ id: unit1 }]);
axiosMock
.onPut(getCourseItemApiUrl(subsection.id))
.reply(500);
const expectedSection = moveUnit([...sections], 2, 0, 1, 0)[0][2];
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, expectedSection);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await sleep(1);
fireEvent.keyDown(draggableButton, { code: 'Space' });
await waitFor(async () => {
const saveStatus = store.getState().courseOutline.savingStatus;
expect(saveStatus).toEqual(RequestStatus.FAILED);
});
const unit1New = store.getState().courseOutline.sectionsList[2].childInfo.children[0].childInfo.children[0].id;
expect(unit1).toBe(unit1New);
});
it('check whether unit copy & paste option works correctly', async () => {
const { findAllByTestId, queryByTestId, findAllByRole } = render(<RootWrapper />);
// get first section -> first subsection -> first unit element
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children;
axiosMock
.onGet(getXBlockApiUrl(section.id))
.reply(200, courseSectionMock);
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
// mock api call
axiosMock
.onPost(getClipboardUrl(), {
usage_key: unit.id,
}).reply(200, clipboardUnit);
// check that initialUserClipboard state is empty
const { initialUserClipboard } = store.getState().courseOutline;
expect(initialUserClipboard).toBeUndefined();
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
// move first unit back to second position to test move down option
const copyButton = await within(unitElement).findByText(cardHeaderMessages.menuCopy.defaultMessage);
await act(async () => fireEvent.click(copyButton));
// check that initialUserClipboard state is updated
expect(store.getState().generic.clipboardData).toEqual(clipboardUnit);
[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// find clipboard content label
const clipboardLabel = await within(subsectionElement).findByText(
pasteButtonMessages.pasteButtonWhatsInClipboardText.defaultMessage,
);
await act(async () => fireEvent.mouseOver(clipboardLabel));
// find clipboard content popover link
const popoverContent = queryByTestId('popover-content');
expect(popoverContent.tagName).toBe('A');
expect(popoverContent).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${unit.studioUrl}`);
// check paste button functionality
// mock api call
axiosMock
.onPost(getXBlockBaseApiUrl(), {
parent_locator: subsection.id,
staged_content: 'clipboard',
}).reply(200, {
staticFileNotices: {
newFiles: ['some.css'],
conflictingFiles: ['con.css'],
errorFiles: ['error.css'],
},
});
const pasteBtn = await within(subsectionElement).findByText(subsectionMessages.pasteButton.defaultMessage);
await act(async () => fireEvent.click(pasteBtn));
[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const lastUnitElement = (await within(subsectionElement).findAllByTestId('unit-card')).slice(-1)[0];
expect(lastUnitElement).toHaveTextContent(unit.displayName);
// check pasteFileNotices in store
expect(store.getState().courseOutline.pasteFileNotices).toEqual({
newFiles: ['some.css'],
conflictingFiles: ['con.css'],
errorFiles: ['error.css'],
});
// 3 alerts should be present
const alerts = await findAllByRole('alert');
expect(alerts.length).toEqual(3);
// check alerts for errorFiles
let dismissBtn = await within(alerts[0]).findByText('Dismiss');
fireEvent.click(dismissBtn);
// check alerts for conflictingFiles
dismissBtn = await within(alerts[1]).findByText('Dismiss');
fireEvent.click(dismissBtn);
// check alerts for newFiles
dismissBtn = await within(alerts[2]).findByText('Dismiss');
fireEvent.click(dismissBtn);
// check pasteFileNotices in store
expect(store.getState().courseOutline.pasteFileNotices).toEqual({});
});
it('should show toats on export tags', async () => {
const expectedResponse = 'this is a test';
axiosMock
.onGet(exportTags(courseId))
.reply(200, expectedResponse);
useLocation.mockReturnValue({
pathname: '/foo-bar',
hash: '#export-tags',
});
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
window.URL.revokeObjectURL = jest.fn();
render(<RootWrapper />);
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
expect(expectedRequest.length).toBe(1);
expect(await screen.findByText('Course tags exported successfully')).toBeInTheDocument();
});
it('should show toast on export tags error', async () => {
axiosMock
.onGet(exportTags(courseId))
.reply(404);
useLocation.mockReturnValue({
pathname: '/foo-bar',
hash: '#export-tags',
});
render(<RootWrapper />);
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
});
});