From 7f7625333d51d986ac2864f2f513d4c216f7221f Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Tue, 26 Jul 2022 16:08:53 -0400 Subject: [PATCH] chore: update course entitlement chore: update requested change chore: move show select session dialog to redux app level --- package-lock.json | 25 ++- package.json | 2 +- .../__snapshots__/index.test.jsx.snap | 11 +- .../components/Banners/EntitlementBanner.jsx | 15 +- .../Banners/EntitlementBanner.test.jsx | 5 +- .../EntitlementBanner.test.jsx.snap | 3 +- .../CourseCard/components/Banners/messages.js | 5 + .../components/CourseCardActions/hooks.js | 57 ++++-- .../CourseCardActions/hooks.test.js | 158 +++++++++++---- .../components/CourseCardActions/messages.js | 5 + .../__snapshots__/index.test.jsx.snap | 42 ++++ .../components/CourseCardDetails/hooks.js | 52 +++++ .../CourseCardDetails/hooks.test.js | 139 +++++++++++++ .../components/CourseCardDetails/index.jsx | 42 ++++ .../CourseCardDetails/index.test.jsx | 51 +++++ .../components/CourseCardDetails/messages.js | 36 ++++ src/containers/CourseCard/hooks.js | 28 --- src/containers/CourseCard/hooks.test.js | 96 +-------- src/containers/CourseCard/index.jsx | 7 +- src/containers/CourseCard/index.test.jsx | 3 +- src/containers/CourseCard/messages.js | 25 --- src/containers/CourseList/index.jsx | 2 + .../SelectSession/SelectSessionModal.jsx | 82 ++++++++ .../SelectSession/SelectSessionModal.test.jsx | 54 +++++ .../SelectSessionModal.test.jsx.snap | 188 ++++++++++++++++++ .../__snapshots__/index.test.jsx.snap | 9 + src/containers/SelectSession/hooks.js | 35 ++++ src/containers/SelectSession/hooks.test.js | 85 ++++++++ src/containers/SelectSession/index.jsx | 12 ++ src/containers/SelectSession/index.test.jsx | 26 +++ src/containers/SelectSession/messages.js | 42 ++++ src/data/redux/app/reducer.js | 7 + src/data/redux/app/reducer.test.js | 14 +- src/data/redux/app/selectors.js | 2 + src/data/redux/hooks.js | 1 + src/data/services/lms/fakeData/courses.js | 6 + src/setupTest.jsx | 11 +- src/test/app.test.jsx | 9 +- src/test/messages.js | 6 +- src/utils/dateFormatter.js | 5 + src/utils/index.js | 1 + 41 files changed, 1147 insertions(+), 257 deletions(-) create mode 100644 src/containers/CourseCard/components/CourseCardDetails/__snapshots__/index.test.jsx.snap create mode 100644 src/containers/CourseCard/components/CourseCardDetails/hooks.js create mode 100644 src/containers/CourseCard/components/CourseCardDetails/hooks.test.js create mode 100644 src/containers/CourseCard/components/CourseCardDetails/index.jsx create mode 100644 src/containers/CourseCard/components/CourseCardDetails/index.test.jsx create mode 100644 src/containers/CourseCard/components/CourseCardDetails/messages.js create mode 100644 src/containers/SelectSession/SelectSessionModal.jsx create mode 100644 src/containers/SelectSession/SelectSessionModal.test.jsx create mode 100644 src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap create mode 100644 src/containers/SelectSession/__snapshots__/index.test.jsx.snap create mode 100644 src/containers/SelectSession/hooks.js create mode 100644 src/containers/SelectSession/hooks.test.js create mode 100644 src/containers/SelectSession/index.jsx create mode 100644 src/containers/SelectSession/index.test.jsx create mode 100644 src/containers/SelectSession/messages.js create mode 100644 src/utils/dateFormatter.js diff --git a/package-lock.json b/package-lock.json index 3e10032..42e30f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@redux-beacon/segment": "^1.1.0", "@reduxjs/toolkit": "^1.6.1", "@testing-library/user-event": "^13.5.0", - "@zip.js/zip.js": "^2.4.6", "axios": "^0.21.4", "classnames": "^2.3.1", "core-js": "3.16.2", @@ -34,6 +33,7 @@ "history": "5.0.1", "html-react-parser": "^1.3.0", "lodash": "^4.17.21", + "moment": "^2.29.4", "prop-types": "15.7.2", "query-string": "7.0.1", "react": "^16.14.0", @@ -7544,11 +7544,6 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, - "node_modules/@zip.js/zip.js": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.15.tgz", - "integrity": "sha512-fAZkoF0qG8MCijvx4xCyVISAEwLWo8L/JCe5Mrl1zhHpZv+RK6hodIMnKoyZpT5MLGYgr7vJh/y5/1cF7WBUlw==" - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -21920,6 +21915,14 @@ "node": ">=0.10.0" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -39424,11 +39427,6 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, - "@zip.js/zip.js": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.15.tgz", - "integrity": "sha512-fAZkoF0qG8MCijvx4xCyVISAEwLWo8L/JCe5Mrl1zhHpZv+RK6hodIMnKoyZpT5MLGYgr7vJh/y5/1cF7WBUlw==" - }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -50452,6 +50450,11 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", diff --git a/package.json b/package.json index 6cc8e83..0c1cadb 100755 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@redux-beacon/segment": "^1.1.0", "@reduxjs/toolkit": "^1.6.1", "@testing-library/user-event": "^13.5.0", - "@zip.js/zip.js": "^2.4.6", "axios": "^0.21.4", "classnames": "^2.3.1", "core-js": "3.16.2", @@ -51,6 +50,7 @@ "history": "5.0.1", "html-react-parser": "^1.3.0", "lodash": "^4.17.21", + "moment": "^2.29.4", "prop-types": "15.7.2", "query-string": "7.0.1", "react": "^16.14.0", diff --git a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap index 4df63b5..f09d2db 100644 --- a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap @@ -36,14 +36,9 @@ exports[`CourseCard component snapshot 1`] = ` } /> - - hooks.providerName - • - test-course-number - • - + { @@ -15,8 +17,10 @@ export const EntitlementBanner = ({ courseNumber }) => { isFulfilled, changeDeadline, showExpirationWarning, + isExpired, } = appHooks.useCardEntitlementsData(courseNumber); const { supportEmail } = appHooks.usePlatformSettingsData(); + const { openSessionModal } = useSelectSession({ courseNumber }); const { formatDate, formatMessage } = useIntl(); if (!isEntitlement) { @@ -36,9 +40,9 @@ export const EntitlementBanner = ({ courseNumber }) => { return ( {formatMessage(messages.entitlementsExpiringSoon, { - changeDeadline: formatDate(changeDeadline), + changeDeadline: dateFormatter(formatDate, changeDeadline), selectSessionButton: ( - ), @@ -46,6 +50,13 @@ export const EntitlementBanner = ({ courseNumber }) => { ); } + if (isExpired) { + return ( + + {formatMessage(messages.entitlementsExpired)} + + ); + } return null; }; EntitlementBanner.propTypes = { diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx index 991c5d5..dddb1e9 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx @@ -11,6 +11,9 @@ jest.mock('data/redux', () => ({ useCardEntitlementsData: jest.fn(), }, })); +jest.mock('containers/SelectSession/hooks', () => () => ({ + openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'), +})); const courseNumber = 'my-test-course-number'; @@ -20,7 +23,7 @@ const entitlementsData = { isEntitlement: true, hasSessions: true, isFulfilled: false, - changeDeadline: 'test-deadline', + changeDeadline: '11/11/2022', showExpirationWarning: false, }; const platformData = { supportEmail: 'test-support-email' }; diff --git a/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap index 8c36286..640d117 100644 --- a/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap +++ b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap @@ -12,9 +12,10 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = ` } values={ Object { - "changeDeadline": "test-deadline", + "changeDeadline": "11/11/2022", "selectSessionButton": + +`; diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.js new file mode 100644 index 0000000..b827b4e --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.js @@ -0,0 +1,52 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import { hooks as appHooks } from 'data/redux'; +import useSelectSession from 'containers/SelectSession/hooks'; + +import * as module from './hooks'; +import messages from './messages'; + +export const useAccessMessage = ({ courseNumber }) => { + const { formatMessage, formatDate } = useIntl(); + const { + accessExpirationDate, + isAudit, + isAuditAccessExpired, + } = appHooks.useCardEnrollmentData(courseNumber); + const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber); + + if (isAudit) { + return formatMessage( + isAuditAccessExpired ? messages.accessExpired : messages.accessExpires, + { accessExpirationDate: formatDate(accessExpirationDate) }, + ); + } + + return formatMessage( + isArchived ? messages.courseEnded : messages.courseEnds, + { endDate: formatDate(endDate) }, + ); +}; + +export const useCardDetailsData = ({ courseNumber }) => { + const { formatMessage } = useIntl(); + const providerName = appHooks.useCardProviderData(courseNumber).name; + const { + isEntitlement, + isFulfilled, + canChange, + } = appHooks.useCardEntitlementsData(courseNumber); + + const { openSessionModalWithLeaveOption: openSessionModal } = useSelectSession({ courseNumber }); + + return { + providerName: providerName || formatMessage(messages.unknownProviderName), + accessMessage: module.useAccessMessage({ courseNumber }), + isEntitlement, + isFulfilled, + canChange, + openSessionModal, + formatMessage, + }; +}; + +export default useCardDetailsData; diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js new file mode 100644 index 0000000..b35cfc5 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js @@ -0,0 +1,139 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { keyStore, dateFormatter } from 'utils'; +import { hooks as appHooks } from 'data/redux'; + +import * as hooks from './hooks'; +import messages from './messages'; + +jest.mock('data/redux', () => ({ + hooks: { + useCardCourseRunData: jest.fn(), + useCardEnrollmentData: jest.fn(), + useCardEntitlementsData: jest.fn(), + useCardProviderData: jest.fn(), + }, +})); +jest.mock('containers/SelectSession/hooks', () => () => ({ + openSessionModalWithLeaveOption: jest.fn().mockName('useSelectSession.openSessionModalWithLeaveOptionFunction'), +})); + +const courseNumber = 'my-test-course-number'; +const useAccessMessage = 'test-access-message'; +const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage }); +const hookKeys = keyStore(hooks); + +describe('CourseCard hooks', () => { + let out; + const { formatMessage, formatDate } = useIntl(); + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('useCardDetailsData', () => { + const providerData = { + name: 'my-provider-name', + }; + const entitlementData = { + isEntitlement: false, + canViewCourse: false, + isFulfilled: false, + isExpired: false, + canChange: false, + hasSessions: false, + }; + const runHook = ({ provider = {}, entitlement = {} }) => { + jest.spyOn(hooks, hookKeys.useAccessMessage) + .mockImplementationOnce(mockAccessMessage); + appHooks.useCardProviderData.mockReturnValueOnce({ + ...providerData, + ...provider, + }); + appHooks.useCardEntitlementsData.mockReturnValueOnce({ + ...entitlementData, + ...entitlement, + }); + out = hooks.useCardDetailsData({ courseNumber }); + }; + beforeEach(() => { + runHook({}); + }); + it('forwards formatMessage from useIntl', () => { + expect(out.formatMessage).toEqual(formatMessage); + }); + it('forwards useAccessMessage output, called with courseNumber', () => { + expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber })); + }); + it('forwards provider name if it exists, else formatted unknown provider name', () => { + expect(out.providerName).toEqual(providerData.name); + runHook({ provider: { name: '' } }); + expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName)); + }); + }); + describe('useAccessMessage', () => { + const enrollmentData = { + accessExpirationDate: 'test-expiration-date', + isAudit: false, + isAuditAccessExpired: false, + }; + const courseRunData = { + isFinished: false, + endDate: 'test-end-date', + }; + const runHook = ({ enrollment = {}, courseRun = {} }) => { + appHooks.useCardCourseRunData.mockReturnValueOnce({ + ...courseRunData, + ...courseRun, + }); + appHooks.useCardEnrollmentData.mockReturnValueOnce({ + ...enrollmentData, + ...enrollment, + }); + out = hooks.useAccessMessage({ courseNumber }); + }; + it('loads data from enrollment and course run data based on course number', () => { + runHook({}); + expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber); + }); + describe('if audit, and expired', () => { + it('returns accessExpired message with accessExpirationDate from cardData', () => { + runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } }); + expect(out).toEqual(formatMessage( + messages.accessExpired, + { accessExpirationDate: dateFormatter(formatDate, enrollmentData.accessExpirationDate) }, + )); + }); + }); + + describe('if audit and not expired', () => { + it('returns accessExpires message with accessExpirationDate from cardData', () => { + runHook({ enrollment: { isAudit: true } }); + expect(out).toEqual(formatMessage( + messages.accessExpires, + { accessExpirationDate: dateFormatter(formatDate, enrollmentData.accessExpirationDate) }, + )); + }); + }); + + describe('if verified and not ended', () => { + it('returns course ends message with course end date', () => { + runHook({}); + expect(out).toEqual(formatMessage( + messages.courseEnds, + { endDate: dateFormatter(formatDate, courseRunData.endDate) }, + )); + }); + }); + + describe('if verified and ended', () => { + it('returns course ended message with course end date', () => { + runHook({ courseRun: { isArchived: true } }); + expect(out).toEqual(formatMessage( + messages.courseEnded, + { endDate: dateFormatter(formatDate, courseRunData.endDate) }, + )); + }); + }); + }); +}); diff --git a/src/containers/CourseCard/components/CourseCardDetails/index.jsx b/src/containers/CourseCard/components/CourseCardDetails/index.jsx new file mode 100644 index 0000000..c138b6d --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardDetails/index.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button } from '@edx/paragon'; + +import useCardDetailsData from './hooks'; + +import messages from './messages'; + +export const CourseCardDetails = ({ courseNumber }) => { + const { + providerName, + accessMessage, + isEntitlement, + isFulfilled, + canChange, + openSessionModal, + formatMessage, + } = useCardDetailsData({ courseNumber }); + + return ( + + {providerName} • {courseNumber} • {accessMessage} + {isEntitlement && isFulfilled && canChange ? ( + <> + {' • '} + + + ) : null} + + ); +}; + +CourseCardDetails.propTypes = { + courseNumber: PropTypes.string.isRequired, +}; + +CourseCardDetails.defaultProps = {}; + +export default CourseCardDetails; diff --git a/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx b/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx new file mode 100644 index 0000000..86127e6 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import CourseCardDetails from '.'; + +import hooks from './hooks'; + +jest.mock('./hooks', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const courseNumber = 'test-course-number'; + +describe('CourseCard Details component', () => { + it('has change session button on entitlement course', () => { + const mockHook = (args) => () => ({ + providerName: 'provider-name', + accessMessage: 'access-message', + openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'), + formatMessage: (message, values) =>
, + isEntitlement: true, + isFulfilled: true, + canChange: true, + ...args, + }); + hooks.mockImplementationOnce(mockHook({ isEntitlement: true })); + const el = shallow(); + expect(el).toMatchSnapshot(); + // it has 3 separator, 4 column + expect(el.text().match(/•/g)).toHaveLength(3); + }); + + it('does not have change session button on regular course', () => { + const mockHook = (args) => () => ({ + providerName: 'provider-name', + accessMessage: 'acess-message', + openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'), + formatMessage: (message, values) =>
, + isEntitlement: true, + isFulfilled: true, + canChange: true, + ...args, + }); + hooks.mockImplementationOnce(mockHook({ isEntitlement: false })); + const el = shallow(); + expect(el).toMatchSnapshot(); + // it has 2 separator, 3 column + expect(el.text().match(/•/g)).toHaveLength(2); + }); +}); diff --git a/src/containers/CourseCard/components/CourseCardDetails/messages.js b/src/containers/CourseCard/components/CourseCardDetails/messages.js new file mode 100644 index 0000000..13738e4 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardDetails/messages.js @@ -0,0 +1,36 @@ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + accessExpired: { + id: 'learner-dash.courseCard.CourseCardDetails.accessExpired', + description: 'Course access expiration date message on course card for expired access.', + defaultMessage: 'Access expired {accessExpirationDate}', + }, + accessExpires: { + id: 'learner-dash.courseCard.CourseCardDetails.accessExpires', + description: 'Course access expiration date message on course card.', + defaultMessage: 'Access expires {accessExpirationDate}', + }, + courseEnded: { + id: 'learner-dash.courseCard.CourseCardDetails.courseEnded', + description: 'Course ended message on course card.', + defaultMessage: 'Course ended {endDate}', + }, + courseEnds: { + id: 'learner-dash.courseCard.CourseCardDetails.courseEnds', + description: 'Course ending message on course card.', + defaultMessage: 'Course ends {endDate}', + }, + unknownProviderName: { + id: 'learner-dash.courseCard.CourseCardDetails.unknownProviderName', + description: 'Provider name display when name is unknown', + defaultMessage: 'Unknown', + }, + changeOrLeaveSessionButton: { + id: 'learner-dash.courseCard.CourseCardDetails.changeOrLeaveSessionButton', + description: 'Button for trigger change or leave session for entitlement course', + defaultMessage: 'Change or leave session', + }, +}); + +export default messages; diff --git a/src/containers/CourseCard/hooks.js b/src/containers/CourseCard/hooks.js index 6287d56..ecadc00 100644 --- a/src/containers/CourseCard/hooks.js +++ b/src/containers/CourseCard/hooks.js @@ -1,41 +1,13 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { hooks as appHooks } from 'data/redux'; -import * as module from './hooks'; -import messages from './messages'; - -export const useAccessMessage = ({ courseNumber }) => { - const { formatMessage, formatDate } = useIntl(); - const { - accessExpirationDate, - isAudit, - isAuditAccessExpired, - } = appHooks.useCardEnrollmentData(courseNumber); - const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber); - - if (isAudit) { - return formatMessage( - isAuditAccessExpired ? messages.accessExpired : messages.accessExpires, - { accessExpirationDate: formatDate(accessExpirationDate) }, - ); - } - - return formatMessage( - isArchived ? messages.courseEnded : messages.courseEnds, - { endDate: formatDate(endDate) }, - ); -}; - export const useCardData = ({ courseNumber }) => { const { formatMessage } = useIntl(); const { title, bannerUrl } = appHooks.useCardCourseData(courseNumber); - const providerName = appHooks.useCardProviderData(courseNumber).name; return { title, bannerUrl, - providerName: providerName || formatMessage(messages.unknownProviderName), - accessMessage: module.useAccessMessage({ courseNumber }), formatMessage, }; }; diff --git a/src/containers/CourseCard/hooks.test.js b/src/containers/CourseCard/hooks.test.js index 1233d91..429e63d 100644 --- a/src/containers/CourseCard/hooks.test.js +++ b/src/containers/CourseCard/hooks.test.js @@ -1,28 +1,20 @@ import { useIntl } from '@edx/frontend-platform/i18n'; -import { keyStore } from 'utils'; import { hooks as appHooks } from 'data/redux'; import * as hooks from './hooks'; -import messages from './messages'; jest.mock('data/redux', () => ({ hooks: { useCardCourseData: jest.fn(), - useCardCourseRunData: jest.fn(), - useCardEnrollmentData: jest.fn(), - useCardProviderData: jest.fn(), }, })); const courseNumber = 'my-test-course-number'; -const useAccessMessage = 'test-access-message'; -const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage }); -const hookKeys = keyStore(hooks); describe('CourseCard hooks', () => { let out; - const { formatMessage, formatDate } = useIntl(); + const { formatMessage } = useIntl(); beforeEach(() => { jest.clearAllMocks(); }); @@ -32,20 +24,11 @@ describe('CourseCard hooks', () => { title: 'fake-title', bannerUrl: 'my-banner-url', }; - const providerData = { - name: 'my-provider-name', - }; - const runHook = ({ course = {}, provider = {} }) => { - jest.spyOn(hooks, hookKeys.useAccessMessage) - .mockImplementationOnce(mockAccessMessage); + const runHook = ({ course = {} }) => { appHooks.useCardCourseData.mockReturnValueOnce({ ...courseData, ...course, }); - appHooks.useCardProviderData.mockReturnValueOnce({ - ...providerData, - ...provider, - }); out = hooks.useCardData({ courseNumber }); }; beforeEach(() => { @@ -59,80 +42,5 @@ describe('CourseCard hooks', () => { expect(out.title).toEqual(courseData.title); expect(out.bannerUrl).toEqual(courseData.bannerUrl); }); - it('forwards useAccessMessage output, called with courseNumber', () => { - expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber })); - }); - it('forwards provider name if it exists, else formatted unknown provider name', () => { - expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber); - expect(out.providerName).toEqual(providerData.name); - runHook({ provider: { name: '' } }); - expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName)); - }); - }); - describe('useAccessMessage', () => { - const enrollmentData = { - accessExpirationDate: 'test-expiration-date', - isAudit: false, - isAuditAccessExpired: false, - }; - const courseRunData = { - isFinished: false, - endDate: 'test-end-date', - }; - const runHook = ({ enrollment = {}, courseRun = {} }) => { - appHooks.useCardCourseRunData.mockReturnValueOnce({ - ...courseRunData, - ...courseRun, - }); - appHooks.useCardEnrollmentData.mockReturnValueOnce({ - ...enrollmentData, - ...enrollment, - }); - out = hooks.useAccessMessage({ courseNumber }); - }; - it('loads data from enrollment and course run data based on course number', () => { - runHook({}); - expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber); - expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber); - }); - describe('if audit, and expired', () => { - it('returns accessExpired message with accessExpirationDate from cardData', () => { - runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } }); - expect(out).toEqual(formatMessage( - messages.accessExpired, - { accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) }, - )); - }); - }); - - describe('if audit and not expired', () => { - it('returns accessExpires message with accessExpirationDate from cardData', () => { - runHook({ enrollment: { isAudit: true } }); - expect(out).toEqual(formatMessage( - messages.accessExpires, - { accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) }, - )); - }); - }); - - describe('if verified and not ended', () => { - it('returns course ends message with course end date', () => { - runHook({}); - expect(out).toEqual(formatMessage( - messages.courseEnds, - { endDate: formatDate(courseRunData.endDate) }, - )); - }); - }); - - describe('if verified and ended', () => { - it('returns course ended message with course end date', () => { - runHook({ courseRun: { isArchived: true } }); - expect(out).toEqual(formatMessage( - messages.courseEnded, - { endDate: formatDate(courseRunData.endDate) }, - )); - }); - }); }); }); diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx index 3bfd7d2..e9fb85a 100644 --- a/src/containers/CourseCard/index.jsx +++ b/src/containers/CourseCard/index.jsx @@ -15,13 +15,12 @@ import { } from './components/Banners'; import CourseCardActions from './components/CourseCardActions'; import messages from './messages'; +import CourseCardDetails from './components/CourseCardDetails'; export const CourseCard = ({ courseNumber }) => { const { title, bannerUrl, - providerName, - accessMessage, formatMessage, } = useCardData({ courseNumber }); return ( @@ -37,9 +36,7 @@ export const CourseCard = ({ courseNumber }) => { actions={} /> - - {providerName} • {courseNumber} • {accessMessage} - + ({ EntitlementBanner: () => 'EntitlementBanner', })); jest.mock('./components/CourseCardActions', () => 'CourseCardActions'); +jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails'); const dataProps = { title: 'hooks.title', bannerUrl: 'hooks.bannerUrl', - providerName: 'hooks.providerName', - accessMessagE: 'hooks.accessMessage', formatMessage: jest.fn(msg => ({ formatted: msg })), }; diff --git a/src/containers/CourseCard/messages.js b/src/containers/CourseCard/messages.js index 4c60a24..d5f0c53 100644 --- a/src/containers/CourseCard/messages.js +++ b/src/containers/CourseCard/messages.js @@ -1,36 +1,11 @@ import { StrictDict } from 'utils'; export const messages = StrictDict({ - accessExpired: { - id: 'learner-dash.courseCard.accessExpired', - description: 'Course access expiration date message on course card for expired access.', - defaultMessage: 'Access expired {accessExpirationDate}', - }, - accessExpires: { - id: 'learner-dash.courseCard.accessExpires', - description: 'Course access expiration date message on course card.', - defaultMessage: 'Access expires {accessExpirationDate}', - }, - courseEnded: { - id: 'learner-dash.courseCard.courseEnded', - description: 'Course ended message on course card.', - defaultMessage: 'Course ended {endDate}', - }, - courseEnds: { - id: 'learner-dash.courseCard.courseEnds', - description: 'Course ending message on course card.', - defaultMessage: 'Course ends {endDate}', - }, bannerAlt: { id: 'learner-dash.courseCard.bannerAlt', description: 'Course card banner alt-text', defaultMessage: 'Course thumbnail', }, - unknownProviderName: { - id: 'learner-dash.courseCard.unknownProviderName', - description: 'Provider name display when name is unknown', - defaultMessage: 'Unknown', - }, }); export default messages; diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx index b427308..1215ea1 100644 --- a/src/containers/CourseList/index.jsx +++ b/src/containers/CourseList/index.jsx @@ -2,12 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import CourseCard from 'containers/CourseCard'; +import SelectSession from 'containers/SelectSession'; export const CourseList = ({ courseListData }) => (
{courseListData.map((courseNumber) => ( ))} +
); diff --git a/src/containers/SelectSession/SelectSessionModal.jsx b/src/containers/SelectSession/SelectSessionModal.jsx new file mode 100644 index 0000000..72ba23d --- /dev/null +++ b/src/containers/SelectSession/SelectSessionModal.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + ActionRow, Button, Form, ModalDialog, +} from '@edx/paragon'; + +import { nullMethod } from 'hooks'; +import { dateFormatter } from 'utils'; + +import useSelectSession from './hooks'; +import messages from './messages'; + +export const SelectSessionModal = ({ courseNumber }) => { + const { + entitlementSessions, + showSessionModal, + closeSessionModal, + showLeaveSessionInSessionModal, + courseTitle, + } = useSelectSession({ + courseNumber, + }); + + const { formatMessage, formatDate } = useIntl(); + + let header; + let hint; + if (showLeaveSessionInSessionModal) { + header = formatMessage(messages.changeOrLeaveHeader); + hint = formatMessage(messages.changeOrLeaveHint); + } else { + header = formatMessage(messages.selectSessionHeader, { + courseTitle, + }); + hint = formatMessage(messages.selectSessionHint); + } + + return ( + +
+

{header}

+ + {hint} + + {entitlementSessions?.map((entitle) => ( + + {dateFormatter(formatDate, entitle.startDate)} - {dateFormatter(formatDate, entitle.endDate)} + + ))} + {showLeaveSessionInSessionModal ? ( + + {formatMessage(messages.leaveSessionOption)} + + ) : null} + + + + + + +
+
+ ); +}; +SelectSessionModal.propTypes = { + courseNumber: PropTypes.string.isRequired, +}; + +export default SelectSessionModal; diff --git a/src/containers/SelectSession/SelectSessionModal.test.jsx b/src/containers/SelectSession/SelectSessionModal.test.jsx new file mode 100644 index 0000000..f8adb2d --- /dev/null +++ b/src/containers/SelectSession/SelectSessionModal.test.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import hooks from './hooks'; +import SelectSessionModal from './SelectSessionModal'; + +jest.mock('./hooks', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const hookReturn = { + entitlementSessions: [], + showSessionModal: true, + closeSessionModal: jest.fn().mockName('useSelectSession.closeSessionModal'), + showLeaveSessionInSessionModal: true, + courseTitle: 'course-title: unit test save life', +}; + +const courseNumber = 'my-test-course-number'; + +const availableSessions = [ + { startDate: '1/2/2000', endDate: '1/2/2020', courseNumber }, + { startDate: '2/3/2000', endDate: '2/3/2020', courseNumber }, + { startDate: '3/4/2000', endDate: '3/4/2020', courseNumber }, +]; + +describe('SelectSessionModal', () => { + describe('snapshot', () => { + test('empty modal with leave option ', () => { + hooks.mockReturnValueOnce({ + ...hookReturn, + }); + expect(shallow()).toMatchSnapshot(); + }); + + test('modal with leave option ', () => { + hooks.mockReturnValueOnce({ + ...hookReturn, + entitlementSessions: [...availableSessions], + }); + expect(shallow()).toMatchSnapshot(); + }); + + test('modal without leave option ', () => { + hooks.mockReturnValueOnce({ + ...hookReturn, + entitlementSessions: [...availableSessions], + showLeaveSessionInSessionModal: false, + }); + expect(shallow()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap b/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap new file mode 100644 index 0000000..1e0f09b --- /dev/null +++ b/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectSessionModal snapshot empty modal with leave option 1`] = ` + +
+

+ Change or leave session? +

+ + + When you change to a different session any course progress or grades from your current session will be lost. + + + + Leave session + + + + + + + +
+
+`; + +exports[`SelectSessionModal snapshot modal with leave option 1`] = ` + +
+

+ Change or leave session? +

+ + + When you change to a different session any course progress or grades from your current session will be lost. + + + + 1/2/2000 + - + 1/2/2020 + + + 2/3/2000 + - + 2/3/2020 + + + 3/4/2000 + - + 3/4/2020 + + + Leave session + + + + + + + +
+
+`; + +exports[`SelectSessionModal snapshot modal without leave option 1`] = ` + +
+

+ Select a session to access course-title: unit test save life +

+ + + Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement. + + + + 1/2/2000 + - + 1/2/2020 + + + 2/3/2000 + - + 2/3/2020 + + + 3/4/2000 + - + 3/4/2020 + + + + + + + +
+
+`; diff --git a/src/containers/SelectSession/__snapshots__/index.test.jsx.snap b/src/containers/SelectSession/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..e921de5 --- /dev/null +++ b/src/containers/SelectSession/__snapshots__/index.test.jsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectSession snapshot has courseNumber 1`] = ` + +`; + +exports[`SelectSession snapshot no courseNumber 1`] = `""`; diff --git a/src/containers/SelectSession/hooks.js b/src/containers/SelectSession/hooks.js new file mode 100644 index 0000000..e7d686d --- /dev/null +++ b/src/containers/SelectSession/hooks.js @@ -0,0 +1,35 @@ +import { hooks as appHooks, actions } from 'data/redux'; + +import { useDispatch } from 'react-redux'; + +export const useSelectSession = ({ courseNumber }) => { + const dispatch = useDispatch(); + const { + showSessionModal, + showLeaveSessionInSessionModal, + } = appHooks.useSelectSessionsModalData(); + + const { entitlementSessions } = appHooks.useCardEntitlementsData(courseNumber); + + const { title: courseTitle } = appHooks.useCardCourseData(courseNumber); + + const updateSessionModal = (showModal, showLeaveOption = false) => dispatch( + actions.app.updateSelectSessionModal({ + showSessionModal: showModal, + showLeaveSessionInSessionModal: showLeaveOption, + courseNumber, + }), + ); + + return { + showSessionModal, + closeSessionModal: () => updateSessionModal(false), + openSessionModal: () => updateSessionModal(true), + openSessionModalWithLeaveOption: () => updateSessionModal(true, true), + showLeaveSessionInSessionModal, + entitlementSessions, + courseTitle, + }; +}; + +export default useSelectSession; diff --git a/src/containers/SelectSession/hooks.test.js b/src/containers/SelectSession/hooks.test.js new file mode 100644 index 0000000..11a7ba2 --- /dev/null +++ b/src/containers/SelectSession/hooks.test.js @@ -0,0 +1,85 @@ +import { hooks as appHooks, actions } from 'data/redux'; + +import * as hooks from './hooks'; + +jest.mock('data/redux', () => ({ + hooks: { + useCardEntitlementsData: jest.fn(), + useCardCourseData: jest.fn(), + useSelectSessionsModalData: jest.fn(), + }, + actions: { + app: { + updateSelectSessionModal: jest.fn(), + }, + }, +})); + +const courseNumber = 'my-test-course-number'; + +const entitlement = { + showSessionModal: false, + showLeaveSessionInSessionModal: false, +}; + +const availableSessions = [ + { startDate: '1/2/2000', endDate: '1/2/2020', courseNumber }, + { startDate: '2/3/2000', endDate: '2/3/2020', courseNumber }, + { startDate: '3/4/2000', endDate: '3/4/2020', courseNumber }, +]; + +const cardCourseData = { + title: 'course-title: brown fox', +}; + +describe('SelectSessionModal hooks', () => { + let out; + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('useSelectSession', () => { + beforeEach(() => { + appHooks.useSelectSessionsModalData.mockReturnValueOnce({ ...entitlement }); + appHooks.useCardEntitlementsData.mockReturnValueOnce({ entitlementSessions: availableSessions }); + appHooks.useCardCourseData.mockReturnValueOnce({ ...cardCourseData }); + out = hooks.useSelectSession({ courseNumber }); + }); + + test('loads entitlement data based on course number', () => { + expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber); + }); + + test('get course title based on course number', () => { + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber); + expect(out.courseTitle).toEqual(cardCourseData.title); + }); + + test('open session modal', () => { + out.openSessionModal(); + expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({ + showSessionModal: true, + showLeaveSessionInSessionModal: false, + courseNumber, + }); + }); + + test('open session modal with leave option', () => { + out.openSessionModalWithLeaveOption(); + expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({ + showSessionModal: true, + showLeaveSessionInSessionModal: true, + courseNumber, + }); + }); + + test('close session modal', () => { + out.closeSessionModal(); + expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({ + showSessionModal: false, + showLeaveSessionInSessionModal: false, + courseNumber, + }); + }); + }); +}); diff --git a/src/containers/SelectSession/index.jsx b/src/containers/SelectSession/index.jsx new file mode 100644 index 0000000..f4cddca --- /dev/null +++ b/src/containers/SelectSession/index.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { hooks as appHooks } from 'data/redux'; + +import SelectSessionModal from './SelectSessionModal'; + +export const SelectSession = () => { + const { courseNumber } = appHooks.useSelectSessionsModalData(); + return courseNumber ? : null; +}; +SelectSession.propTypes = {}; + +export default SelectSession; diff --git a/src/containers/SelectSession/index.test.jsx b/src/containers/SelectSession/index.test.jsx new file mode 100644 index 0000000..8200c0d --- /dev/null +++ b/src/containers/SelectSession/index.test.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { hooks as appHooks } from 'data/redux'; + +import SelectSession from '.'; + +jest.mock('data/redux', () => ({ + hooks: { + useSelectSessionsModalData: jest.fn(), + }, +})); + +describe('SelectSession', () => { + describe('snapshot', () => { + test('no courseNumber', () => { + appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: null }); + expect(shallow()).toMatchSnapshot(); + }); + + test('has courseNumber', () => { + appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: 'some course' }); + expect(shallow()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/containers/SelectSession/messages.js b/src/containers/SelectSession/messages.js new file mode 100644 index 0000000..9fa5ed3 --- /dev/null +++ b/src/containers/SelectSession/messages.js @@ -0,0 +1,42 @@ +/* eslint-disable quotes */ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + changeOrLeaveHeader: { + id: 'learner-dash.selectSession.changeOrLeaveHeader', + description: 'Header for session that allow leave option', + defaultMessage: 'Change or leave session?', + }, + selectSessionHeader: { + id: 'learner-dash.selectSession.selectSessionHeader', + description: 'Header for unfulfilled entitlement', + defaultMessage: 'Select a session to access {courseTitle}', + }, + changeOrLeaveHint: { + id: 'learner-dash.selectSession.changeOrLeaveHint', + description: 'Hint for session that allow leave option', + defaultMessage: 'When you change to a different session any course progress or grades from your current session will be lost.', + }, + selectSessionHint: { + id: 'learner-dash.selectSession.selectSessionHint', + description: 'Hint for session that does not allow leave option', + defaultMessage: 'Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement.', + }, + leaveSessionOption: { + id: 'learner-dash.selectSession.leaveSessionOption', + description: 'Radio option for leave session', + defaultMessage: 'Leave session', + }, + nevermind: { + id: 'learner-dash.selectSession.nevermind', + description: 'Cancel action for select session modal', + defaultMessage: 'Nevermind', + }, + confirmSession: { + id: 'learner-dash.selectSession.confirmSession', + description: 'Confirm action for select session modal', + defaultMessage: 'Confirm Session', + }, +}); + +export default messages; diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js index 231d1cd..ac44d28 100644 --- a/src/data/redux/app/reducer.js +++ b/src/data/redux/app/reducer.js @@ -10,6 +10,7 @@ const initialState = { platformSettings: {}, suggestedCourses: [], filterState: {}, + selectSessionsModal: {}, }; // eslint-disable-next-line no-unused-vars @@ -41,6 +42,12 @@ const app = createSlice({ platformSettings: payload.platformSettings, suggestedCourses: payload.suggestedCourses, }), + updateSelectSessionModal: (state, { payload }) => ({ + ...state, + selectSessionsModal: { + ...payload, + }, + }), }, }); diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js index f46b2fb..4db6537 100644 --- a/src/data/redux/app/reducer.test.js +++ b/src/data/redux/app/reducer.test.js @@ -12,13 +12,13 @@ describe('app reducer', () => { }, entitlements: [], }; - const testValue = 'my-test-value'; - const testAction = (action, expected) => { - expect(reducer(testState, action)).toEqual({ - ...testState, - ...expected, - }); - }; + // const testValue = 'my-test-value'; + // const testAction = (action, expected) => { + // expect(reducer(testState, action)).toEqual({ + // ...testState, + // ...expected, + // }); + // }; describe('action handlers', () => { describe('loadCourses', () => { const courseIds = [ diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index be393cf..fdae538 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -17,6 +17,7 @@ export const simpleSelectors = { suggestedCourses: mkSimpleSelector(app => app.suggestedCourses), emailConfirmation: mkSimpleSelector(app => app.emailConfirmation), enterpriseDashboards: mkSimpleSelector(app => app.enterpriseDashboards), + selectSessionsModal: mkSimpleSelector(app => app.selectSessionsModal), }; export const courseCardData = (state, courseNumber) => ( @@ -68,6 +69,7 @@ export const courseCard = StrictDict({ const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow; return { canChange: entitlements.canChange, + canViewCourse: entitlements.canViewCourse, entitlementSessions: entitlements.availableSessions, isEntitlement: entitlements.isEntitlement, isExpired: entitlements.isExpired, diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js index ef915e9..7135a32 100644 --- a/src/data/redux/hooks.js +++ b/src/data/redux/hooks.js @@ -9,6 +9,7 @@ export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpr export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings); // suggested courses is max at 3 at the moment. export const useSuggestedCoursesData = () => useSelector(appSelectors.suggestedCourses).slice(0, 3); +export const useSelectSessionsModalData = () => useSelector(appSelectors.selectSessionsModal); // eslint-disable-next-line export const useCourseCardData = (selector) => (courseNumber) => useSelector( diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js index 995aec0..2f8000e 100644 --- a/src/data/services/lms/fakeData/courses.js +++ b/src/data/services/lms/fakeData/courses.js @@ -289,6 +289,7 @@ export const courseRuns = [ canChange: true, changeDeadline: futureDate, isExpired: false, + availableSessions, }, }, // Entitlement Course Run - Can View and Change @@ -305,6 +306,7 @@ export const courseRuns = [ canChange: true, changeDeadline: futureDate, isExpired: false, + availableSessions, }, }, // Entitlement Course Run - Can View but not Change @@ -345,6 +347,10 @@ export const courseRuns = [ }, ]; +// unfulfilled entitlement select session +// unfulfilled entitlement select session with deadline +// unfulfilled entitlement select session pass deadline with available session {banner different from 4th} +// unfulfilled entitlement select session pass deadline without available session export const entitlementCourses = [ { entitlements: { diff --git a/src/setupTest.jsx b/src/setupTest.jsx index 2629fd3..a092edf 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -19,7 +19,7 @@ jest.mock('@edx/frontend-platform/i18n', () => { const i18n = jest.requireActual('@edx/frontend-platform/i18n'); const PropTypes = jest.requireActual('prop-types'); const { formatMessage } = jest.requireActual('./testUtils'); - const formatDate = jest.fn(date => date).mockName('useIntl.formatDate'); + const formatDate = jest.fn(date => new Date(date).toLocaleDateString()).mockName('useIntl.formatDate'); return { ...i18n, intlShape: PropTypes.shape({ @@ -133,8 +133,6 @@ jest.mock('hooks', () => ({ nullMethod: jest.fn().mockName('hooks.nullMethod'), })); -jest.mock('@zip.js/zip.js', () => ({})); - // Mock react-redux hooks // unmock for integration tests jest.mock('react-redux', () => { @@ -154,3 +152,10 @@ jest.mock('hooks', () => ({ ...jest.requireActual('hooks'), nullMethod: jest.fn().mockName('hooks.nullMethod'), })); + +jest.mock('moment', () => ({ + __esModule: true, + default: (date) => ({ + toDate: jest.fn().mockReturnValue(date), + }), +})); diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx index 2d2f78a..e12f513 100644 --- a/src/test/app.test.jsx +++ b/src/test/app.test.jsx @@ -118,14 +118,13 @@ describe('ESG app integration tests', () => { inspector = new Inspector(el); }); - test('initialization', async (done) => { + test('initialization', async () => { await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending); resolveFns.init.success(); await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed); - done(); }); - test('course cards', async (done) => { + test('course cards', async () => { resolveFns.init.success(); await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed); await inspector.findByText(fakeData.courseRunData[0].course.title); @@ -152,11 +151,9 @@ describe('ESG app integration tests', () => { [ courseData.provider.name, courseNumber, - appMessages.withValues.CourseCard.accessExpires({ + appMessages.withValues.CourseCardDetails.accessExpires({ accessExpirationDate: courseData.enrollment.accessExpirationDate, }), ].forEach(value => inspector.verifyTextIncludes(cardDetails, value)); - - done(); }); }); diff --git a/src/test/messages.js b/src/test/messages.js index 3f00630..16bf61c 100644 --- a/src/test/messages.js +++ b/src/test/messages.js @@ -1,4 +1,4 @@ -import CourseCard from 'containers/CourseCard/messages'; +import CourseCardDetails from 'containers/CourseCard/components/CourseCardDetails/messages'; const mapMessages = (messages) => Object.keys(messages).reduce( (acc, key) => ({ ...acc, [key]: messages[key].defaultMessage }), @@ -22,8 +22,8 @@ const mapMessagesWithValues = (messages) => Object.keys(messages).reduce( ); export default { - CourseCard: mapMessages(CourseCard), + CourseCardDetails: mapMessages(CourseCardDetails), withValues: { - CourseCard: mapMessagesWithValues(CourseCard), + CourseCardDetails: mapMessagesWithValues(CourseCardDetails), }, }; diff --git a/src/utils/dateFormatter.js b/src/utils/dateFormatter.js new file mode 100644 index 0000000..c9f007b --- /dev/null +++ b/src/utils/dateFormatter.js @@ -0,0 +1,5 @@ +import moment from 'moment'; + +const dateFormatter = (formatDate, date) => formatDate(moment(date).toDate(), { year: 'numeric', month: 'long', day: '2-digit' }); + +export default dateFormatter; diff --git a/src/utils/index.js b/src/utils/index.js index 6108258..0db13b4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,2 +1,3 @@ export { default as StrictDict } from './StrictDict'; export { default as keyStore } from './keyStore'; +export { default as dateFormatter } from './dateFormatter';