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 './drag-helper/utils'; import { postXBlockBaseApiUrl } from '../course-unit/data/api'; import { COMPONENT_TYPES } from '../generic/block-type-utils/constants'; let axiosMock; let store; const mockPathname = '/foo-bar'; const courseId = '123'; const containerKey = 'lct:org:lib:unit:1'; window.HTMLElement.prototype.scrollIntoView = jest.fn(); 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({}), })); // Mock ComponentPicker to call onComponentSelected on click jest.mock('../library-authoring/component-picker', () => ({ ComponentPicker: (props) => { const onClick = () => { // eslint-disable-next-line react/prop-types props.onComponentSelected({ usageKey: containerKey, blockType: 'unti', }); }; return ( ); }, })); 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 = () => ( ); describe('', () => { beforeEach(async () => { initializeMockApp({ authenticatedUser: { userId: 3, username: 'abc123', administrator: true, roles: [], }, }); useLocation.mockReturnValue({ pathname: mockPathname, }); store = initializeStore({ studioHome: { studioHomeData: { librariesV2Enabled: true } }, }); 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); }); afterEach(() => { jest.restoreAllMocks(); }); it('render CourseOutline component correctly', async () => { const { getByText } = render(); 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(); 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(); 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(); 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 () => { render(); axiosMock .onPost(getCourseBlockApiUrl(courseId), { metadata: { video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, }, }) .reply(500); const optionDropdown = await screen.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 alertElements = screen.queryAllByRole('alert'); expect(alertElements.find( (el) => el.classList.contains('alert-content'), )).toHaveTextContent( pageAlertMessages.alertFailedGeneric.defaultMessage, ); }); it('render error alert after failed reindex correctly', async () => { const { findByText, findByTestId } = render(); 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, findByTestId } = render(); const expandAllButton = await findByTestId('expand-collapse-all-button'); fireEvent.click(expandAllButton); const [section] = store.getState().courseOutline.sectionsList; const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); const draggableButton = sectionsDraggers[1]; axiosMock .onPut(getCourseBlockApiUrl(section.id)) .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, findByTestId } = render(); const expandAllButton = await findByTestId('expand-collapse-all-button'); fireEvent.click(expandAllButton); const [section] = store.getState().courseOutline.sectionsList; const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); const draggableButton = sectionsDraggers[1]; axiosMock .onPut(getCourseBlockApiUrl(section.id)) .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(); 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(); 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(); const [sectionElement] = await findAllByTestId('section-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); 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('adds a unit from library correctly', async () => { render(); const [sectionElement] = await screen.findAllByTestId('section-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const units = await within(subsectionElement).findAllByTestId('unit-card'); expect(units.length).toBe(1); axiosMock .onPost(postXBlockBaseApiUrl()) .reply(200, { locator: 'some', }); const addUnitFromLibraryButton = within(subsectionElement).getByRole('button', { name: /use unit from library/i, }); fireEvent.click(addUnitFromLibraryButton); // click dummy button to execute onComponentSelected prop. const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' }); fireEvent.click(dummyBtn); waitFor(() => 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({ type: COMPONENT_TYPES.libraryV2, category: 'vertical', parent_locator: subsection.id, library_content_key: containerKey, })); }); it('render checklist value correctly', async () => { const { getByText } = render(); 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(); 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(); 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(); 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(); 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', }); render(); const alert = await screen.findByText(pageAlertMessages.configurationErrorTitle.defaultMessage); expect(alert).toBeInTheDocument(); const dismissBtn = await screen.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(); 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 [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 () => { render(); // get section, subsection and unit const [section] = courseOutlineIndexMock.courseStructure.childInfo.children; const [sectionElement] = await screen.findAllByTestId('section-card'); const [subsection] = section.childInfo.children; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const checkDeleteBtn = async (item, element, elementName) => { await waitFor(() => { expect(screen.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 screen.findByRole('button', { name: 'Delete' }); fireEvent.click(confirmButton); await waitFor(() => { expect(screen.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(); // 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 [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(); 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 [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(); 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(); 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(); 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(); 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(); 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(); 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(); 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 [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(); 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(); // 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(); // 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(); // 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(); 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(); 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(); // 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(); // 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 [, 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(); // 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 [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(); // 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 [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(); // 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 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(); // 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 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(); // using first section -> first subsection -> first unit const sections = await findAllByTestId('section-card'); const [sectionElement] = sections; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); // 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'); // 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(); 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 [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(); 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 [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(); // 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 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(); // 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 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 () => { render(); // get first section -> first subsection -> first unit element const [section] = courseOutlineIndexMock.courseStructure.childInfo.children; const [sectionElement] = await screen.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 [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); // 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)); [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 = screen.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'], }); let alerts = await screen.findAllByRole('alert'); // Exclude processing notification toast alerts = alerts.filter((el) => !el.classList.contains('toast-container')); // 3 alerts should be present 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'; // Delay to ensure we see "Please wait." // Without the delay the success message renders too quickly const delayedResponse = axiosMock .onGet(exportTags(courseId)) .withDelayInMs(500); delayedResponse(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(); await screen.findByText('Please wait. Creating export file for course tags...'); const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId)); expect(expectedRequest.length).toBe(1); await screen.findByText('Course tags exported successfully'); }); it('should show toast on export tags error', async () => { // Delay to ensure we see "Please wait." // Without the delay the error renders too quickly const delayedResponse = axiosMock .onGet(exportTags(courseId)) .withDelayInMs(500); delayedResponse(404); useLocation.mockReturnValue({ pathname: '/foo-bar', hash: '#export-tags', }); render(); await screen.findByText('Please wait. Creating export file for course tags...'); await screen.findByText('An error has occurred creating the file'); }); it('sets status to DENIED when API responds with 403', async () => { axiosMock .onGet(getCourseOutlineIndexApiUrl(courseId)) .reply(403); const { getByTestId } = render(); await waitFor(() => { expect(getByTestId('redux-provider')).toBeInTheDocument(); const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus; expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED); }); }); });