diff --git a/package-lock.json b/package-lock.json
index d86341cc98..079935dd9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -57,7 +57,7 @@
"moment-shortformat": "^2.1.0",
"prop-types": "^15.8.1",
"react": "^18.3.1",
- "react-datepicker": "^4.13.0",
+ "react-datepicker": "^7.6.0",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
@@ -2995,28 +2995,62 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "1.6.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
- "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
- "@floating-ui/utils": "^0.2.9"
+ "@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
- "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
+ "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.15",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.15.tgz",
+ "integrity": "sha512-0LGxhBi3BB1DwuSNQAmuaSuertFzNAerlMdPbotjTVnvPtdOs7CkrHLaev5NIXemhzDXNC0tFzuseut7cWA5mw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.5",
+ "@floating-ui/utils": "^0.2.10",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
+ "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
"license": "MIT",
"dependencies": {
- "@floating-ui/core": "^1.6.0",
- "@floating-ui/utils": "^0.2.9"
+ "@floating-ui/dom": "^1.7.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
}
},
+ "node_modules/@floating-ui/react/node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "license": "MIT"
+ },
"node_modules/@floating-ui/utils": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
- "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@formatjs/cli": {
@@ -8620,6 +8654,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -9398,19 +9441,13 @@
}
},
"node_modules/date-fns": {
- "version": "2.30.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
- "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
+ "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.21.0"
- },
- "engines": {
- "node": ">=0.11"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/date-fns"
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debounce": {
@@ -18847,21 +18884,18 @@
}
},
"node_modules/react-datepicker": {
- "version": "4.25.0",
- "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz",
- "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.6.0.tgz",
+ "integrity": "sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==",
"license": "MIT",
"dependencies": {
- "@popperjs/core": "^2.11.8",
- "classnames": "^2.2.6",
- "date-fns": "^2.30.0",
- "prop-types": "^15.7.2",
- "react-onclickoutside": "^6.13.0",
- "react-popper": "^2.3.0"
+ "@floating-ui/react": "^0.27.0",
+ "clsx": "^2.1.1",
+ "date-fns": "^3.6.0"
},
"peerDependencies": {
- "react": "^16.9.0 || ^17 || ^18",
- "react-dom": "^16.9.0 || ^17 || ^18"
+ "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/react-dev-utils": {
diff --git a/package.json b/package.json
index db3a39dde7..7e4149f42f 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
"moment-shortformat": "^2.1.0",
"prop-types": "^15.8.1",
"react": "^18.3.1",
- "react-datepicker": "^4.13.0",
+ "react-datepicker": "^7.6.0",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
diff --git a/src/course-outline/CourseOutline.test.tsx b/src/course-outline/CourseOutline.test.tsx
index 5d3b540f9d..5519ca00e9 100644
--- a/src/course-outline/CourseOutline.test.tsx
+++ b/src/course-outline/CourseOutline.test.tsx
@@ -934,7 +934,7 @@ describe('
Submitted update content
', + date: 'August 15, 2025', + }; + + axiosMock + .onPost(getCourseUpdatesApiUrl(courseId)) + .reply(200, submittedUpdate); + + // Simulate form submit by clicking the Save button + const saveButton = getByRole('button', { name: /Post/i }); // Match case-insensitively + fireEvent.click(saveButton); + + // Expect new update to be rendered + await waitFor(() => { + expect(getByText('Submitted update content')).toBeInTheDocument(); + expect(getByText('August 15, 2025')).toBeInTheDocument(); + }); + }); }); }); diff --git a/src/course-updates/hooks.jsx b/src/course-updates/hooks.jsx index 8bf1d36616..1dcfd37c62 100644 --- a/src/course-updates/hooks.jsx +++ b/src/course-updates/hooks.jsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react'; import { useToggle } from '@openedx/paragon'; import { COMMA_SEPARATED_DATE_FORMAT } from '../constants'; -import { convertToDateFromString } from '../utils'; import { getCourseHandouts, getCourseUpdates } from './data/selectors'; import { REQUEST_TYPES } from './constants'; import { @@ -56,7 +55,7 @@ const useCourseUpdates = ({ courseId }) => { }; const handleUpdatesSubmit = (data) => { - const dateWithoutTimezone = convertToDateFromString(data.date); + const dateWithoutTimezone = (data.date); const dataToSend = { ...data, date: moment(dateWithoutTimezone).format(COMMA_SEPARATED_DATE_FORMAT), diff --git a/src/course-updates/hooks.test.jsx b/src/course-updates/hooks.test.jsx new file mode 100644 index 0000000000..a64a6b25a4 --- /dev/null +++ b/src/course-updates/hooks.test.jsx @@ -0,0 +1,151 @@ +import moment from 'moment'; +import { REQUEST_TYPES } from './constants'; +import { + createCourseUpdateQuery, + editCourseUpdateQuery, + editCourseHandoutsQuery, +} from './data/thunk'; +import { COMMA_SEPARATED_DATE_FORMAT } from '../constants'; + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(), + useSelector: jest.fn(), +})); + +jest.mock('./data/thunk', () => ({ + createCourseUpdateQuery: jest.fn(), + editCourseUpdateQuery: jest.fn(), + editCourseHandoutsQuery: jest.fn(), +})); + +jest.mock('../constants', () => ({ + COMMA_SEPARATED_DATE_FORMAT: 'YYYY,MM,DD', +})); + +describe('handleUpdatesSubmit', () => { + let dispatchMock; + let closeUpdateFormMock; + let setCurrentUpdateMock; + + const courseId = 'course-v1:test+T101+2025_T1'; + + beforeEach(() => { + dispatchMock = jest.fn(); + closeUpdateFormMock = jest.fn(); + setCurrentUpdateMock = jest.fn(); + + jest.requireActual('./hooks'); + }); + + const getMockHookContext = (requestType) => { + jest.requireActual('../hooks'); + + return { + requestType, + courseId, + closeUpdateForm: closeUpdateFormMock, + setCurrentUpdate: setCurrentUpdateMock, + dispatch: dispatchMock, + initialUpdate: { id: 0, date: moment().toDate(), content: '' }, + }; + }; + + const testData = { + id: 5, + content: 'Sample content', + date: new Date('2025-08-01T00:00:00Z'), + }; + + it('dispatches createCourseUpdateQuery when requestType is add_new_update', () => { + const formattedDate = moment(testData.date).format(COMMA_SEPARATED_DATE_FORMAT); + createCourseUpdateQuery.mockReturnValue('mockCreateAction'); + + const context = getMockHookContext(REQUEST_TYPES.add_new_update); + const submitFn = (data) => { + const date = moment(data.date).format(COMMA_SEPARATED_DATE_FORMAT); + const action = createCourseUpdateQuery(context.courseId, { + date, + content: data.content, + }); + context.closeUpdateForm(); + context.setCurrentUpdate(context.initialUpdate); + context.dispatch(action); + }; + + submitFn(testData); + + expect(createCourseUpdateQuery).toHaveBeenCalledWith(courseId, { + date: formattedDate, + content: 'Sample content', + }); + expect(dispatchMock).toHaveBeenCalledWith('mockCreateAction'); + expect(closeUpdateFormMock).toHaveBeenCalled(); + expect(setCurrentUpdateMock).toHaveBeenCalledWith(expect.objectContaining({ id: 0 })); + }); + + it('dispatches editCourseUpdateQuery when requestType is edit_update', () => { + const formattedDate = moment(testData.date).format(COMMA_SEPARATED_DATE_FORMAT); + editCourseUpdateQuery.mockReturnValue('mockEditAction'); + + const context = getMockHookContext(REQUEST_TYPES.edit_update); + const submitFn = (data) => { + const date = moment(data.date).format(COMMA_SEPARATED_DATE_FORMAT); + const action = editCourseUpdateQuery(context.courseId, { + id: data.id, + date, + content: data.content, + }); + context.closeUpdateForm(); + context.setCurrentUpdate(context.initialUpdate); + context.dispatch(action); + }; + + submitFn(testData); + + expect(editCourseUpdateQuery).toHaveBeenCalledWith(courseId, { + id: 5, + date: formattedDate, + content: 'Sample content', + }); + expect(dispatchMock).toHaveBeenCalledWith('mockEditAction'); + expect(closeUpdateFormMock).toHaveBeenCalled(); + expect(setCurrentUpdateMock).toHaveBeenCalledWith(expect.objectContaining({ id: 0 })); + }); + + it('dispatches editCourseHandoutsQuery when requestType is edit_handouts', () => { + editCourseHandoutsQuery.mockReturnValue('mockHandoutAction'); + + const context = getMockHookContext(REQUEST_TYPES.edit_handouts); + const submitFn = (data) => { + const formatted = { + ...data, + date: moment(data.date).format(COMMA_SEPARATED_DATE_FORMAT), + data: data.data || '', + }; + const action = editCourseHandoutsQuery(context.courseId, formatted); + context.closeUpdateForm(); + context.setCurrentUpdate(context.initialUpdate); + context.dispatch(action); + }; + + submitFn(testData); + + expect(editCourseHandoutsQuery).toHaveBeenCalledWith(courseId, expect.objectContaining({ + date: expect.any(String), + content: 'Sample content', + })); + expect(dispatchMock).toHaveBeenCalledWith('mockHandoutAction'); + expect(closeUpdateFormMock).toHaveBeenCalled(); + expect(setCurrentUpdateMock).toHaveBeenCalledWith(expect.objectContaining({ id: 0 })); + }); + it('extracts date without formatting for internal logic', () => { + getMockHookContext(REQUEST_TYPES.edit_update); + + const submitFn = (data) => { + const dateWithoutTimezone = data.date; + expect(dateWithoutTimezone).toEqual(new Date('2025-08-01T00:00:00Z')); + }; + + submitFn(testData); + }); +}); diff --git a/src/course-updates/update-form/UpdateForm.jsx b/src/course-updates/update-form/UpdateForm.jsx index 23fa88321c..49c4060daa 100644 --- a/src/course-updates/update-form/UpdateForm.jsx +++ b/src/course-updates/update-form/UpdateForm.jsx @@ -13,8 +13,6 @@ import { Calendar as CalendarIcon, Error as ErrorIcon } from '@openedx/paragon/i import { Formik } from 'formik'; import { - convertToStringFromDate, - convertToDateFromString, isValidDate, } from '../../utils'; import { DATE_FORMAT, DEFAULT_EMPTY_WYSIWYG_VALUE } from '../../constants'; @@ -73,7 +71,7 @@ const UpdateForm = ({