diff --git a/package.json b/package.json index e0b2b0c1b..b58efd4f7 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "stylelint": "stylelint \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json", "lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx .", "lint:fix": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx . --fix", - "snapshot": "fedx-scripts jest --updateSnapshot", + "snapshot": "TZ=UTC fedx-scripts jest --updateSnapshot", "start": "fedx-scripts webpack-dev-server --progress", "start:with-theme": "paragon install-theme && npm start && npm install", - "test": "fedx-scripts jest --coverage --passWithNoTests", + "test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests", "types": "tsc --noEmit" }, "husky": { diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index cdc4f86b7..205717dae 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -20,9 +20,6 @@ import { } from './data/api'; import { RequestStatus } from '../data/constants'; import { - configureCourseSectionQuery, - configureCourseSubsectionQuery, - configureCourseUnitQuery, fetchCourseBestPracticesQuery, fetchCourseLaunchQuery, fetchCourseOutlineIndexQuery, @@ -615,58 +612,69 @@ describe('', () => { // section doesn't display badges }); - it('check configure section when configure query is successful', async () => { - const { findAllByTestId, findByPlaceholderText } = render(); + it('check configure modal for section', async () => { + const { findByTestId, findAllByTestId } = render(); const section = courseOutlineIndexMock.courseStructure.childInfo.children[0]; - const newReleaseDate = '2025-08-10T10:00:00Z'; + 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: newReleaseDate, + start: newReleaseDateIso, }, }) .reply(200, { dummy: 'value' }); axiosMock .onGet(getXBlockApiUrl(section.id)) - .reply(200, section); + .reply(200, { + ...section, + start: newReleaseDateIso, + }); const [firstSection] = await findAllByTestId('section-card'); const sectionDropdownButton = await within(firstSection).findByTestId('section-card-header__menu-button'); - fireEvent.click(sectionDropdownButton); - - axiosMock - .onGet(getXBlockApiUrl(section.id)) - .reply(200, { - ...section, - start: newReleaseDate, - }); - - await executeThunk(configureCourseSectionQuery(section.id, true, newReleaseDate), store.dispatch); - fireEvent.click(sectionDropdownButton); + await act(async () => fireEvent.click(sectionDropdownButton)); const configureBtn = await within(firstSection).findByTestId('section-card-header__menu-configure-button'); - fireEvent.click(configureBtn); + 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'); - const datePicker = await findByPlaceholderText('MM/DD/YYYY'); - expect(datePicker).toHaveValue('08/10/2025'); + 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 subsection when configure subsection query is successful', async () => { + it('check configure modal for subsection', async () => { const { findAllByTestId, - findByText, - findAllByRole, - findByRole, findByTestId, } = render(); const section = courseOutlineIndexMock.courseStructure.childInfo.children[0]; const subsection = section.childInfo.children[0]; - const newReleaseDate = '2025-08-10T10:00:00Z'; + const newReleaseDate = '2025-08-10T05:00:00Z'; const newGraderType = 'Homework'; - const newDue = '2025-09-10T10:00:00Z'; + const newDue = '2025-09-10T00:00:00Z'; const isTimeLimited = true; const defaultTimeLimitMinutes = 210; @@ -675,10 +683,10 @@ describe('', () => { publish: 'republish', graderType: newGraderType, metadata: { - visible_to_staff_only: true, + visible_to_staff_only: null, due: newDue, hide_after_due: false, - show_correctness: false, + show_correctness: 'always', is_practice_exam: false, is_time_limited: isTimeLimited, exam_review_rules: '', @@ -690,14 +698,9 @@ describe('', () => { }) .reply(200, { dummy: 'value' }); - axiosMock - .onGet(getXBlockApiUrl(section.id)) - .reply(200, section); - const [currentSection] = await findAllByTestId('section-card'); const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card'); - const subsectionDropdownButton = firstSubsection.querySelector('#subsection-card-header__menu'); - expect(subsectionDropdownButton).toBeInTheDocument(); + const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button'); subsection.start = newReleaseDate; subsection.due = newDue; @@ -709,42 +712,78 @@ describe('', () => { .onGet(getXBlockApiUrl(section.id)) .reply(200, section); - await executeThunk(configureCourseSubsectionQuery( - subsection.id, - section.id, - true, - newReleaseDate, - newGraderType, - newDue, - true, - defaultTimeLimitMinutes, - false, - false, - ), store.dispatch); fireEvent.click(subsectionDropdownButton); const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button'); fireEvent.click(configureBtn); - expect(await findByText(newGraderType)).toBeInTheDocument(); - const releaseDateStack = await findByTestId('release-date-stack'); - const releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY'); - expect(releaseDatePicker).toHaveValue('08/10/2025'); - const dueDateStack = await findByTestId('due-date-stack'); - const dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY'); - expect(dueDatePicker).toHaveValue('09/10/2025'); + // update fields + let configureModal = await findByTestId('configure-modal'); + expect(await within(configureModal).findByText(newGraderType)).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 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 graderTypeDropdown = await within(configureModal).findByTestId('grader-type-select'); + fireEvent.change(graderTypeDropdown, { target: { value: newGraderType } }); - const advancedTab = await findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage }); + let advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage }); fireEvent.click(advancedTab); - const radioButtons = await findAllByRole('radio'); + 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).findByRole('textbox'); + fireEvent.change(hours, { target: { value: '03: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({ + publish: 'republish', + graderType: newGraderType, + metadata: { + visible_to_staff_only: null, + due: newDue, + hide_after_due: false, + show_correctness: 'always', + is_practice_exam: false, + is_time_limited: isTimeLimited, + exam_review_rules: '', + is_proctored_enabled: false, + default_time_limit_minutes: defaultTimeLimitMinutes, + is_onboarding_exam: false, + start: newReleaseDate, + }, + })); + + // 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'); + dueDateStack = await await within(configureModal).findByTestId('due-date-stack'); + dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY'); + expect(dueDatePicker).toHaveValue('09/10/2025'); + graderTypeDropdown = await within(configureModal).findByTestId('grader-type-select'); + expect(graderTypeDropdown).toHaveValue(newGraderType); + + 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); - const hoursWrapper = await findByTestId('advanced-tab-hours-picker-wrapper'); - const hours = await within(hoursWrapper).findByRole('textbox'); + hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper'); + hours = await within(hoursWrapper).findByRole('textbox'); expect(hours).toHaveValue('03:30'); }); - it('check configure unit when configure query is successful', async () => { - const { findAllByTestId, findByText, findByTestId } = render(); + 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; @@ -771,8 +810,7 @@ describe('', () => { const subsectionExpandButton = await within(firstSubsection).getByTestId('subsection-card-header__expanded-btn'); fireEvent.click(subsectionExpandButton); const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card'); - const unitDropdownButton = firstUnit.querySelector('#unit-card-header__menu'); - expect(unitDropdownButton).toBeInTheDocument(); + const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button'); // after configuraiton response unit.visibilityState = 'staff_only'; @@ -800,33 +838,49 @@ describe('', () => { ], selectedPartitionIndex: 0, }; + subsection.childInfo.children[0] = unit; + section.childInfo.children[0] = subsection; axiosMock .onGet(getXBlockApiUrl(section.id)) .reply(200, section); - await executeThunk( - configureCourseUnitQuery(unit.id, section.id, isVisibleToStaffOnly, newGroupAccess), - store.dispatch, - ); - fireEvent.click(unitDropdownButton); const configureBtn = await within(firstUnit).getByTestId('unit-card-header__menu-configure-button'); - // console.log('configureBtn', configureBtn); fireEvent.click(configureBtn); - expect(await findByText(configureModalMessages.unitVisibility.defaultMessage)).toBeInTheDocument(); - const visibilityCheckbox = await findByTestId('unit-visibility-checkbox'); + 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 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(); - const groupeType = await findByTestId('group-type-select'); + groupeType = await within(configureModal).findByTestId('group-type-select'); expect(groupeType).toHaveValue('0'); - const checkboxes = await within(await findByTestId('group-checkboxes')).findAllByRole('checkbox'); + 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(); diff --git a/src/course-outline/configure-modal/ConfigureModal.jsx b/src/course-outline/configure-modal/ConfigureModal.jsx index 271e8eb6c..764d99ac0 100644 --- a/src/course-outline/configure-modal/ConfigureModal.jsx +++ b/src/course-outline/configure-modal/ConfigureModal.jsx @@ -275,24 +275,26 @@ const ConfigureModal = ({ isFullscreenOnMobile isFullscreenScroll > - - - {intl.formatMessage(messages.title, { title: displayName })} - - - - {renderModalBody(category)} - - - - - {intl.formatMessage(messages.cancelButton)} - - - - +
+ + + {intl.formatMessage(messages.title, { title: displayName })} + + + + {renderModalBody(category)} + + + + + {intl.formatMessage(messages.cancelButton)} + + + + +
) );