From 7a46b3c2a83085006a5dbdd8b8bf800c5bcfa8e2 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 22 Jul 2022 01:38:21 -0400 Subject: [PATCH] chore: test and api updates --- .../components/Banners/CertificateBanner.jsx | 49 +++++--- .../Banners/CertificateBanner.test.jsx | 111 ++++++++++++++++++ .../components/Banners/EntitlementBanner.jsx | 39 ++++-- .../Banners/EntitlementBanner.test.jsx | 69 +++++++++++ .../CertificateBanner.test.jsx.snap | 33 ++++++ .../EntitlementBanner.test.jsx.snap | 60 ++++++++++ .../CourseCard/components/Banners/messages.js | 62 +++++++++- src/containers/CourseCard/hooks.test.js | 2 +- src/data/redux/app/hooks.js | 23 ---- src/data/redux/app/reducer.js | 26 ++-- src/data/redux/app/reducer.test.js | 61 ++++++---- src/data/redux/app/selectors.js | 29 +++-- src/data/redux/thunkActions/app.js | 6 +- src/data/services/lms/api.js | 2 +- src/data/services/lms/fakeData/courses.js | 69 ++++++++--- src/setupTest.jsx | 3 +- src/test/app.test.jsx | 4 +- 17 files changed, 538 insertions(+), 110 deletions(-) create mode 100644 src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx create mode 100644 src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx create mode 100644 src/containers/CourseCard/components/Banners/__snapshots__/CertificateBanner.test.jsx.snap create mode 100644 src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap delete mode 100644 src/data/redux/app/hooks.js diff --git a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx index 3ef40e4..9ff06f6 100644 --- a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Hyperlink } from '@edx/paragon'; +import { MailtoLink, Hyperlink } from '@edx/paragon'; import { CheckCircle } from '@edx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -19,61 +19,76 @@ export const CertificateBanner = ({ courseNumber }) => { hasFinished, } = appHooks.useCardEnrollmentData(courseNumber); const { isPassing } = appHooks.useCardGradeData(courseNumber); - const { minPassingGrade } = appHooks.useCardCourseRunData(courseNumber); + const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(courseNumber); + const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData(); const { formatMessage } = useIntl(); + const emailLink = address => address && {address}; + if (certificate.isRestricted) { return ( - {formatMessage(messages.certRestricted)} - info@example.com - {isVerified && ( - <> - If you would like a refund on your Certificate of Achievement, please contact our billing address billing@example.com - + {formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })} + {isVerified && ' '} + {isVerified && formatMessage( + messages.certRefundContactBilling, + { billingEmail: emailLink(billingEmail) }, )} ); } if (!isPassing) { if (isAudit) { - return ( Grade required to pass the course: {minPassingGrade}% ); + return ( + + {formatMessage(messages.passingGrade, { minPassingGrade })} + + ); } if (hasFinished) { return ( - You are not eligible for a certificate. View grades. + {formatMessage(messages.notEligibleForCert)}. + {' '} + {formatMessage(messages.viewGrades)} ); } return ( - Grade required for a certificate: {minPassingGrade}% + {formatMessage(messages.certMinGrade, { minPassingGrade })} ); } if (certificate.isDownloadable) { - if (certificate.previewUrl) { + if (certificate.certPreviewUrl) { return ( - Congratulations. Your certificate is ready. + {formatMessage(messages.certReady)} {' '} - View Certificate. + + {formatMessage(messages.viewCertificate)} + ); } return ( - Congratulations. Your certificate is ready. + {formatMessage(messages.certReady)} {' '} - Download Certificate. + + {formatMessage(messages.downloadCertificate)} + ); } if (certificate.isEarnedButUnavailable) { return ( - Your grade and certificate will be ready after {certificate.availableDate}. + {formatMessage( + messages.gradeAndCertReadyAfter, + { availableDate: certificate.availableDate }, + )} ); } diff --git a/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx b/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx new file mode 100644 index 0000000..24a23fa --- /dev/null +++ b/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Hyperlink } from '@edx/paragon'; + +import { hooks as appHooks } from 'data/redux'; +import { CourseBanner } from './CourseBanner'; + +import messages from './messages'; + +jest.mock('components/Banner', () => 'Banner'); +jest.mock('data/redux', () => ({ + hooks: { + useCardCourseData: jest.fn(), + useCardCourseRunData: jest.fn(), + useCardEnrollmentData: jest.fn(), + }, +})); + +const courseNumber = 'my-test-course-number'; + +let el; + +const enrollmentData = { + isVerified: false, + canUpgrade: false, + isAuditAccessExpired: false, +}; +const courseRunData = { + isActive: false, +}; +const courseData = { + website: 'test-course-website', +}; + +const render = (overrides = {}) => { + const { + course = {}, + courseRun = {}, + enrollment = {}, + } = overrides; + appHooks.useCardCourseData.mockReturnValueOnce({ + ...courseData, + ...course, + }); + appHooks.useCardCourseRunData.mockReturnValueOnce({ + ...courseRunData, + ...courseRun, + }); + appHooks.useCardEnrollmentData.mockReturnValueOnce({ + ...enrollmentData, + ...enrollment, + }); + el = shallow(); +}; + +describe('CourseBanner', () => { + it('initializes data with course number from enrollment, course and course run data', () => { + render(); + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber); + }); + test('no display if learner is verified', () => { + render({ enrollment: { isVerified: true } }); + expect(el.isEmptyRender()).toEqual(true); + }); + describe('audit access expired, can upgrade', () => { + beforeEach(() => { + render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } }); + }); + test('snapshot: (auditAccessExpired, upgradeToAccess)', () => { + expect(el).toMatchSnapshot(); + }); + test('messages: (auditAccessExpired, upgradeToAccess)', () => { + expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage); + expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage); + }); + }); + describe('audit access expired, cannot upgrade', () => { + beforeEach(() => { + render({ enrollment: { isAuditAccessExpired: true } }); + }); + test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => { + expect(el).toMatchSnapshot(); + }); + test('messages: (auditAccessExpired, upgradeToAccess)', () => { + expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage); + expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage); + }); + }); + describe('course run active and cannot upgrade', () => { + beforeEach(() => { + render({ courseRun: { isActive: true } }); + }); + test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => { + expect(el).toMatchSnapshot(); + }); + test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => { + expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage); + const link = el.find(Hyperlink); + expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage); + expect(link.props().destination).toEqual(courseData.website); + }); + }); + test('no display if audit access not expired and (course is not active or can upgrade)', () => { + render(); + expect(el.isEmptyRender()).toEqual(true); + render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } }); + expect(el.isEmptyRender()).toEqual(true); + }); +}); diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx index 8c24139..771e1c4 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx @@ -1,27 +1,52 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, MailtoLink } from '@edx/paragon'; import { hooks as appHooks } from 'data/redux'; import Banner from 'components/Banner'; +import messages from './messages'; export const EntitlementBanner = ({ courseNumber }) => { const { - canChange, isEntitlement, - isExpired, + hasSessions, isFulfilled, + changeDeadline, + showExpirationWarning, } = appHooks.useCardEntitlementsData(courseNumber); + const { supportEmail } = appHooks.usePlatformSettingsData(); + const { formatDate, formatMessage } = useIntl(); if (!isEntitlement) { return null; } - if (isExpired || isFulfilled) { - return null; + + if (!hasSessions && !isFulfilled) { + return ( + + {formatMessage(messages.entitlementsUnavailable, { + emailLink: supportEmail && {supportEmail}, + })} + + ); } - return canChange - ? (You must select a session to access the course.) - : (The deadline to select a session has passed); + if (showExpirationWarning) { + return ( + + {formatMessage(messages.entitlementsExpiringSoon, { + changeDeadline: formatDate(changeDeadline), + selectSessionButton: ( + + ), + })} + + ); + } + return null; }; EntitlementBanner.propTypes = { courseNumber: PropTypes.string.isRequired, diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx new file mode 100644 index 0000000..b805750 --- /dev/null +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { hooks as appHooks } from 'data/redux'; +import EntitlementBanner from './EntitlementBanner'; + +jest.mock('@edx/frontend-platform/i18n', () => ({ + useIntl: jest.fn(), +})); +jest.mock('components/Banner', () => 'Banner'); +jest.mock('data/redux', () => ({ + hooks: { + usePlatformSettingsData: jest.fn(), + useCardEntitlementsData: jest.fn(), + }, +})); + +const courseNumber = 'my-test-course-number'; + +let el; + +const entitlementsData = { + isEntitlement: true, + hasSessions: true, + isFulfilled: false, + changeDeadline: 'test-deadline', + showExpirationWarning: false, +}; +const platformData = { supportEmail: 'test-support-email' }; + +const render = (overrides = {}) => { + const { entitlements = {} } = overrides; + appHooks.useCardEntitlementsData.mockReturnValueOnce({ ...entitlementsData, ...entitlements }); + appHooks.usePlatformSettingsData.mockReturnValueOnce(platformData); + el = shallow(); +}; + +describe('EntitlementBanner', () => { + beforeEach(() => { + useIntl.mockReturnValue({ + formatDate: (date) => date, + formatMessage: (message, values) =>
, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('initializes data with course number from entitlements', () => { + render(); + expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber); + }); + test('no display if not an entitlement', () => { + render({ entitlements: { isEntitlement: false } }); + expect(el.isEmptyRender()).toEqual(true); + }); + test('snapshot: no sessions available', () => { + render({ entitlements: { isFulfilled: false, hasSessions: false } }); + expect(el).toMatchSnapshot(); + }); + test('snapshot: expiration warning', () => { + render({ entitlements: { showExpirationWarning: true } }); + expect(el).toMatchSnapshot(); + }); + test('no display if sessions available and not displaying warning', () => { + render(); + expect(el.isEmptyRender()).toEqual(true); + }); +}); diff --git a/src/containers/CourseCard/components/Banners/__snapshots__/CertificateBanner.test.jsx.snap b/src/containers/CourseCard/components/Banners/__snapshots__/CertificateBanner.test.jsx.snap new file mode 100644 index 0000000..5c3160a --- /dev/null +++ b/src/containers/CourseCard/components/Banners/__snapshots__/CertificateBanner.test.jsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = ` + + Your audit access to this course has expired. + + Upgrade now to access your course again. + +`; + +exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = ` + + Your audit access to this course has expired. + + + Find another course + + +`; + +exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = ` + + Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future. + + + Explore course details. + + +`; diff --git a/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap new file mode 100644 index 0000000..cad1507 --- /dev/null +++ b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EntitlementBanner snapshot: expiration warning 1`] = ` + +
+
+ , + } + } + /> + +`; + +exports[`EntitlementBanner snapshot: no sessions available 1`] = ` + +
+ test-support-email + , + } + } + /> + +`; diff --git a/src/containers/CourseCard/components/Banners/messages.js b/src/containers/CourseCard/components/Banners/messages.js index d6889c4..052d800 100644 --- a/src/containers/CourseCard/components/Banners/messages.js +++ b/src/containers/CourseCard/components/Banners/messages.js @@ -29,7 +29,67 @@ export const messages = StrictDict({ certRestricted: { id: 'learner-dash.courseCard.banners.certificateRestricted', description: 'Restricted certificate warning message', - defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting ', + defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.', + }, + certRefundContactBilling: { + id: 'learner-dash.courseCard.banners.certificateRefundContactBilling', + description: 'Message to learners to contact billing for certificate refunds', + defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}', + }, + passingGrade: { + id: 'learner-dash.courseCard.banners.passingGrade', + description: 'Message to learners with minimum passing grade for the course', + defaultMessage: 'Grade required to pass the course: {minPassingGrade}', + }, + notEligibleForCert: { + id: 'learner-dash.courseCard.banners.notEligibleForCert', + description: 'Certificate inelligibility message', + defaultMessage: 'You are not eligible for a certificate.', + }, + viewGrades: { + id: 'learner-dash.courseCard.banners.viewGrades', + description: 'Gradses link text', + defaultMessage: 'View grades.', + }, + certReady: { + id: 'learner-dash.courseCard.banners.certReady', + description: 'Certificate ready message', + defaultMessage: 'Congratulations. Your certificate is ready.', + }, + viewCertificate: { + id: 'learner-dash.courseCard.banners.viewCertificate', + description: 'Certificate link text', + defaultMessage: 'View Certificate.', + }, + certMinGrade: { + id: 'learner-dash.courseCard.banners.certMinGrade', + description: 'Passing grade requirement message', + defaultMessage: 'Grade required for a certificate: {minPassingGrade}', + }, + downloadCertificate: { + id: 'learner-dash.courseCard.banners.downloadCertificate', + description: 'Certificate download link text', + defaultMessage: 'Download Certificate.', + }, + gradeAndCertReadyAfter: { + id: 'learner-dash.courseCard.banners.gradseAndCertReadyAfter', + description: 'Grade and certificate availability date message', + defaultMessage: 'Your grade and certificate will be ready after {availableDate}.', + }, + entitlementsUnavailable: { + id: 'learner-dash.courseCard.banners.entitlementsUnavailable', + description: 'Entitlements course message when no sessions are available', + defaultMessage: 'There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.', + }, + entitlementsExpiringSoon: { + id: 'learner-dash.courseCard.banners.entitlementsExpiringSoon', + description: 'Entitlements course message when the entitlement is expiring soon.', + defaultMessage: 'You must {selectSessionButton} by {changeDeadline} to access the course.', + }, + selectSession: { + id: 'learner-dash.courseCard.banners.selectSession', + description: 'Entitlements session selection link text', + defaultMessage: 'select a session', }, }); diff --git a/src/containers/CourseCard/hooks.test.js b/src/containers/CourseCard/hooks.test.js index a537bac..1233d91 100644 --- a/src/containers/CourseCard/hooks.test.js +++ b/src/containers/CourseCard/hooks.test.js @@ -127,7 +127,7 @@ describe('CourseCard hooks', () => { describe('if verified and ended', () => { it('returns course ended message with course end date', () => { - runHook({ courseRun: { isFinished: true } }); + runHook({ courseRun: { isArchived: true } }); expect(out).toEqual(formatMessage( messages.courseEnded, { endDate: formatDate(courseRunData.endDate) }, diff --git a/src/data/redux/app/hooks.js b/src/data/redux/app/hooks.js deleted file mode 100644 index 09c9de6..0000000 --- a/src/data/redux/app/hooks.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useSelector } from 'react-redux'; - -import selectors from './selectors'; - -const { courseCard } = selectors; - -export const useEmailConfirmationData = () => useSelector(selectors.emailConfirmation); -export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboards); -export const usePlatformSettingsData = () => useSelector(selectors.platformSettings); - -// eslint-disable-next-line -export const useCourseCardData = (selector) => (courseNumber) => useSelector( - (state) => selector(selectors.courseData(state)[courseNumber]), -); - -export const useCardCertificateData = useCourseCardData(courseCard.certificates); -export const useCardCourseData = useCourseCardData(courseCard.course); -export const useCardCourseRunData = useCourseCardData(courseCard.courseRun); -export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment); -export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements); -export const useCardGradesData = useCourseCardData(courseCard.grades); -export const useCardProviderData = useCourseCardData(courseCard.provider); -export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms); diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js index dec92e8..0ef8192 100644 --- a/src/data/redux/app/reducer.js +++ b/src/data/redux/app/reducer.js @@ -9,6 +9,7 @@ const initialState = { enterpriseDashboards: {}, platformSettings: {}, suggestedCourses: {}, + filterState: {}, }; // eslint-disable-next-line no-unused-vars @@ -16,18 +17,23 @@ const app = createSlice({ name: 'app', initialState, reducers: { - loadEnrollments: (state, { payload }) => ({ + loadCourses: (state, { payload: { enrollments, entitlements } }) => ({ ...state, - enrollments: payload.map(curr => curr.courseRun.courseNumber), - courseData: payload.reduce( - (obj, curr) => ({ - ...obj, - [curr.courseRun.courseNumber]: curr, - }), - {}, - ), + enrollments: [ + ...enrollments.map(curr => curr.courseRun.courseNumber), + ...entitlements.map(curr => curr.courseRun.courseNumber), + ], + courseData: { + ...entitlements.reduce( + (obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }), + {}, + ), + ...enrollments.reduce( + (obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }), + {}, + ), + }, }), - loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }), loadGlobalData: (state, { payload }) => ({ ...state, emailConfirmation: payload.emailConfirmation, diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js index 549ad75..f46b2fb 100644 --- a/src/data/redux/app/reducer.test.js +++ b/src/data/redux/app/reducer.test.js @@ -20,45 +20,66 @@ describe('app reducer', () => { }); }; describe('action handlers', () => { - test('loadEntitlements loads entitlements from payload', () => { - testAction( - actions.loadEntitlements(testValue), - { entitlements: testValue }, - ); - }); - describe('loadEnrollments', () => { - const enrollments = [ + describe('loadCourses', () => { + const courseIds = [ 'course-1', 'course-2', 'course-3', ]; - const courseData = { - [enrollments[0]]: { - courseRun: { courseNumber: enrollments[0] }, + const entitlementIds = [ + 'entitlement-course-1', + 'entitlement-course-2', + ]; + const enrollmentData = [ + { + courseRun: { courseNumber: courseIds[0] }, course: 1, some: 'data', }, - [enrollments[1]]: { - courseRun: { courseNumber: enrollments[1] }, + { + courseRun: { courseNumber: courseIds[1] }, course: 2, some: 'other data', }, - [enrollments[2]]: { - courseRun: { courseNumber: enrollments[2] }, + { + courseRun: { courseNumber: courseIds[2] }, course: 3, some: 'still different data', }, - }; - const enrollmentData = enrollments.map(v => courseData[v]); + ]; + const entitlementData = [ + { + courseRun: { courseNumber: entitlementIds[0] }, + course: 4, + some: 'STILL different data', + }, + { + courseRun: { courseNumber: entitlementIds[1] }, + course: 5, + some: 'still DIFFERENT data', + }, + ]; let out; beforeEach(() => { - out = reducer(testState, actions.loadEnrollments(enrollmentData)); + out = reducer(testState, actions.loadCourses({ + enrollments: enrollmentData, + entitlements: entitlementData, + })); }); it('loads list of courseRun ids into enrollments field', () => { - expect(out.enrollments).toEqual(enrollments); + expect(out.enrollments).toEqual([ + ...courseIds, + ...entitlementIds, + ]); }); it('loads object keyed by courseRun ids into courseData field', () => { - expect(out.courseData).toEqual(courseData); + expect(out.courseData).toEqual({ + [courseIds[0]]: enrollmentData[0], + [courseIds[1]]: enrollmentData[1], + [courseIds[2]]: enrollmentData[2], + [entitlementIds[0]]: entitlementData[0], + [entitlementIds[1]]: entitlementData[1], + }); }); }); }); diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index 7923f43..3d61ee7 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -26,11 +26,15 @@ const mkCardSelector = (sel) => (state, courseNumber) => ( sel(courseCardData(state, courseNumber)) ); +const dateSixMonthsFromNow = new Date(); +dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180); + export const courseCard = StrictDict({ certificates: mkCardSelector(({ certificates }) => ({ availableDate: certificates.availableDate, - downloadUrl: certificates.downloadUrls?.download, - previewUrl: certificates.downloadUrls?.preview, + certDownloadUrl: certificates.certDownloadUrl, + honorCertDownloadUrl: certificates.honorCertDownloadUrl, + certPreviewUrl: certificates.certPreviewUrl, isDownloadable: certificates.isDownloadable, isEarnedButUnavailable: certificates.isEarned && !certificates.isAvailable, isRestricted: certificates.isRestricted, @@ -58,13 +62,20 @@ export const courseCard = StrictDict({ lastEnrolled: enrollment.lastEnrollment, isEnrolled: enrollment.isEnrolled, })), - entitlements: mkCardSelector(({ entitlements }) => ({ - canChange: entitlements.canChange, - entitlementSessions: entitlements.availableSessions, - isEntitlement: entitlements.isEntitlement, - isExpired: entitlements.isExpired, - isFulfilled: entitlements.isFulfilled, - })), + entitlements: mkCardSelector(({ entitlements }) => { + const deadline = new Date(entitlements.changeDeadline); + const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow; + return { + canChange: entitlements.canChange, + entitlementSessions: entitlements.availableSessions, + isEntitlement: entitlements.isEntitlement, + isExpired: entitlements.isExpired, + isFulfilled: entitlements.isFulfilled, + hasSessions: entitlements.availableSessions?.length > 0, + changeDeadline: entitlements.changeDeadline, + showExpirationWarning, + }; + }), grades: mkCardSelector(({ grades }) => ({ isPassing: grades.isPassing })), provider: mkCardSelector(({ provider }) => ({ name: provider?.name })), relatedPrograms: mkCardSelector(({ relatedPrograms }) => ({ diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js index 6bc34dc..196f085 100644 --- a/src/data/redux/thunkActions/app.js +++ b/src/data/redux/thunkActions/app.js @@ -15,8 +15,7 @@ import requests from './requests'; export const initialize = () => (dispatch) => ( dispatch(requests.initializeList({ onSuccess: (({ enrollments, entitlements, ...globalData }) => { - dispatch(actions.app.loadEnrollments(enrollments)); - dispatch(actions.app.loadEntitlements(entitlements)); + dispatch(actions.app.loadCourses({ enrollments, entitlements })); dispatch(actions.app.loadGlobalData(globalData)); }), })) @@ -25,8 +24,7 @@ export const initialize = () => (dispatch) => ( export const refreshList = () => (dispatch) => ( dispatch(requests.initializeList({ onSuccess: (({ enrollments, entitlements, ...globalData }) => { - dispatch(actions.app.loadEnrollments(enrollments)); - dispatch(actions.app.loadEntitlements(entitlements)); + dispatch(actions.app.loadCourses({ enrollments, entitlements })); dispatch(actions.app.loadGlobalData(globalData)); }), })) diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index 0556f6f..e1ba3b8 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -17,7 +17,7 @@ import { *********************************************************************************/ const initializeList = () => Promise.resolve({ enrollments: fakeData.courseRunData, - entitlements: fakeData.entitlementCourses, + entitlements: fakeData.entitlementData, ...fakeData.globalData, }); diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js index da83787..23d2ad4 100644 --- a/src/data/services/lms/fakeData/courses.js +++ b/src/data/services/lms/fakeData/courses.js @@ -48,6 +48,9 @@ const logos = { const pastDate = '11/11/2000'; const futureDate = '11/11/3030'; +const soonDate = new Date(); +soonDate.setDate(soonDate.getDate() + 60); +const soonDateStr = soonDate.toDateString(); const globalData = { emailConfirmation: { @@ -100,7 +103,9 @@ export const genCertificateData = (data = {}) => ({ isAvailable: false, isEarned: false, isDownloadable: false, - downloadUrls: null, // { preview, download } + certPreviewUrl: 'edx.com/courses/my-course-url/cert-preview', + certDownloadUrl: 'edx.com/courses/my-course-url/cert-download', + honorCertDownloadUrl: 'edx.com/courses/my-course-url/honor-cert-download', ...data, }); @@ -224,10 +229,8 @@ export const courseRuns = [ isAvailable: true, isDownloadable: true, availableDate: pastDate, - downloadUrls: { - preview: logos.edx, - download: logos.social, - }, + certDownloadUrl: logos.social, + certPreviewUrl: logos.edx, }), entitlements: { isEntitlement: false }, }, @@ -241,9 +244,7 @@ export const courseRuns = [ isAvailable: true, isDownloadable: true, availableDate: pastDate, - downloadUrls: { - download: logos.social, - }, + certDownloadUrl: logos.social, }), entitlements: { isEntitlement: false }, }, @@ -319,7 +320,6 @@ export const courseRuns = [ export const entitlementCourses = [ { - course: { title: genCourseTitle(100) }, entitlements: { isEntitlement: true, availableSessions, @@ -331,7 +331,17 @@ export const entitlementCourses = [ isExpired: false, }, }, { - course: { title: genCourseTitle(101) }, + entitlements: { + isEntitlement: true, + availableSessions, + isRefundable: true, + isFulfilled: false, + canViewCourse: false, + changeDeadline: soonDateStr, + canChange: true, + isExpired: false, + }, + }, { entitlements: { isEntitlement: true, availableSessions, @@ -343,10 +353,9 @@ export const entitlementCourses = [ isExpired: false, }, }, { - course: { title: genCourseTitle(102) }, entitlements: { isEntitlement: true, - availableSessions, + availableSessions: [], isRefundable: true, isFulfilled: false, canViewCourse: false, @@ -387,13 +396,45 @@ export const courseRunData = courseRuns.map( ...data, courseRun: genCourseRunData({ ...data.courseRun, courseNumber }), ...iteratedData[providerIndex], - credit: { isPurchased: false, requestStatus: null }, + }; + }, +); + +export const entitlementData = entitlementCourses.map( + (data, index) => { + const title = genCourseTitle(100 + index); + const courseNumber = genCourseID(100 + index); + const providerIndex = index % 3; + const iteratedData = [ + { + provider: providers.edx, + course: { title, bannerUrl: logos.edx }, + relatedPrograms, + }, + { + provider: providers.mit, + course: { title, bannerUrl: logos.science }, + relatedPrograms: [relatedPrograms[0]], + }, + { + provider: null, + course: { title, bannerUrl: logos.social }, + relatedPrograms: [], + }, + ]; + return { + ...data, + enrollment: genEnrollmentData(), + grades: { isPassing: true }, + certificates: genCertificateData(), + courseRun: genCourseRunData({ ...data.courseRun, courseNumber }), + ...iteratedData[providerIndex], }; }, ); export default { courseRunData, - entitlementCourses, + entitlementData, globalData, }; diff --git a/src/setupTest.jsx b/src/setupTest.jsx index 8b8d4d9..c613c94 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().mockName('useIntl.formatDate'); + const formatDate = jest.fn(date => date).mockName('useIntl.formatDate'); return { ...i18n, intlShape: PropTypes.shape({ @@ -86,6 +86,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon Hyperlink: 'Hyperlink', Icon: 'Icon', IconButton: 'IconButton', + MailtoLink: 'MailtoLink', ModalDialog: { Header: 'ModalDialog.Header', Body: 'ModalDialog.Body', diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx index 7c4f801..2d2f78a 100644 --- a/src/test/app.test.jsx +++ b/src/test/app.test.jsx @@ -80,7 +80,8 @@ const mockApi = () => { resolveFns.init = { success: () => resolve({ enrollments: fakeData.courseRunData, - entitlements: fakeData.entitlementCourses, + entitlements: fakeData.entitlementData, + ...fakeData.globalData, }), }; })); @@ -148,7 +149,6 @@ describe('ESG app integration tests', () => { ); cardDetails = inspector.get.card.details(card); - console.log({ enrollment: courseData.enrollment }); [ courseData.provider.name, courseNumber,