diff --git a/src/App.scss b/src/App.scss index 76c9a8b..747472b 100755 --- a/src/App.scss +++ b/src/App.scss @@ -12,6 +12,9 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4. @import "~@edx/frontend-component-footer/dist/_footer"; #root { + input[type=checkbox] { + transform: none; + } display: flex; flex-direction: column; min-height: 100vh; diff --git a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap index f09d2db..e9c6a51 100644 --- a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap @@ -2,7 +2,7 @@ exports[`CourseCard component snapshot 1`] = `
} title={ @@ -37,19 +37,19 @@ exports[`CourseCard component snapshot 1`] = ` /> } > @@ -59,13 +59,13 @@ exports[`CourseCard component snapshot 1`] = ` data-testid="CourseCardBanners" > - +
diff --git a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx index 9ff06f6..879a2d2 100644 --- a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx @@ -11,15 +11,15 @@ import Banner from 'components/Banner'; import messages from './messages'; -export const CertificateBanner = ({ courseNumber }) => { - const certificate = appHooks.useCardCertificateData(courseNumber); +export const CertificateBanner = ({ cardId }) => { + const certificate = appHooks.useCardCertificateData(cardId); const { isAudit, isVerified, hasFinished, - } = appHooks.useCardEnrollmentData(courseNumber); - const { isPassing } = appHooks.useCardGradeData(courseNumber); - const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(courseNumber); + } = appHooks.useCardEnrollmentData(cardId); + const { isPassing } = appHooks.useCardGradeData(cardId); + const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId); const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData(); const { formatMessage } = useIntl(); @@ -96,7 +96,7 @@ export const CertificateBanner = ({ courseNumber }) => { return null; }; CertificateBanner.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; export default CertificateBanner; diff --git a/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx b/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx index 24a23fa..b224fce 100644 --- a/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx +++ b/src/containers/CourseCard/components/Banners/CertificateBanner.test.jsx @@ -16,7 +16,7 @@ jest.mock('data/redux', () => ({ }, })); -const courseNumber = 'my-test-course-number'; +const cardId = 'my-test-course-number'; let el; @@ -50,15 +50,15 @@ const render = (overrides = {}) => { ...enrollmentData, ...enrollment, }); - el = shallow(); + 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); + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId); + expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); + expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId); }); test('no display if learner is verified', () => { render({ enrollment: { isVerified: true } }); diff --git a/src/containers/CourseCard/components/Banners/CourseBanner.jsx b/src/containers/CourseCard/components/Banners/CourseBanner.jsx index 3154190..e443e58 100644 --- a/src/containers/CourseCard/components/Banners/CourseBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CourseBanner.jsx @@ -8,14 +8,14 @@ import { hooks as appHooks } from 'data/redux'; import Banner from 'components/Banner'; import messages from './messages'; -export const CourseBanner = ({ courseNumber }) => { +export const CourseBanner = ({ cardId }) => { const { isVerified, isAuditAccessExpired, canUpgrade, - } = appHooks.useCardEnrollmentData(courseNumber); - const courseRun = appHooks.useCardCourseRunData(courseNumber); - const course = appHooks.useCardCourseData(courseNumber); + } = appHooks.useCardEnrollmentData(cardId); + const courseRun = appHooks.useCardCourseRunData(cardId); + const course = appHooks.useCardCourseData(cardId); const { formatMessage } = useIntl(); if (isVerified) { return null; } @@ -52,7 +52,7 @@ export const CourseBanner = ({ courseNumber }) => { return null; }; CourseBanner.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; export default CourseBanner; diff --git a/src/containers/CourseCard/components/Banners/CourseBanner.test.jsx b/src/containers/CourseCard/components/Banners/CourseBanner.test.jsx index 24a23fa..b224fce 100644 --- a/src/containers/CourseCard/components/Banners/CourseBanner.test.jsx +++ b/src/containers/CourseCard/components/Banners/CourseBanner.test.jsx @@ -16,7 +16,7 @@ jest.mock('data/redux', () => ({ }, })); -const courseNumber = 'my-test-course-number'; +const cardId = 'my-test-course-number'; let el; @@ -50,15 +50,15 @@ const render = (overrides = {}) => { ...enrollmentData, ...enrollment, }); - el = shallow(); + 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); + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId); + expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); + expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId); }); test('no display if learner is verified', () => { render({ enrollment: { isVerified: true } }); diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx index 748e9fc..7f38de2 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx @@ -7,10 +7,10 @@ import { hooks as appHooks } from 'data/redux'; import { dateFormatter } from 'utils'; import Banner from 'components/Banner'; -import useSelectSession from 'containers/SelectSession/hooks'; +import useSelectSessionModalData from 'containers/SelectSessionModal/hooks'; import messages from './messages'; -export const EntitlementBanner = ({ courseNumber }) => { +export const EntitlementBanner = ({ cardId }) => { const { isEntitlement, hasSessions, @@ -18,9 +18,9 @@ export const EntitlementBanner = ({ courseNumber }) => { changeDeadline, showExpirationWarning, isExpired, - } = appHooks.useCardEntitlementsData(courseNumber); + } = appHooks.useCardEntitlementsData(cardId); const { supportEmail } = appHooks.usePlatformSettingsData(); - const { openSessionModal } = useSelectSession({ courseNumber }); + const { openSessionModal } = useSelectSessionModalData({ cardId }); const { formatDate, formatMessage } = useIntl(); if (!isEntitlement) { @@ -42,7 +42,7 @@ export const EntitlementBanner = ({ courseNumber }) => { {formatMessage(messages.entitlementsExpiringSoon, { changeDeadline: dateFormatter(formatDate, changeDeadline), selectSessionButton: ( - ), @@ -60,7 +60,7 @@ export const EntitlementBanner = ({ courseNumber }) => { return null; }; EntitlementBanner.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; export default EntitlementBanner; diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx index dddb1e9..132f066 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.test.jsx @@ -11,11 +11,11 @@ jest.mock('data/redux', () => ({ useCardEntitlementsData: jest.fn(), }, })); -jest.mock('containers/SelectSession/hooks', () => () => ({ - openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'), +jest.mock('containers/SelectSessionModal/hooks', () => () => ({ + openSessionModal: (cardId) => jest.fn().mockName(`useSelectSessionModalData.openSessionModal(${cardId})`), })); -const courseNumber = 'my-test-course-number'; +const cardId = 'my-test-course-number'; let el; @@ -32,13 +32,13 @@ const render = (overrides = {}) => { const { entitlements = {} } = overrides; appHooks.useCardEntitlementsData.mockReturnValueOnce({ ...entitlementsData, ...entitlements }); appHooks.usePlatformSettingsData.mockReturnValueOnce(platformData); - el = shallow(); + el = shallow(); }; describe('EntitlementBanner', () => { it('initializes data with course number from entitlements', () => { render(); - expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(cardId); }); test('no display if not an entitlement', () => { render({ entitlements: { isEntitlement: false } }); 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 640d117..743cfe3 100644 --- a/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap +++ b/src/containers/CourseCard/components/Banners/__snapshots__/EntitlementBanner.test.jsx.snap @@ -15,7 +15,7 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = ` "changeDeadline": "11/11/2022", "selectSessionButton": `; diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.js index b827b4e..901af41 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/hooks.js +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.js @@ -1,51 +1,55 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { hooks as appHooks } from 'data/redux'; -import useSelectSession from 'containers/SelectSession/hooks'; +import useSelectSessionModalData from 'containers/SelectSessionModal/hooks'; import * as module from './hooks'; import messages from './messages'; -export const useAccessMessage = ({ courseNumber }) => { +export const useAccessMessage = ({ cardId }) => { const { formatMessage, formatDate } = useIntl(); - const { - accessExpirationDate, - isAudit, - isAuditAccessExpired, - } = appHooks.useCardEnrollmentData(courseNumber); - const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber); - - if (isAudit) { + const enrollment = appHooks.useCardEnrollmentData(cardId); + const courseRun = appHooks.useCardCourseRunData(cardId); + if (enrollment.isEnrolled) { + if (enrollment.isAudit) { + const { + accessExpirationDate, + isAuditAccessExpired, + } = enrollment; + return formatMessage( + isAuditAccessExpired ? messages.accessExpired : messages.accessExpires, + { accessExpirationDate: formatDate(accessExpirationDate) }, + ); + } + const { isArchived, endDate } = courseRun; return formatMessage( - isAuditAccessExpired ? messages.accessExpired : messages.accessExpires, - { accessExpirationDate: formatDate(accessExpirationDate) }, + isArchived ? messages.courseEnded : messages.courseEnds, + { endDate: formatDate(endDate) }, ); } - - return formatMessage( - isArchived ? messages.courseEnded : messages.courseEnds, - { endDate: formatDate(endDate) }, - ); + return null; }; -export const useCardDetailsData = ({ courseNumber }) => { +export const useCardDetailsData = ({ cardId }) => { const { formatMessage } = useIntl(); - const providerName = appHooks.useCardProviderData(courseNumber).name; + const providerName = appHooks.useCardProviderData(cardId).name; + const { courseNumber } = appHooks.useCardCourseData(cardId); const { isEntitlement, isFulfilled, canChange, - } = appHooks.useCardEntitlementsData(courseNumber); + } = appHooks.useCardEntitlementsData(cardId); - const { openSessionModalWithLeaveOption: openSessionModal } = useSelectSession({ courseNumber }); + const { openSessionModal } = useSelectSessionModalData(); return { providerName: providerName || formatMessage(messages.unknownProviderName), - accessMessage: module.useAccessMessage({ courseNumber }), + accessMessage: module.useAccessMessage({ cardId }), isEntitlement, isFulfilled, canChange, - openSessionModal, + openSessionModal: openSessionModal(cardId), formatMessage, + courseNumber, }; }; diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js index b35cfc5..c074c60 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js @@ -8,22 +8,24 @@ import messages from './messages'; jest.mock('data/redux', () => ({ hooks: { + useCardCourseData: jest.fn(), useCardCourseRunData: jest.fn(), useCardEnrollmentData: jest.fn(), useCardEntitlementsData: jest.fn(), useCardProviderData: jest.fn(), }, })); -jest.mock('containers/SelectSession/hooks', () => () => ({ - openSessionModalWithLeaveOption: jest.fn().mockName('useSelectSession.openSessionModalWithLeaveOptionFunction'), +jest.mock('containers/SelectSessionModal/hooks', () => () => ({ + openSessionModal: jest.fn().mockName('useSelectSession.openSessionModalFunction'), })); -const courseNumber = 'my-test-course-number'; +const cardId = 'my-test-card-id'; +const courseNumber = 'test-course-number'; const useAccessMessage = 'test-access-message'; -const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage }); +const mockAccessMessage = (args) => ({ cardId: args.cardId, useAccessMessage }); const hookKeys = keyStore(hooks); -describe('CourseCard hooks', () => { +describe('CourseCardDetails hooks', () => { let out; const { formatMessage, formatDate } = useIntl(); beforeEach(() => { @@ -53,7 +55,8 @@ describe('CourseCard hooks', () => { ...entitlementData, ...entitlement, }); - out = hooks.useCardDetailsData({ courseNumber }); + appHooks.useCardCourseData.mockReturnValueOnce({ courseNumber }); + out = hooks.useCardDetailsData({ cardId }); }; beforeEach(() => { runHook({}); @@ -61,8 +64,8 @@ describe('CourseCard hooks', () => { 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 useAccessMessage output, called with cardId', () => { + expect(out.accessMessage).toEqual(mockAccessMessage({ cardId })); }); it('forwards provider name if it exists, else formatted unknown provider name', () => { expect(out.providerName).toEqual(providerData.name); @@ -70,15 +73,17 @@ describe('CourseCard hooks', () => { expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName)); }); }); + describe('useAccessMessage', () => { const enrollmentData = { + isEnrolled: true, accessExpirationDate: 'test-expiration-date', isAudit: false, isAuditAccessExpired: false, }; const courseRunData = { isFinished: false, - endDate: 'test-end-date', + endDate: '10/20/1000', }; const runHook = ({ enrollment = {}, courseRun = {} }) => { appHooks.useCardCourseRunData.mockReturnValueOnce({ @@ -89,13 +94,15 @@ describe('CourseCard hooks', () => { ...enrollmentData, ...enrollment, }); - out = hooks.useAccessMessage({ courseNumber }); + out = hooks.useAccessMessage({ cardId }); }; + it('loads data from enrollment and course run data based on course number', () => { runHook({}); - expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber); - expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); + expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId); }); + describe('if audit, and expired', () => { it('returns accessExpired message with accessExpirationDate from cardData', () => { runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } }); diff --git a/src/containers/CourseCard/components/CourseCardDetails/index.jsx b/src/containers/CourseCard/components/CourseCardDetails/index.jsx index c138b6d..d44540b 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/index.jsx +++ b/src/containers/CourseCard/components/CourseCardDetails/index.jsx @@ -7,7 +7,7 @@ import useCardDetailsData from './hooks'; import messages from './messages'; -export const CourseCardDetails = ({ courseNumber }) => { +export const CourseCardDetails = ({ cardId }) => { const { providerName, accessMessage, @@ -16,11 +16,18 @@ export const CourseCardDetails = ({ courseNumber }) => { canChange, openSessionModal, formatMessage, - } = useCardDetailsData({ courseNumber }); + courseNumber, + } = useCardDetailsData({ cardId }); return ( - {providerName} • {courseNumber} • {accessMessage} + {providerName} • {courseNumber} + {!(isEntitlement && !isFulfilled) && ( + <> + {' • '} + {accessMessage} + + )} {isEntitlement && isFulfilled && canChange ? ( <> {' • '} @@ -34,7 +41,7 @@ export const CourseCardDetails = ({ courseNumber }) => { }; CourseCardDetails.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; CourseCardDetails.defaultProps = {}; diff --git a/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx b/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx index 86127e6..f83a4df 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx +++ b/src/containers/CourseCard/components/CourseCardDetails/index.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { formatMessage } from 'testUtils'; + import CourseCardDetails from '.'; import hooks from './hooks'; @@ -10,7 +12,7 @@ jest.mock('./hooks', () => ({ default: jest.fn(), })); -const courseNumber = 'test-course-number'; +const cardId = 'test-card-id'; describe('CourseCard Details component', () => { it('has change session button on entitlement course', () => { @@ -18,14 +20,14 @@ describe('CourseCard Details component', () => { providerName: 'provider-name', accessMessage: 'access-message', openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'), - formatMessage: (message, values) =>
, + formatMessage, isEntitlement: true, isFulfilled: true, canChange: true, ...args, }); hooks.mockImplementationOnce(mockHook({ isEntitlement: true })); - const el = shallow(); + const el = shallow(); expect(el).toMatchSnapshot(); // it has 3 separator, 4 column expect(el.text().match(/•/g)).toHaveLength(3); @@ -43,7 +45,7 @@ describe('CourseCard Details component', () => { ...args, }); hooks.mockImplementationOnce(mockHook({ isEntitlement: false })); - const el = shallow(); + 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/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx index d696c89..67bc1ad 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -8,7 +8,7 @@ import EmailSettingsModal from 'containers/EmailSettingsModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; import useCourseCardMenuData from './hooks'; -export const CourseCardMenu = ({ courseNumber }) => { +export const CourseCardMenu = ({ cardId }) => { const { emailSettingsModal, unenrollModal, @@ -34,18 +34,18 @@ export const CourseCardMenu = ({ courseNumber }) => { ); }; CourseCardMenu.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; export default CourseCardMenu; diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap index c3797c8..dfe4df4 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap @@ -11,8 +11,8 @@ exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = ` useRelatedProgramsBadge.programsMessage diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.jsx b/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.jsx index 0021cb0..7586ca1 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.jsx +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.jsx @@ -11,10 +11,10 @@ export const state = StrictDict({ isOpen: (val) => React.useState(val), // eslint-disable-line }); -export const useRelatedProgramsBadgeData = ({ courseNumber }) => { +export const useRelatedProgramsBadgeData = ({ cardId }) => { const [isOpen, setIsOpen] = module.state.isOpen(false); const { formatMessage } = useIntl(); - const numPrograms = appHooks.useCardRelatedProgramsData(courseNumber).length; + const numPrograms = appHooks.useCardRelatedProgramsData(cardId).length; let programsMessage = ''; if (numPrograms) { programsMessage = formatMessage( diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js b/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js index 39987fd..cf412f7 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js @@ -12,7 +12,7 @@ jest.mock('data/redux', () => ({ }, })); -const courseNumber = 'my-test-course-number'; +const cardId = 'test-card-id'; const state = new MockUseState(hooks); const numPrograms = 27; @@ -33,7 +33,7 @@ describe('RelatedProgramsBadge hooks', () => { appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: numPrograms, }); - out = hooks.useRelatedProgramsBadgeData({ courseNumber }); + out = hooks.useRelatedProgramsBadgeData({ cardId }); }); afterEach(state.restore); @@ -55,17 +55,17 @@ describe('RelatedProgramsBadge hooks', () => { expect(out.isOpen).toEqual(state.stateVals.isOpen); }); - test('forwards numPrograms from relatedPrograms.length for the courseNumber', () => { + test('forwards numPrograms from relatedPrograms.length for the cardId', () => { expect(out.numPrograms).toEqual(numPrograms); }); test('returns empty programsMessage if no programs', () => { appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 }); - out = hooks.useRelatedProgramsBadgeData({ courseNumber }); + out = hooks.useRelatedProgramsBadgeData({ cardId }); expect(out.programsMessage).toEqual(''); }); test('returns badgeLabelSingular programsMessage if 1 programs', () => { appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 }); - out = hooks.useRelatedProgramsBadgeData({ courseNumber }); + out = hooks.useRelatedProgramsBadgeData({ cardId }); expect(out.programsMessage).toEqual(formatMessage( messages.badgeLabelSingular, { numPrograms: 1 }, diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/index.jsx b/src/containers/CourseCard/components/RelatedProgramsBadge/index.jsx index 0bfc10c..a4e94e1 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/index.jsx +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/index.jsx @@ -8,14 +8,14 @@ import { Program } from '@edx/paragon/icons'; import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal'; import useRelatedProgramsBadgeData from './hooks'; -export const RelatedProgramsBadge = ({ courseNumber }) => { +export const RelatedProgramsBadge = ({ cardId }) => { const { isOpen, openModal, closeModal, numPrograms, programsMessage, - } = useRelatedProgramsBadgeData({ courseNumber }); + } = useRelatedProgramsBadgeData({ cardId }); return (numPrograms > 0) && ( <> - + ); }; RelatedProgramsBadge.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; export default RelatedProgramsBadge; diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/index.test.jsx b/src/containers/CourseCard/components/RelatedProgramsBadge/index.test.jsx index 3ae8a57..3382df1 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/index.test.jsx +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/index.test.jsx @@ -15,16 +15,16 @@ const hookProps = { programsMessage: 'useRelatedProgramsBadge.programsMessage', }; -const courseNumber = 'test-course-number'; +const cardId = 'test-course-number'; describe('RelatedProgramsBadge component', () => { test('empty render: no programs', () => { useRelatedProgramsBadge.mockReturnValueOnce({ ...hookProps, numPrograms: 0 }); - const el = shallow(); + const el = shallow(); expect(el.isEmptyRender()).toEqual(true); }); test('snapshot: 3 programs', () => { useRelatedProgramsBadge.mockReturnValueOnce(hookProps); - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); diff --git a/src/containers/CourseCard/hooks.js b/src/containers/CourseCard/hooks.js index ecadc00..9d7fadf 100644 --- a/src/containers/CourseCard/hooks.js +++ b/src/containers/CourseCard/hooks.js @@ -1,11 +1,13 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { hooks as appHooks } from 'data/redux'; -export const useCardData = ({ courseNumber }) => { +export const useCardData = ({ cardId }) => { const { formatMessage } = useIntl(); - const { title, bannerUrl } = appHooks.useCardCourseData(courseNumber); + const { title, bannerUrl } = appHooks.useCardCourseData(cardId); + const { isEnrolled } = appHooks.useCardEnrollmentData(cardId); return { + isEnrolled, title, bannerUrl, formatMessage, diff --git a/src/containers/CourseCard/hooks.test.js b/src/containers/CourseCard/hooks.test.js index 429e63d..050323b 100644 --- a/src/containers/CourseCard/hooks.test.js +++ b/src/containers/CourseCard/hooks.test.js @@ -7,10 +7,11 @@ import * as hooks from './hooks'; jest.mock('data/redux', () => ({ hooks: { useCardCourseData: jest.fn(), + useCardEnrollmentData: jest.fn(), }, })); -const courseNumber = 'my-test-course-number'; +const cardId = 'my-test-course-number'; describe('CourseCard hooks', () => { let out; @@ -29,7 +30,8 @@ describe('CourseCard hooks', () => { ...courseData, ...course, }); - out = hooks.useCardData({ courseNumber }); + appHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: 'test-is-enrolled' }); + out = hooks.useCardData({ cardId }); }; beforeEach(() => { runHook({}); @@ -38,7 +40,7 @@ describe('CourseCard hooks', () => { expect(out.formatMessage).toEqual(formatMessage); }); it('passes course title and banner URL form course data', () => { - expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId); expect(out.title).toEqual(courseData.title); expect(out.bannerUrl).toEqual(courseData.bannerUrl); }); diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx index e9fb85a..c73204f 100644 --- a/src/containers/CourseCard/index.jsx +++ b/src/containers/CourseCard/index.jsx @@ -17,14 +17,15 @@ import CourseCardActions from './components/CourseCardActions'; import messages from './messages'; import CourseCardDetails from './components/CourseCardDetails'; -export const CourseCard = ({ courseNumber }) => { +export const CourseCard = ({ cardId }) => { const { + isEnrolled, title, bannerUrl, formatMessage, - } = useCardData({ courseNumber }); + } = useCardData({ cardId }); return ( -
+
{ {title}} - actions={} + actions={} /> - + } + textElement={} > - +
- - - + + + {isEnrolled && }
); }; CourseCard.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, }; CourseCard.defaultProps = {}; diff --git a/src/containers/CourseCard/index.test.jsx b/src/containers/CourseCard/index.test.jsx index 95d77e5..e827045 100644 --- a/src/containers/CourseCard/index.test.jsx +++ b/src/containers/CourseCard/index.test.jsx @@ -23,14 +23,18 @@ const dataProps = { title: 'hooks.title', bannerUrl: 'hooks.bannerUrl', formatMessage: jest.fn(msg => ({ formatted: msg })), + isEnrolled: true, }; -const courseNumber = 'test-course-number'; +const cardId = 'test-card-id'; describe('CourseCard component', () => { test('snapshot', () => { hooks.mockReturnValueOnce(dataProps); - expect(shallow()).toMatchSnapshot(); - expect(hooks).toHaveBeenCalledWith({ courseNumber }); + expect(shallow()).toMatchSnapshot(); + expect(hooks).toHaveBeenCalledWith({ cardId }); + }); + test('snapshot: not enrolled (no certificate card)', () => { + hooks.mockReturnValueOnce({ ...dataProps, isEnrolled: true }); }); }); diff --git a/src/containers/CourseFilterControls/ActiveCourseFilters.jsx b/src/containers/CourseFilterControls/ActiveCourseFilters.jsx new file mode 100644 index 0000000..ce500e0 --- /dev/null +++ b/src/containers/CourseFilterControls/ActiveCourseFilters.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { Button, Chip } from '@edx/paragon'; +import { CloseSmall } from '@edx/paragon/icons'; + +import messages from './messages'; +import './index.scss'; + +export const ActiveCourseFilters = ({ + filters, + setFilters, + handleRemoveFilter, +}) => { + const { formatMessage } = useIntl(); + return ( +
+ {filters.map(filter => ( + + {formatMessage(messages[filter])} + + ))} + +
+ ); +}; +ActiveCourseFilters.propTypes = { + filters: PropTypes.arrayOf(PropTypes.string).isRequired, + setFilters: PropTypes.shape({ + remove: PropTypes.func, + clear: PropTypes.func, + }).isRequired, + handleRemoveFilter: PropTypes.func.isRequired, +}; + +export default ActiveCourseFilters; diff --git a/src/containers/CourseFilterControls/CourseFilterControls.jsx b/src/containers/CourseFilterControls/CourseFilterControls.jsx new file mode 100644 index 0000000..fe1185a --- /dev/null +++ b/src/containers/CourseFilterControls/CourseFilterControls.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { + Form, + Button, + ModalPopup, +} from '@edx/paragon'; +import { Tune } from '@edx/paragon/icons'; + +import FilterForm from './components/FilterForm'; +import SortForm from './components/SortForm'; +import useCourseFilterControlsData from './hooks'; +import messages from './messages'; + +import './index.scss'; + +export const CourseFilterControls = ({ + sortBy, + setSortBy, + filters, + setFilters, +}) => { + const { formatMessage } = useIntl(); + const { + isOpen, + open, + close, + target, + setTarget, + handleFilterChange, + handleSortChange, + } = useCourseFilterControlsData({ + setFilters, + setSortBy, + }); + return ( +
+ + +
+
+
+ +
+
+
+ +
+
+
+
+
+ ); +}; +CourseFilterControls.propTypes = { + sortBy: PropTypes.string.isRequired, + setSortBy: PropTypes.func.isRequired, + filters: PropTypes.arrayOf(PropTypes.string).isRequired, + setFilters: PropTypes.shape({ + add: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + }).isRequired, +}; + +export default CourseFilterControls; diff --git a/src/containers/CourseFilterControls/components/Checkbox.jsx b/src/containers/CourseFilterControls/components/Checkbox.jsx new file mode 100644 index 0000000..adfad51 --- /dev/null +++ b/src/containers/CourseFilterControls/components/Checkbox.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { Form } from '@edx/paragon'; + +import messages from '../messages'; + +export const Checkbox = ({ filterKey }) => { + const { formatMessage } = useIntl(); + return ( + + {formatMessage(messages[filterKey])} + + ); +}; +Checkbox.propTypes = { + filterKey: PropTypes.string.isRequired, +}; + +export default Checkbox; diff --git a/src/containers/CourseFilterControls/components/FilterForm.jsx b/src/containers/CourseFilterControls/components/FilterForm.jsx new file mode 100644 index 0000000..f94625f --- /dev/null +++ b/src/containers/CourseFilterControls/components/FilterForm.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { FilterKeys } from 'data/constants/app'; + +import { Form } from '@edx/paragon'; + +import Checkbox from './Checkbox'; + +export const FilterForm = ({ + filters, + handleFilterChange, +}) => ( + +
Course Status
+ + + + + + + +
+); +FilterForm.propTypes = { + filters: PropTypes.arrayOf(PropTypes.string).isRequired, + handleFilterChange: PropTypes.func.isRequired, +}; + +export default FilterForm; diff --git a/src/containers/CourseFilterControls/components/SortForm.jsx b/src/containers/CourseFilterControls/components/SortForm.jsx new file mode 100644 index 0000000..f545b1d --- /dev/null +++ b/src/containers/CourseFilterControls/components/SortForm.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { SortKeys } from 'data/constants/app'; + +import { Form } from '@edx/paragon'; + +import messages from '../messages'; + +export const SortForm = ({ + handleSortChange, + sortBy, +}) => { + const { formatMessage } = useIntl(); + return ( + <> +
{formatMessage(messages.sort)}
+ + + {formatMessage(messages.sortLastEnrolled)} + + + {formatMessage(messages.sortTitle)} + + + + ); +}; +SortForm.propTypes = { + handleSortChange: PropTypes.func.isRequired, + sortBy: PropTypes.string.isRequired, +}; + +export default SortForm; diff --git a/src/containers/CourseFilterControls/hooks.js b/src/containers/CourseFilterControls/hooks.js new file mode 100644 index 0000000..72985ac --- /dev/null +++ b/src/containers/CourseFilterControls/hooks.js @@ -0,0 +1,29 @@ +import React from 'react'; + +import { useToggle } from '@edx/paragon'; + +export const useCourseFilterControlsData = ({ + setFilters, + setSortBy, +}) => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = React.useState(null); + const handleFilterChange = ({ target: { checked, value } }) => { + const update = checked ? setFilters.add : setFilters.remove; + update(value); + }; + const handleSortChange = ({ target: { value } }) => { + setSortBy(value); + }; + return { + isOpen, + open, + close, + target, + setTarget, + handleFilterChange, + handleSortChange, + }; +}; + +export default useCourseFilterControlsData; diff --git a/src/containers/CourseFilterControls/index.jsx b/src/containers/CourseFilterControls/index.jsx new file mode 100644 index 0000000..32ead25 --- /dev/null +++ b/src/containers/CourseFilterControls/index.jsx @@ -0,0 +1,6 @@ +import CourseFilterControls from './CourseFilterControls'; + +export { default as ActiveCourseFilters } from './ActiveCourseFilters'; + +export { CourseFilterControls }; +export default CourseFilterControls; diff --git a/src/containers/CourseFilterControls/index.scss b/src/containers/CourseFilterControls/index.scss new file mode 100644 index 0000000..8da93f9 --- /dev/null +++ b/src/containers/CourseFilterControls/index.scss @@ -0,0 +1,20 @@ +#course-filter-controls-card { + width: 512px; + height: 288px; + &.no-enrollments { + height: 172px; + } + .filter-form-heading { + font-weight: bold; + font-size: 18px; + } + hr { + width: 1px; + }; + .filter-form-col { + width: 256px; + display: inline-block; + text-align: left; + } +} + diff --git a/src/containers/CourseFilterControls/messages.js b/src/containers/CourseFilterControls/messages.js new file mode 100644 index 0000000..fa36829 --- /dev/null +++ b/src/containers/CourseFilterControls/messages.js @@ -0,0 +1,55 @@ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + inProgress: { + id: 'learner-dash.courseListFilters.inProgress', + description: 'in-progress filter checkbox label for course list filters', + defaultMessage: 'In-Progress', + }, + notStarted: { + id: 'learner-dash.courseListFilters.notStarted', + description: 'Not-Started filter checkbox label for course list filters', + defaultMessage: 'Not Started', + }, + done: { + id: 'learner-dash.courseListFilters.done', + description: 'done filter checkbox label for course list filters', + defaultMessage: 'Done', + }, + notEnrolled: { + id: 'learner-dash.courseListFilters.notEnrolled', + description: 'not-enrolled filter checkbox label for course list filters', + defaultMessage: 'Not Enrolled', + }, + upgraded: { + id: 'learner-dash.courseListFilters.upgraded', + description: 'upgraded filter checkbox label for course list filters', + defaultMessage: 'Upgraded', + }, + clearAll: { + id: 'learner-dash.courseListFilters.clearAll', + description: 'clear all filters button text', + defaultMessage: 'Clear all', + }, + sort: { + id: 'learner-dash.courseListFilters.sort', + description: 'Sort radio form heading', + defaultMessage: 'Sort', + }, + sortLastEnrolled: { + id: 'learner-dash.courseListFilters.sortLastEnrolled', + description: 'Last enrolled sort option text', + defaultMessage: 'Last enrolled', + }, + sortTitle: { + id: 'learner-dash.courseListFilters.sortTitle', + description: 'Title sort option text', + defaultMessage: 'Title (A-Z)', + }, + refine: { + id: 'learner-dash.courseListFilters.refine', + description: 'Filter button container text', + defaultMessage: 'Refine', + }, +}); +export default messages; diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx index 1215ea1..da22dde 100644 --- a/src/containers/CourseList/index.jsx +++ b/src/containers/CourseList/index.jsx @@ -1,20 +1,87 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Pagination, + useCheckboxSetValues, +} from '@edx/paragon'; + +import { hooks as appHooks } from 'data/redux'; +import { ListPageSize, SortKeys } from 'data/constants/app'; +import { ActiveCourseFilters, CourseFilterControls } from 'containers/CourseFilterControls'; import CourseCard from 'containers/CourseCard'; -import SelectSession from 'containers/SelectSession'; -export const CourseList = ({ courseListData }) => ( -
- {courseListData.map((courseNumber) => ( - - ))} - -
-); +import messages from './messages'; + +export const useCourseListData = () => { + const [pageNumber, setPageNumber] = React.useState(1); + const [sortBy, setSortBy] = React.useState(SortKeys.title); + const [filters, setFilters] = useCheckboxSetValues([]); + const { numPages, visible } = appHooks.useCurrentCourseList({ + sortBy, + isAscending: true, + filters, + pageNumber, + pageSize: ListPageSize, + }); + const handleRemoveFilter = (filter) => () => setFilters.remove(filter); + return { + numPages, + setPageNumber, + visibleList: visible, + filterOptions: { + sortBy, + setSortBy, + filters, + setFilters, + handleRemoveFilter, + }, + showFilters: filters.length > 0, + }; +}; + +export const CourseList = () => { + const { + filterOptions, + setPageNumber, + numPages, + showFilters, + visibleList, + } = useCourseListData(); + return ( +
+
+

+ +

+
+ +
+
+ { showFilters && ( +
+ +
+ )} +
+ {visibleList.map(({ cardId }) => ( + + ))} + +
+
+ ); +}; CourseList.propTypes = { - courseListData: PropTypes.arrayOf(PropTypes.string).isRequired, }; export default CourseList; diff --git a/src/containers/CourseList/messages.js b/src/containers/CourseList/messages.js new file mode 100644 index 0000000..ec73e1c --- /dev/null +++ b/src/containers/CourseList/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + myCourses: { + id: 'dashboard.mycourses', + defaultMessage: 'My Courses', + description: 'Course list heading', + }, +}); + +export default messages; diff --git a/src/containers/Dashboard/index.jsx b/src/containers/Dashboard/index.jsx index ae3708e..4e59824 100644 --- a/src/containers/Dashboard/index.jsx +++ b/src/containers/Dashboard/index.jsx @@ -1,44 +1,41 @@ import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import { selectors, thunkActions } from 'data/redux'; +import { + thunkActions, + hooks as appHooks, +} from 'data/redux'; import CourseList from 'containers/CourseList'; import WidgetSidebar from 'containers/WidgetSidebar'; import EmptyCourse from 'containers/EmptyCourse'; +import SelectSessionModal from 'containers/SelectSessionModal'; +import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal'; -import messages from './messages'; -import * as module from '.'; +import './index.scss'; -export const useDashboardData = ({ dispatch }) => { +export const Dashboard = () => { + const dispatch = useDispatch(); React.useEffect( () => { dispatch(thunkActions.app.initialize()); }, [dispatch], ); - return { - enrollments: useSelector(selectors.app.enrollments), - entitlements: useSelector(selectors.app.entitlements), - }; -}; -export const Dashboard = () => { - const dispatch = useDispatch(); - const { - enrollments, - // entitlements, - } = module.useDashboardData({ dispatch }); + const hasCourses = appHooks.useHasCourses(); + const hasAvailableDashboards = appHooks.useHasAvailableDashboards(); return ( -
- {enrollments.length ? ( +
+ {hasAvailableDashboards && } + {hasCourses ? ( <> -

- -

-
- - +
+
+ + +
+
+ +
) : ( diff --git a/src/containers/Dashboard/index.scss b/src/containers/Dashboard/index.scss new file mode 100644 index 0000000..6dd1e29 --- /dev/null +++ b/src/containers/Dashboard/index.scss @@ -0,0 +1,4 @@ +#course-list-heading-container { + display: flex; + justify-content: space-between; +} diff --git a/src/containers/Dashboard/messages.js b/src/containers/Dashboard/messages.js index 35a5251..ec73e1c 100644 --- a/src/containers/Dashboard/messages.js +++ b/src/containers/Dashboard/messages.js @@ -1,10 +1,10 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - myCourse: { - id: 'dashboard.mycourse', + myCourses: { + id: 'dashboard.mycourses', defaultMessage: 'My Courses', - description: 'My Courses', + description: 'Course list heading', }, }); diff --git a/src/containers/EmailSettingsModal/hooks.js b/src/containers/EmailSettingsModal/hooks.js index 24a93cc..5a15e26 100644 --- a/src/containers/EmailSettingsModal/hooks.js +++ b/src/containers/EmailSettingsModal/hooks.js @@ -12,10 +12,10 @@ export const state = StrictDict({ export const useEmailData = ({ closeModal, - courseNumber, + cardId, // dispatch, }) => { - const { isEmailEnabled } = appHooks.useCardEnrollmentData(courseNumber); + const { isEmailEnabled } = appHooks.useCardEnrollmentData(cardId); const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled); const onToggle = React.useCallback( () => setToggleValue(!toggleValue), diff --git a/src/containers/EmailSettingsModal/hooks.test.js b/src/containers/EmailSettingsModal/hooks.test.js index accee42..90aa9c5 100644 --- a/src/containers/EmailSettingsModal/hooks.test.js +++ b/src/containers/EmailSettingsModal/hooks.test.js @@ -9,7 +9,7 @@ jest.mock('data/redux', () => ({ }, })); -const courseNumber = 'my-test-course-number'; +const cardId = 'my-test-course-number'; const closeModal = jest.fn(); const state = new MockUseState(hooks); @@ -26,12 +26,12 @@ describe('EmailSettingsModal hooks', () => { beforeEach(() => { state.mock(); appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEmailEnabled: true }); - out = hooks.useEmailData({ closeModal, courseNumber }); + out = hooks.useEmailData({ closeModal, cardId }); }); afterEach(state.restore); test('loads enrollment data based on course number', () => { - expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber); + expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId); }); test('initializes toggle value to cardData.isEmailEnabled', () => { @@ -39,7 +39,7 @@ describe('EmailSettingsModal hooks', () => { expect(out.toggleValue).toEqual(true); appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEmailEnabled: false }); - out = hooks.useEmailData({ closeModal, courseNumber }); + out = hooks.useEmailData({ closeModal, cardId }); state.expectInitializedWith(state.keys.toggle, false); expect(out.toggleValue).toEqual(false); }); diff --git a/src/containers/EmailSettingsModal/index.jsx b/src/containers/EmailSettingsModal/index.jsx index 261ae4c..f44ee4e 100644 --- a/src/containers/EmailSettingsModal/index.jsx +++ b/src/containers/EmailSettingsModal/index.jsx @@ -18,14 +18,14 @@ import messages from './messages'; export const EmailSettingsModal = ({ closeModal, show, - courseNumber, + cardId, }) => { const dispatch = useDispatch(); const { toggleValue, onToggle, save, - } = useEmailData({ dispatch, closeModal, courseNumber }); + } = useEmailData({ dispatch, closeModal, cardId }); const { formatMessage } = useIntl(); return ( @@ -52,7 +52,7 @@ export const EmailSettingsModal = ({ ); }; EmailSettingsModal.propTypes = { - courseNumber: PropTypes.string.isRequired, + cardId: PropTypes.string.isRequired, closeModal: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, }; diff --git a/src/containers/EmailSettingsModal/index.test.jsx b/src/containers/EmailSettingsModal/index.test.jsx index 6a82846..a835772 100644 --- a/src/containers/EmailSettingsModal/index.test.jsx +++ b/src/containers/EmailSettingsModal/index.test.jsx @@ -19,7 +19,7 @@ const hookProps = { const props = { closeModal: jest.fn().mockName('closeModal'), show: true, - courseNumber: 'test-course-number', + cardId: 'test-course-number', }; const dispatch = useDispatch(); @@ -33,11 +33,11 @@ describe('EmailSettingsModal', () => { hooks.mockReturnValueOnce(hookProps); shallow(); }); - it('calls hook w/ dispatch from redux hook, and closeModal, courseNumber from props', () => { + it('calls hook w/ dispatch from redux hook, and closeModal, cardId from props', () => { expect(hooks).toHaveBeenCalledWith({ closeModal: props.closeModal, dispatch, - courseNumber: props.courseNumber, + cardId: props.cardId, }); }); }); diff --git a/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap b/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..068c3c3 --- /dev/null +++ b/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EnterpriseDashboard snapshot 1`] = ` + +
+

+ You have access to the edX, Inc. dashboard +

+

+ To access the coureses available to you through edX, Inc., visit the edX, Inc. dashboard now. +

+ + + + +
+
+`; diff --git a/src/containers/EnterpriseDashboardModal/hooks.js b/src/containers/EnterpriseDashboardModal/hooks.js new file mode 100644 index 0000000..2812eb0 --- /dev/null +++ b/src/containers/EnterpriseDashboardModal/hooks.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { hooks as appHooks } from 'data/redux'; +import { StrictDict } from 'utils'; +import * as module from './hooks'; + +export const state = StrictDict({ + showModal: (val) => React.useState(val), // eslint-disable-line +}); + +export const useEnterpriseDashboardHook = () => { + const [showModal, setShowModal] = module.state.showModal(true); + const { mostRecentDashboard } = appHooks.useEnterpriseDashboardData(); + const handleClick = () => setShowModal(false); + return { + showModal, + handleClick, + mostRecentDashboard, + }; +}; + +export default useEnterpriseDashboardHook; diff --git a/src/containers/EnterpriseDashboardModal/hooks.test.js b/src/containers/EnterpriseDashboardModal/hooks.test.js new file mode 100644 index 0000000..43183bb --- /dev/null +++ b/src/containers/EnterpriseDashboardModal/hooks.test.js @@ -0,0 +1,44 @@ +import { MockUseState } from 'testUtils'; +import { hooks as appHooks } from 'data/redux'; + +import * as hooks from './hooks'; + +jest.mock('data/redux', () => ({ + hooks: { + useEnterpriseDashboardData: jest.fn(), + }, +})); + +const state = new MockUseState(hooks); + +const enterpriseDashboardData = { + mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' }, +}; + +describe('EnterpriseDashboard hooks', () => { + appHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData }); + + describe('state values', () => { + state.testGetter(state.keys.showModal); + }); + + describe('behavior', () => { + let out; + + beforeEach(() => { + state.mock(); + out = hooks.useEnterpriseDashboardHook(); + }); + afterEach(state.restore); + + test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => { + expect(out.mostRecentDashboard).toMatchObject(enterpriseDashboardData.mostRecentDashboard); + }); + + test('modal initializes to shown when rendered and closes on click', () => { + state.expectInitializedWith(state.keys.showModal, true); + out.handleClick(); + expect(state.values.showModal).toEqual(false); + }); + }); +}); diff --git a/src/containers/EnterpriseDashboardModal/index.jsx b/src/containers/EnterpriseDashboardModal/index.jsx new file mode 100644 index 0000000..2908a5e --- /dev/null +++ b/src/containers/EnterpriseDashboardModal/index.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + ModalDialog, ActionRow, Button, +} from '@edx/paragon'; + +import messages from './messages'; +import useEnterpriseDashboardHook from './hooks'; + +export const EnterpriseDashboardModal = () => { + const { formatMessage } = useIntl(); + const { + showModal, + handleClick, + mostRecentDashboard, + } = useEnterpriseDashboardHook(); + return ( + +
+

+ {formatMessage(messages.enterpriseDialogHeader, { + label: mostRecentDashboard.label, + })} +

+

+ {formatMessage(messages.enterpriseDialogBody, { + label: mostRecentDashboard.label, + })} +

+ + + + +
+
+ ); +}; + +EnterpriseDashboardModal.propTypes = {}; + +export default EnterpriseDashboardModal; diff --git a/src/containers/EnterpriseDashboardModal/index.test.jsx b/src/containers/EnterpriseDashboardModal/index.test.jsx new file mode 100644 index 0000000..d036bdd --- /dev/null +++ b/src/containers/EnterpriseDashboardModal/index.test.jsx @@ -0,0 +1,22 @@ +import { shallow } from 'enzyme'; +import EnterpriseDashboard from '.'; + +import useEnterpriseDashboardHook from './hooks'; + +jest.mock('./hooks', () => ({ + __esModule: true, + default: jest.fn(), +})); + +describe('EnterpriseDashboard', () => { + test('snapshot', () => { + const hookData = { + mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' }, + showDialog: false, + handleClick: jest.fn().mockName('useEnterpriseDashboardHook.handleClick'), + }; + useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookData }); + const el = shallow(); + expect(el).toMatchSnapshot(); + }); +}); diff --git a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/messages.js b/src/containers/EnterpriseDashboardModal/messages.js similarity index 84% rename from src/containers/LearnerDashboardHeader/EnterpriseDashboard/messages.js rename to src/containers/EnterpriseDashboardModal/messages.js index 8b9c118..f8a8656 100644 --- a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/messages.js +++ b/src/containers/EnterpriseDashboardModal/messages.js @@ -1,11 +1,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - dashboard: { - id: 'leanerDashboard.menu.dashboard.label', - defaultMessage: 'Dashboard', - description: 'The text for the user menu Dashboard navigation link.', - }, enterpriseDialogHeader: { id: 'leanerDashboard.enterpriseDialogHeader', defaultMessage: 'You have access to the {label} dashboard', diff --git a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx index 004595d..1bfc405 100644 --- a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx +++ b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx @@ -6,11 +6,12 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Dropdown, Icon } from '@edx/paragon'; import { Person } from '@edx/paragon/icons'; +import { hooks as appHooks } from 'data/redux'; import messages from './messages'; -import EnterpriseDashboard from './EnterpriseDashboard'; export const AuthenticatedUserDropdown = ({ username }) => { const { formatMessage } = useIntl(); + const { availableDashboards } = appHooks.useEnterpriseDashboardData(); return ( <> @@ -21,7 +22,18 @@ export const AuthenticatedUserDropdown = ({ username }) => { - + SWITCH DASHBOARD + Personal + {availableDashboards && availableDashboards.map((dashboard) => ( + + {dashboard.label} {formatMessage(messages.dashboard)} + + ))} + {formatMessage(messages.profile)} @@ -36,6 +48,7 @@ export const AuthenticatedUserDropdown = ({ username }) => { {formatMessage(messages.help)} + {formatMessage(messages.signOut)} diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js index fc8dc5d..ef29d8c 100644 --- a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js @@ -24,6 +24,7 @@ export const useConfirmEmailBannerData = () => { const openConfirmModalButtonClick = () => { dispatch(thunkActions.app.sendConfirmEmail()); openConfirmModal(); + closePageBanner(); }; const userConfirmEmailButtonClick = () => { diff --git a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/__snapshots__/index.test.jsx.snap b/src/containers/LearnerDashboardHeader/EnterpriseDashboard/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 66150b6..0000000 --- a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,130 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EnterpriseDashboard snapshot initilized 1`] = ` - - - Personal - - Dashboard - - - edX, Inc. - - Dashboard - - - Harvard - - Dashboard - - -
-

- You have access to the undefined dashboard -

-

- To access the coureses available to you through undefined, visit the undefined dashboard now. -

- - - - -
-
-
-`; - -exports[`EnterpriseDashboard snapshot select item and open modal 1`] = ` - - - Personal - - Dashboard - - - edX, Inc. - - Dashboard - - - Harvard - - Dashboard - - -
-

- You have access to the Personal dashboard -

-

- To access the coureses available to you through Personal, visit the Personal dashboard now. -

- - - - -
-
-
-`; diff --git a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/hooks.js b/src/containers/LearnerDashboardHeader/EnterpriseDashboard/hooks.js deleted file mode 100644 index 465c343..0000000 --- a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/hooks.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { hooks as appHooks } from 'data/redux'; -import { StrictDict } from 'utils'; -import * as module from './hooks'; - -export const state = StrictDict({ - showDialog: (val) => React.useState(val), // eslint-disable-line - selectedItem: (val) => React.useState(val), // eslint-disable-line -}); - -export const useEnterpriseDashboardHook = () => { - const { availableDashboards, mostRecentDashboard } = appHooks.useEnterpriseDashboardData(); - const [showDialog, setShowDialog] = module.state.showDialog(false); - const [selectedItem, setSelectedItem] = module.state.selectedItem({}); - - const beginSelectDashboardItem = (val) => () => { - setSelectedItem(val); - setShowDialog(true); - }; - - const cancelSelectDashboardItem = () => { - setSelectedItem({}); - setShowDialog(false); - }; - - return { - availableDashboards, - mostRecentDashboard, - showDialog, - - selectedItem, - beginSelectDashboardItem, - cancelSelectDashboardItem, - }; -}; - -export default useEnterpriseDashboardHook; diff --git a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/hooks.test.js b/src/containers/LearnerDashboardHeader/EnterpriseDashboard/hooks.test.js deleted file mode 100644 index 744d97e..0000000 --- a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/hooks.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { MockUseState } from 'testUtils'; -import { hooks as appHooks } from 'data/redux'; - -import * as hooks from './hooks'; - -jest.mock('data/redux', () => ({ - hooks: { - useEnterpriseDashboardData: jest.fn(), - }, -})); - -const state = new MockUseState(hooks); - -const enterpriseDashboardData = { - availableDashboards: [ - { label: 'Personal', url: '/dashboard' }, - { label: 'edX, Inc.', url: '/edx-dashboard' }, - { label: 'Harvard', url: '/harvard-dashboard' }, - ], - mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' }, -}; - -describe('EnterpriseDashboard hooks', () => { - appHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData }); - - describe('state values', () => { - state.testGetter(state.keys.showDialog); - state.testGetter(state.keys.selectedItem); - }); - - describe('behavior', () => { - let out; - - beforeEach(() => { - state.mock(); - out = hooks.useEnterpriseDashboardHook(); - }); - afterEach(state.restore); - - test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => { - expect(out.availableDashboards).toMatchObject(enterpriseDashboardData.availableDashboards); - expect(out.mostRecentDashboard).toMatchObject(enterpriseDashboardData.mostRecentDashboard); - }); - - test('modal is open on begin select dashboard item', () => { - state.expectInitializedWith('showDialog', false); - state.expectInitializedWith('selectedItem', {}); - const selectedItem = { abitary: 'not so true' }; - out.beginSelectDashboardItem(selectedItem)(); - expect(state.values.showDialog).toEqual(true); - expect(state.values.selectedItem).toMatchObject(selectedItem); - }); - - test('modal is close on cancel select dashboard item', () => { - out.cancelSelectDashboardItem(); - expect(state.values.selectedItem).toMatchObject({}); - expect(state.values.showDialog).toEqual(false); - }); - }); -}); diff --git a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/index.jsx b/src/containers/LearnerDashboardHeader/EnterpriseDashboard/index.jsx deleted file mode 100644 index 5252db0..0000000 --- a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -// import PropTypes from 'prop-types'; - -import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Dropdown, ModalDialog, ActionRow, Button, -} from '@edx/paragon'; - -import { nullMethod } from 'hooks'; - -import messages from './messages'; -import useEnterpriseDashboardHook from './hooks'; - -export const EnterpriseDashboard = () => { - const { formatMessage } = useIntl(); - const { - availableDashboards, - mostRecentDashboard, - showDialog, - - selectedItem, - beginSelectDashboardItem, - cancelSelectDashboardItem, - } = useEnterpriseDashboardHook(); - - return ( - <> - {availableDashboards.map((dashboard) => ( - - {dashboard.label} {formatMessage(messages.dashboard)} - - ))} - -
-

- {formatMessage(messages.enterpriseDialogHeader, { - label: selectedItem.label, - })} -

-

- {formatMessage(messages.enterpriseDialogBody, { - label: selectedItem.label, - })} -

- - - - -
-
- - ); -}; - -EnterpriseDashboard.propTypes = {}; - -export default EnterpriseDashboard; diff --git a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/index.test.jsx b/src/containers/LearnerDashboardHeader/EnterpriseDashboard/index.test.jsx deleted file mode 100644 index 74b9205..0000000 --- a/src/containers/LearnerDashboardHeader/EnterpriseDashboard/index.test.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { shallow } from 'enzyme'; -import EnterpriseDashboard from '.'; - -import useEnterpriseDashboardHook from './hooks'; - -jest.mock('./hooks', () => ({ - __esModule: true, - default: jest.fn(), -})); - -const enterpriseDashboardData = { - availableDashboards: [ - { label: 'Personal', url: '/dashboard' }, - { label: 'edX, Inc.', url: '/edx-dashboard' }, - { label: 'Harvard', url: '/harvard-dashboard' }, - ], - mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' }, -}; - -describe('EnterpriseDashboard', () => { - describe('snapshot', () => { - const hookReturn = { - ...enterpriseDashboardData, - showDialog: false, - - selectedItem: {}, - beginSelectDashboardItem: jest.fn().mockName('beginSelectDashboardItem'), - cancelSelectDashboardItem: jest - .fn() - .mockName('cancelSelectDashboardItem'), - }; - - test('initilized', () => { - useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookReturn }); - const el = shallow(); - expect(el).toMatchSnapshot(); - }); - - test('select item and open modal', () => { - useEnterpriseDashboardHook.mockReturnValueOnce({ - ...hookReturn, - selectedItem: enterpriseDashboardData.availableDashboards[0], - showDialog: true, - }); - const el = shallow(); - expect(el).toMatchSnapshot(); - }); - }); -}); diff --git a/src/containers/LearnerDashboardHeader/index.jsx b/src/containers/LearnerDashboardHeader/index.jsx index 01c6cb9..02daff0 100644 --- a/src/containers/LearnerDashboardHeader/index.jsx +++ b/src/containers/LearnerDashboardHeader/index.jsx @@ -6,6 +6,7 @@ import { Program } from '@edx/paragon/icons'; import { Button } from '@edx/paragon'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; + import GreetingBanner from './GreetingBanner'; import messages from './messages'; import ConfirmEmailBanner from './ConfirmEmailBanner'; diff --git a/src/containers/LearnerDashboardHeader/messages.js b/src/containers/LearnerDashboardHeader/messages.js index 02e2700..94d1c53 100644 --- a/src/containers/LearnerDashboardHeader/messages.js +++ b/src/containers/LearnerDashboardHeader/messages.js @@ -1,6 +1,11 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ + dashboard: { + id: 'leanerDashboard.menu.dashboard.label', + defaultMessage: 'Dashboard', + description: 'The text for the user menu Dashboard navigation link.', + }, help: { id: 'leanerDashboard.help.label', defaultMessage: 'Help', diff --git a/src/containers/RelatedProgramsModal/hooks.js b/src/containers/RelatedProgramsModal/hooks.js index adcf7ff..ad134b1 100644 --- a/src/containers/RelatedProgramsModal/hooks.js +++ b/src/containers/RelatedProgramsModal/hooks.js @@ -1,10 +1,10 @@ import { hooks as appHooks } from 'data/redux'; export const useProgramData = ({ - courseNumber, + cardId, }) => ({ - courseTitle: appHooks.useCardCourseData(courseNumber).title, - relatedPrograms: appHooks.useCardRelatedProgramsData(courseNumber).list, + courseTitle: appHooks.useCardCourseData(cardId).title, + relatedPrograms: appHooks.useCardRelatedProgramsData(cardId).list, }); export default useProgramData; diff --git a/src/containers/RelatedProgramsModal/hooks.test.js b/src/containers/RelatedProgramsModal/hooks.test.js index 8ada046..001f043 100644 --- a/src/containers/RelatedProgramsModal/hooks.test.js +++ b/src/containers/RelatedProgramsModal/hooks.test.js @@ -9,7 +9,7 @@ jest.mock('data/redux', () => ({ }, })); -const courseNumber = 'test-course-number'; +const cardId = 'test-course-number'; const courseTitle = 'test-course-title'; const relatedPrograms = ['some', 'programs']; @@ -18,9 +18,9 @@ describe('RelatedProgramsModal hooks', () => { it('forwards course title and related programs list by course number', () => { appHooks.useCardCourseData.mockReturnValue({ title: courseTitle }); appHooks.useCardRelatedProgramsData.mockReturnValue({ list: relatedPrograms }); - const out = hooks.useProgramData({ courseNumber }); - expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber); - expect(appHooks.useCardRelatedProgramsData).toHaveBeenCalledWith(courseNumber); + const out = hooks.useProgramData({ cardId }); + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId); + expect(appHooks.useCardRelatedProgramsData).toHaveBeenCalledWith(cardId); expect(out).toEqual({ courseTitle, relatedPrograms }); }); }); diff --git a/src/containers/RelatedProgramsModal/index.jsx b/src/containers/RelatedProgramsModal/index.jsx index 87bc366..f25f8b9 100644 --- a/src/containers/RelatedProgramsModal/index.jsx +++ b/src/containers/RelatedProgramsModal/index.jsx @@ -13,10 +13,10 @@ import './index.scss'; export const RelatedProgramsModal = ({ isOpen, closeModal, - courseNumber, + cardId, }) => { const { formatMessage } = useIntl(); - const { courseTitle, relatedPrograms } = useProgramData({ courseNumber }); + const { courseTitle, relatedPrograms } = useProgramData({ cardId }); return ( ({ useProgramData: jest.fn(), })); -const courseNumber = 'test-course-number'; +const cardId = 'test-course-number'; const hookProps = { courseTitle: 'hookProps.courseTitle', relatedPrograms: [ @@ -31,7 +31,7 @@ const hookProps = { const props = { isOpen: true, closeModal: jest.fn().mockName('props.closeModal'), - courseNumber, + cardId, }; describe('RelatedProgramsModal', () => { diff --git a/src/containers/SelectSession/SelectSessionModal.jsx b/src/containers/SelectSession/SelectSessionModal.jsx deleted file mode 100644 index 72ba23d..0000000 --- a/src/containers/SelectSession/SelectSessionModal.jsx +++ /dev/null @@ -1,82 +0,0 @@ -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/__snapshots__/SelectSessionModal.test.jsx.snap b/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap deleted file mode 100644 index 1e0f09b..0000000 --- a/src/containers/SelectSession/__snapshots__/SelectSessionModal.test.jsx.snap +++ /dev/null @@ -1,188 +0,0 @@ -// 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 deleted file mode 100644 index e921de5..0000000 --- a/src/containers/SelectSession/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index e7d686d..0000000 --- a/src/containers/SelectSession/hooks.js +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 11a7ba2..0000000 --- a/src/containers/SelectSession/hooks.test.js +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index f4cddca..0000000 --- a/src/containers/SelectSession/index.jsx +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 8200c0d..0000000 --- a/src/containers/SelectSession/index.test.jsx +++ /dev/null @@ -1,26 +0,0 @@ -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/SelectSessionModal/__snapshots__/index.test.jsx.snap b/src/containers/SelectSessionModal/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..70e0308 --- /dev/null +++ b/src/containers/SelectSessionModal/__snapshots__/index.test.jsx.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectSessionModal snapshot empty modal with leave option 1`] = ` + +

+ test-header +

+ + + test-hint + + + + Leave session + + + + + + + +
+`; + +exports[`SelectSessionModal snapshot modal with leave option 1`] = ` + +

+ test-header +

+ + + test-hint + + + + 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`] = ` + +

+ test-header +

+ + + test-hint + + + + 1/2/2000 + - + 1/2/2020 + + + 2/3/2000 + - + 2/3/2020 + + + 3/4/2000 + - + 3/4/2020 + + + + + + + +
+`; diff --git a/src/containers/SelectSessionModal/hooks.js b/src/containers/SelectSessionModal/hooks.js new file mode 100644 index 0000000..83482bd --- /dev/null +++ b/src/containers/SelectSessionModal/hooks.js @@ -0,0 +1,46 @@ +import { useDispatch } from 'react-redux'; + +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { hooks as appHooks } from 'data/redux'; + +import messages from './messages'; + +export const useSelectSessionModalData = () => { + const dispatch = useDispatch(); + const selectedCardId = appHooks.useSelectSessionModalData().cardId; + + const { + entitlementSessions, + isFulfilled, + } = appHooks.useCardEntitlementsData(selectedCardId); + + const { title: courseTitle } = appHooks.useCardCourseData(selectedCardId); + + const { formatMessage } = useIntl(); + + let header; + let hint; + if (isFulfilled) { + header = formatMessage(messages.changeOrLeaveHeader); + hint = formatMessage(messages.changeOrLeaveHint); + } else { + header = formatMessage(messages.selectSessionHeader, { + courseTitle, + }); + hint = formatMessage(messages.selectSessionHint); + } + const updateCallback = appHooks.useUpdateSelectSessionModalCallback; + + return { + showModal: selectedCardId != null, + closeSessionModal: updateCallback(dispatch, null), + openSessionModal: (cardId) => updateCallback(dispatch, cardId), + showLeaveOption: isFulfilled, + entitlementSessions, + hint, + header, + }; +}; + +export default useSelectSessionModalData; diff --git a/src/containers/SelectSessionModal/hooks.test.js b/src/containers/SelectSessionModal/hooks.test.js new file mode 100644 index 0000000..9b7471c --- /dev/null +++ b/src/containers/SelectSessionModal/hooks.test.js @@ -0,0 +1,110 @@ +import { useDispatch } from 'react-redux'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { hooks as appHooks } from 'data/redux'; + +import messages from './messages'; +import * as hooks from './hooks'; + +jest.mock('data/redux', () => ({ + hooks: { + useCardEntitlementsData: jest.fn(), + useCardCourseData: jest.fn(), + useSelectSessionModalData: jest.fn(), + useUpdateSelectSessionModalCallback: jest.fn((...args) => ({ + updateSelectSession: args, + })), + }, + actions: { + app: { + updateSelectSessionModal: jest.fn(), + }, + }, +})); + +const selectedCardId = 'test-selected-card-id'; + +const selectSessionData = { + cardId: selectedCardId, +}; + +const entitlementsData = { + entitlementSessions: [ + { startDate: '1/2/2000', endDate: '1/2/2020', cardId: 'session-id-1' }, + { startDate: '2/3/2000', endDate: '2/3/2020', cardId: 'session-id-2' }, + { startDate: '3/4/2000', endDate: '3/4/2020', cardId: 'session-id-3' }, + ], + isFullfilled: false, +}; + +const cardCourseData = { + title: 'course-title: brown fox', +}; + +const { formatMessage } = useIntl(); +const dispatch = useDispatch(); + +describe('SelectSessionModal hooks', () => { + let out; + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('useSelectSession', () => { + const runHook = ({ selectSession = {}, entitlements = {}, course = {} }) => { + appHooks.useSelectSessionModalData.mockReturnValueOnce({ + ...selectSessionData, + ...selectSession, + }); + appHooks.useCardEntitlementsData.mockReturnValueOnce({ + ...entitlementsData, + ...entitlements, + }); + appHooks.useCardCourseData.mockReturnValueOnce({ + ...cardCourseData, + ...course, + }); + out = hooks.useSelectSessionModalData(); + }; + beforeEach(() => { + runHook({}); + }); + + describe('initialization', () => { + test('loads entitlement data based on course number', () => { + expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(selectedCardId); + }); + + test('get course title based on course number', () => { + expect(appHooks.useCardCourseData).toHaveBeenCalledWith(selectedCardId); + }); + }); + + describe('output', () => { + test('showModal returns true if selectedCardId is not null or undefined', () => { + expect(out.showModal).toEqual(true); + runHook({ selectSession: { cardId: null } }); + expect(out.showModal).toEqual(false); + runHook({ selectSession: { cardId: undefined } }); + expect(out.showModal).toEqual(false); + }); + test('displays change or leave header and hint if fulfilled', () => { + expect(out.header).toEqual(formatMessage( + messages.selectSessionHeader, + { courseTitle: cardCourseData.title }, + )); + expect(out.hint).toEqual(formatMessage(messages.selectSessionHint)); + }); + test('displays select session header (w/ courseTitle) and hint if unfulfilled', () => { + runHook({ entitlements: { isFulfilled: true } }); + expect(out.header).toEqual(formatMessage(messages.changeOrLeaveHeader)); + expect(out.hint).toEqual(formatMessage(messages.changeOrLeaveHint)); + }); + test('closeSessionModal returns update callback wth dispatch and null card id', () => { + expect(out.closeSessionModal).toEqual( + appHooks.useUpdateSelectSessionModalCallback(dispatch, null), + ); + }); + }); + }); +}); diff --git a/src/containers/SelectSessionModal/index.jsx b/src/containers/SelectSessionModal/index.jsx new file mode 100644 index 0000000..edc7d59 --- /dev/null +++ b/src/containers/SelectSessionModal/index.jsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + ActionRow, + Button, + Form, + ModalDialog, +} from '@edx/paragon'; + +import { nullMethod } from 'hooks'; +import { dateFormatter } from 'utils'; + +import useSelectSessionModalData from './hooks'; +import messages from './messages'; + +export const SelectSessionModal = () => { + const { + entitlementSessions, + showModal, + closeSessionModal, + showLeaveOption, + header, + hint, + } = useSelectSessionModalData(); + + const { formatMessage, formatDate } = useIntl(); + + return ( + +

{header}

+ + {hint} + + {entitlementSessions?.map((session) => ( + + {dateFormatter(formatDate, session.startDate)} - {dateFormatter(formatDate, session.endDate)} + + ))} + {showLeaveOption && ( + + {formatMessage(messages.leaveSessionOption)} + + )} + + + + + + +
+ ); +}; + +export default SelectSessionModal; diff --git a/src/containers/SelectSession/SelectSessionModal.test.jsx b/src/containers/SelectSessionModal/index.test.jsx similarity index 64% rename from src/containers/SelectSession/SelectSessionModal.test.jsx rename to src/containers/SelectSessionModal/index.test.jsx index f8adb2d..e91da0d 100644 --- a/src/containers/SelectSession/SelectSessionModal.test.jsx +++ b/src/containers/SelectSessionModal/index.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import hooks from './hooks'; -import SelectSessionModal from './SelectSessionModal'; +import SelectSessionModal from '.'; jest.mock('./hooks', () => ({ __esModule: true, @@ -11,10 +11,11 @@ jest.mock('./hooks', () => ({ const hookReturn = { entitlementSessions: [], - showSessionModal: true, - closeSessionModal: jest.fn().mockName('useSelectSession.closeSessionModal'), - showLeaveSessionInSessionModal: true, - courseTitle: 'course-title: unit test save life', + showModal: true, + closeSessionModal: jest.fn().mockName('useSelectSessionModalData.closeSessionModal'), + showLeaveOption: true, + header: 'test-header', + hint: 'test-hint', }; const courseNumber = 'my-test-course-number'; @@ -31,7 +32,7 @@ describe('SelectSessionModal', () => { hooks.mockReturnValueOnce({ ...hookReturn, }); - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); test('modal with leave option ', () => { @@ -39,16 +40,16 @@ describe('SelectSessionModal', () => { ...hookReturn, entitlementSessions: [...availableSessions], }); - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); test('modal without leave option ', () => { hooks.mockReturnValueOnce({ ...hookReturn, entitlementSessions: [...availableSessions], - showLeaveSessionInSessionModal: false, + showLeaveOption: false, }); - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); }); diff --git a/src/containers/SelectSession/messages.js b/src/containers/SelectSessionModal/messages.js similarity index 95% rename from src/containers/SelectSession/messages.js rename to src/containers/SelectSessionModal/messages.js index 9fa5ed3..c6861b3 100644 --- a/src/containers/SelectSession/messages.js +++ b/src/containers/SelectSessionModal/messages.js @@ -10,7 +10,7 @@ export const messages = StrictDict({ selectSessionHeader: { id: 'learner-dash.selectSession.selectSessionHeader', description: 'Header for unfulfilled entitlement', - defaultMessage: 'Select a session to access {courseTitle}', + defaultMessage: 'Select a session', }, changeOrLeaveHint: { id: 'learner-dash.selectSession.changeOrLeaveHint', diff --git a/src/containers/WidgetSidebar/index.scss b/src/containers/WidgetSidebar/index.scss index 6ea7bad..71ae8fc 100644 --- a/src/containers/WidgetSidebar/index.scss +++ b/src/containers/WidgetSidebar/index.scss @@ -1,8 +1,8 @@ @import "@edx/paragon/scss/core/core"; - .widget-sidebar { + margin-top: map-get($spacers, 5); width: 400px; flex-shrink: 0; padding-left: map-get($spacers, 2); padding-right: map-get($spacers, 2); -} \ No newline at end of file +} diff --git a/src/data/constants/app.js b/src/data/constants/app.js index 1e12922..f053dc0 100644 --- a/src/data/constants/app.js +++ b/src/data/constants/app.js @@ -1,4 +1,20 @@ import { getConfig } from '@edx/frontend-platform'; +import { StrictDict } from 'utils'; export const routePath = `${getConfig().PUBLIC_PATH}:courseId`; export const locationId = window.location.pathname.slice(1); + +export const SortKeys = StrictDict({ + enrolled: 'enrolled', + title: 'title', +}); + +export const FilterKeys = StrictDict({ + inProgress: 'inProgress', + notStarted: 'notStarted', + done: 'done', + notEnrolled: 'notEnrolled', + upgraded: 'upgraded', +}); + +export const ListPageSize = 50; diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js index ac44d28..c0aaeb5 100644 --- a/src/data/redux/app/reducer.js +++ b/src/data/redux/app/reducer.js @@ -10,30 +10,25 @@ const initialState = { platformSettings: {}, suggestedCourses: [], filterState: {}, - selectSessionsModal: {}, + selectSessionModal: {}, }; +export const cardId = (val) => `card-${val}`; + // eslint-disable-next-line no-unused-vars const app = createSlice({ name: 'app', initialState, reducers: { - loadCourses: (state, { payload: { enrollments, entitlements } }) => ({ + loadCourses: (state, { payload: { courses } }) => ({ ...state, - 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 }), - {}, - ), - }, + courseData: courses.reduce( + (obj, curr, index) => ({ + ...obj, + [cardId(index)]: { ...curr, cardId: cardId(index) }, + }), + {}, + ), }), loadGlobalData: (state, { payload }) => ({ ...state, @@ -44,9 +39,7 @@ const app = createSlice({ }), updateSelectSessionModal: (state, { payload }) => ({ ...state, - selectSessionsModal: { - ...payload, - }, + selectSessionModal: { cardId: payload }, }), }, }); diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js index 4db6537..ef70879 100644 --- a/src/data/redux/app/reducer.test.js +++ b/src/data/redux/app/reducer.test.js @@ -1,4 +1,6 @@ -import { initialState, reducer, actions } from './reducer'; +import { + cardId, initialState, reducer, actions, +} from './reducer'; describe('app reducer', () => { describe('reducers', () => { @@ -12,13 +14,6 @@ describe('app reducer', () => { }, entitlements: [], }; - // const testValue = 'my-test-value'; - // const testAction = (action, expected) => { - // expect(reducer(testState, action)).toEqual({ - // ...testState, - // ...expected, - // }); - // }; describe('action handlers', () => { describe('loadCourses', () => { const courseIds = [ @@ -32,29 +27,29 @@ describe('app reducer', () => { ]; const enrollmentData = [ { - courseRun: { courseNumber: courseIds[0] }, + courseRun: { cardId: courseIds[0] }, course: 1, some: 'data', }, { - courseRun: { courseNumber: courseIds[1] }, + courseRun: { cardId: courseIds[1] }, course: 2, some: 'other data', }, { - courseRun: { courseNumber: courseIds[2] }, + courseRun: { cardId: courseIds[2] }, course: 3, some: 'still different data', }, ]; const entitlementData = [ { - courseRun: { courseNumber: entitlementIds[0] }, + courseRun: { cardId: entitlementIds[0] }, course: 4, some: 'STILL different data', }, { - courseRun: { courseNumber: entitlementIds[1] }, + courseRun: { cardId: entitlementIds[1] }, course: 5, some: 'still DIFFERENT data', }, @@ -62,23 +57,16 @@ describe('app reducer', () => { let out; beforeEach(() => { out = reducer(testState, actions.loadCourses({ - enrollments: enrollmentData, - entitlements: entitlementData, + courses: [...enrollmentData, ...entitlementData], })); }); - it('loads list of courseRun ids into enrollments field', () => { - expect(out.enrollments).toEqual([ - ...courseIds, - ...entitlementIds, - ]); - }); it('loads object keyed by courseRun ids into courseData field', () => { expect(out.courseData).toEqual({ - [courseIds[0]]: enrollmentData[0], - [courseIds[1]]: enrollmentData[1], - [courseIds[2]]: enrollmentData[2], - [entitlementIds[0]]: entitlementData[0], - [entitlementIds[1]]: entitlementData[1], + [cardId(0)]: { ...enrollmentData[0], cardId: cardId(0) }, + [cardId(1)]: { ...enrollmentData[1], cardId: cardId(1) }, + [cardId(2)]: { ...enrollmentData[2], cardId: cardId(2) }, + [cardId(3)]: { ...entitlementData[0], cardId: cardId(3) }, + [cardId(4)]: { ...entitlementData[1], cardId: cardId(4) }, }); }); }); diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index fdae538..ebcb7c5 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -1,6 +1,7 @@ import { createSelector } from 'reselect'; import { StrictDict } from 'utils'; +import { FilterKeys } from 'data/constants/app'; import * as module from './selectors'; @@ -10,24 +11,36 @@ const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb); // top-level app data selectors export const simpleSelectors = { - enrollments: mkSimpleSelector(app => app.enrollments), - entitlements: mkSimpleSelector(app => app.entitlements), courseData: mkSimpleSelector(app => app.courseData), platformSettings: mkSimpleSelector(app => app.platformSettings), suggestedCourses: mkSimpleSelector(app => app.suggestedCourses), emailConfirmation: mkSimpleSelector(app => app.emailConfirmation), enterpriseDashboards: mkSimpleSelector(app => app.enterpriseDashboards), - selectSessionsModal: mkSimpleSelector(app => app.selectSessionsModal), + selectSessionModal: mkSimpleSelector(app => app.selectSessionModal), }; -export const courseCardData = (state, courseNumber) => ( - module.simpleSelectors.courseData(state)[courseNumber] +export const numCourses = createSelector( + [module.simpleSelectors.courseData], + (courseData) => Object.keys(courseData).length, +); +export const hasCourses = createSelector([module.numCourses], (num) => num > 0); +export const hasAvailableDashboards = createSelector( + [module.simpleSelectors.enterpriseDashboards], + (data) => !!data.availableDashboards, ); -const mkCardSelector = (sel) => (state, courseNumber) => ( - sel(courseCardData(state, courseNumber)) +export const courseCardData = (state, cardId) => ( + module.simpleSelectors.courseData(state)[cardId] ); +const mkCardSelector = (sel) => (state, cardId) => { + const cardData = module.courseCardData(state, cardId); + if (cardData) { + return sel(cardData); + } + return {}; +}; + const dateSixMonthsFromNow = new Date(); dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180); @@ -43,28 +56,41 @@ export const courseCard = StrictDict({ })), course: mkCardSelector(({ course }) => ({ bannerUrl: course.bannerUrl, + courseNumber: course.courseNumber, title: course.title, website: course.website, })), - courseRun: mkCardSelector(({ courseRun }) => ({ + courseRun: mkCardSelector(({ courseRun }) => (courseRun === null ? {} : { endDate: courseRun?.endDate, + courseId: courseRun.courseId, isArchived: courseRun.isArchived, isStarted: courseRun.isStarted, isFinished: courseRun.isFinished, minPassingGrade: courseRun.minPassingGrade, })), - enrollment: mkCardSelector(({ enrollment }) => ({ - accessExpirationDate: enrollment.accessExpirationDate, - canUpgrade: enrollment.canUpgrade, - hasStarted: enrollment.hasStarted, - isAudit: enrollment.isAudit, - isAuditAccessExpired: enrollment.isAuditAccessExpired, - isEmailEnabled: enrollment.isEmailEnabled, - isVerified: enrollment.isVerified, - lastEnrolled: enrollment.lastEnrollment, - isEnrolled: enrollment.isEnrolled, - })), + enrollment: mkCardSelector(({ enrollment }) => { + if (enrollment == null) { + return { + isEnrolled: false, + }; + } + return { + accessExpirationDate: enrollment.accessExpirationDate, + canUpgrade: enrollment.canUpgrade, + hasStarted: enrollment.hasStarted, + hasFinished: enrollment.hasFinished, + isAudit: enrollment.isAudit, + isAuditAccessExpired: enrollment.isAuditAccessExpired, + isEmailEnabled: enrollment.isEmailEnabled, + isVerified: enrollment.isVerified, + lastEnrolled: enrollment.lastEnrollment, + isEnrolled: enrollment.isEnrolled, + }; + }), entitlements: mkCardSelector(({ entitlements }) => { + if (!entitlements) { + return {}; + } const deadline = new Date(entitlements.changeDeadline); const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow; return { @@ -96,7 +122,71 @@ export const courseCard = StrictDict({ })), }); +export const currentList = (state, { + sortBy, + isAscending, + filters, + pageNumber, + pageSize, +}) => { + let list = Object.values(module.simpleSelectors.courseData(state)); + if (filters.length) { + list = list.filter(course => { + if (filters.includes(FilterKeys.notEnrolled)) { + if (!course.enrollment.isEnrolled) { + return false; + } + } + if (filters.includes(FilterKeys.done)) { + if (!course.enrollment.hasFinished) { + return false; + } + } + if (filters.includes(FilterKeys.upgraded)) { + if (!course.enrollment.isVerified) { + return false; + } + } + if (filters.includes(FilterKeys.inProgress)) { + if (!course.enrollment.hasStarted) { + return false; + } + } + if (filters.includes(FilterKeys.notStarted)) { + if (course.enrollment.hasStarted) { + return false; + } + } + return true; + }); + } + if (sortBy === 'enrolled') { + list = list.sort((a, b) => { + const dateA = new Date(a.enrollment.lastEnrolled); + const dateB = new Date(b.enrollment.lastEnrolled); + if (dateA < dateB) { return isAscending ? -1 : 1; } + if (dateA > dateB) { return isAscending ? 1 : 1; } + return 0; + }); + } else { + list = list.sort((a, b) => { + const titleA = a.course.title.toLowerCase(); + const titleB = b.course.title.toLowerCase(); + if (titleA < titleB) { return isAscending ? -1 : 1; } + if (titleA > titleB) { return isAscending ? 1 : 1; } + return 0; + }); + } + return { + visible: list.slice((pageNumber - 1) * pageSize, pageNumber * pageSize), + numPages: Math.ceil(list.length / pageSize), + }; +}; + export default StrictDict({ ...simpleSelectors, courseCard, + currentList, + hasCourses, + hasAvailableDashboards, }); diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js index 7135a32..b1d33e0 100644 --- a/src/data/redux/hooks.js +++ b/src/data/redux/hooks.js @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux'; +import { actions as appActions } from './app/reducer'; import appSelectors from './app/selectors'; const { courseCard } = appSelectors; @@ -9,11 +10,17 @@ 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); +export const useSelectSessionModalData = () => useSelector(appSelectors.selectSessionModal); + +export const useHasCourses = () => useSelector(appSelectors.hasCourses); +export const useHasAvailableDashboards = () => useSelector(appSelectors.hasAvailableDashboards); +export const useCurrentCourseList = (opts) => useSelector( + state => appSelectors.currentList(state, opts), +); // eslint-disable-next-line -export const useCourseCardData = (selector) => (courseNumber) => useSelector( - (state) => selector(state, courseNumber), +export const useCourseCardData = (selector) => (cardId) => useSelector( + (state) => selector(state, cardId), ); export const useCardCertificateData = useCourseCardData(courseCard.certificates); @@ -24,3 +31,7 @@ export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements export const useCardGradeData = useCourseCardData(courseCard.grades); export const useCardProviderData = useCourseCardData(courseCard.provider); export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms); + +export const useUpdateSelectSessionModalCallback = (dispatch, cardId) => () => dispatch( + appActions.updateSelectSessionModal(cardId), +); diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js index 184d56e..2024cd2 100644 --- a/src/data/redux/thunkActions/app.js +++ b/src/data/redux/thunkActions/app.js @@ -14,8 +14,8 @@ import requests from './requests'; */ export const initialize = () => (dispatch) => ( dispatch(requests.initializeList({ - onSuccess: (({ enrollments, entitlements, ...globalData }) => { - dispatch(actions.app.loadCourses({ enrollments, entitlements })); + onSuccess: (({ courses, ...globalData }) => { + dispatch(actions.app.loadCourses({ courses })); dispatch(actions.app.loadGlobalData(globalData)); }), })) @@ -23,8 +23,8 @@ export const initialize = () => (dispatch) => ( export const refreshList = () => (dispatch) => ( dispatch(requests.initializeList({ - onSuccess: (({ enrollments, entitlements, ...globalData }) => { - dispatch(actions.app.loadCourses({ enrollments, entitlements })); + onSuccess: (({ courses, ...globalData }) => { + dispatch(actions.app.loadCourses({ courses })); dispatch(actions.app.loadGlobalData(globalData)); }), })) diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index e1ba3b8..3a6b0fa 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -16,8 +16,10 @@ import { * GET Actions *********************************************************************************/ const initializeList = () => Promise.resolve({ - enrollments: fakeData.courseRunData, - entitlements: fakeData.entitlementData, + courses: [ + ...fakeData.courseRunData, + ...fakeData.entitlementData, + ], ...fakeData.globalData, }); diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js index 2f8000e..9e89fa4 100644 --- a/src/data/services/lms/fakeData/courses.js +++ b/src/data/services/lms/fakeData/courses.js @@ -37,7 +37,9 @@ export const relatedPrograms = [ }, ]; -export const genCourseID = (index) => `course-id${index}`; +export const genCardId = (index) => `card-id${index}`; +export const genCourseId = (index) => `course-number${index}-course-id${index}`; +export const genCourseNumber = (index) => `course-number${index}`; export const genCourseTitle = (index) => `Course Name ${index}`; const logos = { @@ -59,7 +61,6 @@ const globalData = { }, enterpriseDashboards: { availableDashboards: [ - { label: 'Personal', url: '/dashboard' }, { label: 'edX, Inc.', url: '/edx-dashboard' }, { label: 'Harvard', url: '/harvard-dashboard' }, ], @@ -112,15 +113,15 @@ export const genCourseRunData = (data = {}) => ({ }); export const genEnrollmentData = (data = {}) => ({ - accessExpirationDate: futureDate, - canUpgrade: data.verified ? null : true, + accessExpirationDate: ((data.isEnrolled === false) ? null : futureDate), + canUpgrade: (data.isVerified ? null : true), + hasFinished: false, hasStarted: false, - isAudit: true, - isAuditAccessExpired: data.verified ? null : false, + isAudit: !data.isVerified || data.isEnrolled, + isAuditAccessExpired: data.isVerified ? null : false, isEmailEnabled: false, isEnrolled: true, isVerified: false, - lastEnrolled: pastDate, ...data, }); @@ -137,150 +138,169 @@ export const genCertificateData = (data = {}) => ({ }); export const availableSessions = [ - { startDate: '1/2/2000', endDate: '1/2/2020', courseNumber: genCourseID(100) }, - { startDate: '2/3/2000', endDate: '2/3/2020', courseNumber: genCourseID(101) }, - { startDate: '3/4/2000', endDate: '3/4/2020', courseNumber: genCourseID(102) }, + { startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) }, + { startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) }, + { startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) }, + { startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) }, + { startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) }, + { startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) }, + { startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) }, + { startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) }, + { startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) }, + { startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) }, + { startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) }, + { startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) }, ]; export const courseRuns = [ - // audit, pending, can upgrade + // audit, can upgrade, course not started, + {}, + // audit, can upgrade, course started { - enrollment: genEnrollmentData({ isAudit: true }), - grades: { isPassing: true }, - courseRun: { isPending: true }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, - }, - // audit, started, cannot upgrade, restricted - { - enrollment: genEnrollmentData({ isAudit: true, canUpgrade: false }), - grades: { isPassing: true }, courseRun: { isStarted: true }, - certificates: genCertificateData({ isRestricted: true }), - entitlements: { isEntitlement: false }, }, - // audit, started, can upgrade + // audit, can upgrade, course started, learner started { - enrollment: genEnrollmentData({ isAudit: true, canUpgrade: true }), - grades: { isPassing: true }, courseRun: { isStarted: true }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, + enrollment: { hasStarted: true }, }, - // audit, started, not passing + // audit, can upgrade, course started, learner started, not passing { - enrollment: genEnrollmentData({ isAudit: true, canUpgrade: true }), + courseRun: { isStarted: true }, + enrollment: { hasStarted: true }, grades: { isPassing: false }, + }, + // audit, access expired, can upgrade, course started, learner started + { courseRun: { isStarted: true }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, - }, - // audit, started, audit access expired, can upgrade - { - enrollment: genEnrollmentData({ isAudit: true, isAuditAccessExpired: true }), - grades: { isPassing: true }, - courseRun: { isStarted: true, accessExpirationDate: pastDate }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, - }, - // audit, started, audit access expired, cannot upgrade - { - enrollment: genEnrollmentData({ - isAudit: true, + enrollment: { + hasStarted: true, isAuditAccessExpired: true, + accessExpirationDate: pastDate, + }, + }, + // audit, access expired, cannot upgrade, course started, learner started + { + courseRun: { isStarted: true }, + enrollment: { + accessExpirationDate: pastDate, + isAuditAccessExpired: true, + hasStarted: true, canUpgrade: false, - }), - grades: { isPassing: true }, - courseRun: { isStarted: true, accessExpirationDate: pastDate }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, + }, }, - // verified, pending, restricted + + // verified, course not started + { enrollment: { isVerified: true } }, + // verified, course started, learner not started { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, - courseRun: { isPending: true }, - certificates: genCertificateData({ isRestricted: true }), - entitlements: { isEntitlement: false }, - }, - // verified, started - { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, courseRun: { isStarted: true }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, + enrollment: { isVerified: true }, }, - // verified, not passing + // verified, course started, learner started, passing { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), + courseRun: { isStarted: true }, + enrollment: { hasStarted: true, isVerified: true }, + }, + // verified, course started, learner started, not passing + { + courseRun: { isStarted: true }, grades: { isPassing: false }, - courseRun: { isStarted: true }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, + enrollment: { hasStarted: true, isVerified: true }, }, - // verified, finished, not passing + // verified, learner started, not finished, not passing { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), + enrollment: { hasStarted: true, isVerified: true }, grades: { isPassing: false }, - courseRun: { isArchived: true, endDate: pastDate }, - certificates: genCertificateData(), - entitlements: { isEntitlement: false }, }, - // verified, restricted + // verified, learner finished, passing, restricted { - enrollment: genEnrollmentData({ isVerified: true }), - grades: { isPassing: true }, + enrollment: { + hasFinished: true, + hasStarted: true, + isVerified: true, + }, courseRun: { isStarted: true }, - certificates: genCertificateData({ isRestricted: true }), - entitlements: { isEntitlement: false }, + certificates: { isRestricted: true }, }, - // verified, earned but not available + // verified, learner finished, passing, cert earned but not available { - enrollment: genEnrollmentData({ isVerified: true }), - grades: { isPassing: true }, + enrollment: { + hasFinished: true, + hasStarted: true, + isVerified: true, + }, courseRun: { isStarted: true }, - certificates: genCertificateData({ + certificates: { isEarned: true, availableDate: futureDate, - }), - entitlements: { isEntitlement: false }, + isAvailable: false, + }, }, - // verified, earned, downloadable (web + link) + // verified, learner finished, cert earned, downloadable (web + link) { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, + enrollment: { + hasFinished: true, + hasStarted: true, + isVerified: true, + }, courseRun: { isStarted: true }, - certificates: genCertificateData({ + certificates: { isEarned: true, isAvailable: true, isDownloadable: true, availableDate: pastDate, certDownloadUrl: logos.social, certPreviewUrl: logos.edx, - }), - entitlements: { isEntitlement: false }, + }, }, - // verified, earned, downloadable (link) + // verified, learner finished, cert earned, downloadable (link only) { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, + enrollment: { + hasFinished: true, + hasStarted: true, + isVerified: true, + }, courseRun: { isStarted: true }, - certificates: genCertificateData({ + certificates: { isEarned: true, isAvailable: true, isDownloadable: true, availableDate: pastDate, certDownloadUrl: logos.social, - }), - entitlements: { isEntitlement: false }, + }, + }, + // verified, course archived, learner finished, cert earned, downloadable (web + link) + { + enrollment: { + hasFinished: true, + hasStarted: true, + isVerified: true, + }, + courseRun: { + isStarted: true, + isArchived: true, + endDate: pastDate, + }, + certificates: { + isEarned: true, + isAvailable: true, + isDownloadable: true, + availableDate: pastDate, + certDownloadUrl: logos.social, + certPreviewUrl: logos.edx, + }, + }, + // verified, course archived, learner started, not finished, not passing + { + enrollment: { hasStarted: true, isVerified: true }, + grades: { isPassing: false }, + courseRun: { isArchived: true, endDate: pastDate }, }, // Entitlement Course Run - Cannot view yet { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, - courseRun: { isPending: true }, - certificates: genCertificateData(), + enrollment: { isVerified: true }, + courseRun: { isStarted: false }, entitlements: { isEntitlement: true, isFulfilled: true, @@ -294,10 +314,8 @@ export const courseRuns = [ }, // Entitlement Course Run - Can View and Change { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, + enrollment: { isVerified: true }, courseRun: { isStarted: true }, - certificates: genCertificateData(), entitlements: { isEntitlement: true, isFulfilled: true, @@ -311,10 +329,8 @@ export const courseRuns = [ }, // Entitlement Course Run - Can View but not Change { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, + enrollment: { isVerified: true }, courseRun: { isStarted: true }, - certificates: genCertificateData(), entitlements: { isEntitlement: true, isFulfilled: true, @@ -327,14 +343,8 @@ export const courseRuns = [ }, // Entitlement Course Run - Expired { - enrollment: genEnrollmentData({ isAudit: false, isVerified: true }), - grades: { isPassing: true }, - courseRun: { - isStarted: true, - isArchived: true, - endDate: pastDate, - }, - certificates: genCertificateData(), + enrollment: { isVerified: true }, + courseRun: { isStarted: true, isArchived: true, endDate: pastDate }, entitlements: { isEntitlement: true, isFulfilled: true, @@ -406,28 +416,41 @@ export const entitlementCourses = [ export const courseRunData = courseRuns.map( (data, index) => { const title = genCourseTitle(index); - const courseNumber = genCourseID(index); + const cardId = genCardId(index); + const courseId = genCourseId(index); + const courseNumber = genCourseNumber(index); const providerIndex = index % 3; + const lastEnrolled = new Date(); + lastEnrolled.setDate(lastEnrolled.getDate() - index); const iteratedData = [ { provider: providers.edx, - course: { title, bannerUrl: logos.edx }, + course: { title, bannerUrl: logos.edx, courseNumber }, relatedPrograms, }, { provider: providers.mit, - course: { title, bannerUrl: logos.science }, + course: { title, bannerUrl: logos.science, courseNumber }, relatedPrograms: [relatedPrograms[0]], }, { provider: null, - course: { title, bannerUrl: logos.social }, + course: { title, bannerUrl: logos.social, courseNumber }, relatedPrograms: [], }, ]; return { + cardId, + grades: { isPassing: true }, + entitlements: null, ...data, - courseRun: genCourseRunData({ ...data.courseRun, courseNumber }), + certificates: genCertificateData(data.certificates), + enrollment: genEnrollmentData(data.enrollment), + courseRun: genCourseRunData({ + ...data.courseRun, + courseId, + lastEnrolled, + }), ...iteratedData[providerIndex], }; }, @@ -436,31 +459,44 @@ export const courseRunData = courseRuns.map( export const entitlementData = entitlementCourses.map( (data, index) => { const title = genCourseTitle(100 + index); - const courseNumber = genCourseID(100 + index); + const cardId = genCardId(100 + index); + const courseNumber = genCourseNumber(100 + index); const providerIndex = index % 3; const iteratedData = [ { provider: providers.edx, - course: { title, bannerUrl: logos.edx }, + course: { courseNumber, title, bannerUrl: logos.edx }, relatedPrograms, }, { provider: providers.mit, - course: { title, bannerUrl: logos.science }, + course: { courseNumber, title, bannerUrl: logos.science }, relatedPrograms: [relatedPrograms[0]], }, { provider: null, - course: { title, bannerUrl: logos.social }, + course: { courseNumber, title, bannerUrl: logos.social }, relatedPrograms: [], }, ]; return { + cardId, + enrollment: genEnrollmentData({ + isEnrolled: false, + lastEnrolled: null, + accessExpirationDate: null, + canUpgrade: false, + hasFinished: false, + hasStarted: false, + isAudit: false, + isAuditAccessExpired: false, + isEmailEnabled: false, + isVerified: false, + }), + grades: null, + certificates: null, + courseRun: null, ...data, - enrollment: genEnrollmentData(), - grades: { isPassing: true }, - certificates: genCertificateData(), - courseRun: genCourseRunData({ ...data.courseRun, courseNumber }), ...iteratedData[providerIndex], }; }, diff --git a/src/data/services/lms/shapes.js b/src/data/services/lms/shapes.js index 5df0455..d4d76f3 100644 --- a/src/data/services/lms/shapes.js +++ b/src/data/services/lms/shapes.js @@ -14,7 +14,7 @@ export const shapes = StrictDict({ }), courseRun: PropTypes.shape({ accessExpirationDate: PropTypes.string, - courseNumber: PropTypes.string, + cardId: PropTypes.string, isArchived: PropTypes.bool, isFinished: PropTypes.bool, isPending: PropTypes.bool, @@ -43,7 +43,7 @@ export const shapes = StrictDict({ availableSessions: PropTypes.shape({ startDate: PropTypes.string, endDate: PropTypes.string, - courseNumber: PropTypes.string, + cardId: PropTypes.string, }), canChange: PropTypes.bool, isFulfilled: PropTypes.bool, diff --git a/src/setupTest.jsx b/src/setupTest.jsx index a092edf..d882fb6 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -125,6 +125,7 @@ jest.mock('@edx/paragon/icons', () => ({ })); jest.mock('data/constants/app', () => ({ + ...jest.requireActual('data/constants/app'), locationId: 'fake-location-id', })); diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx index e12f513..4b48031 100644 --- a/src/test/app.test.jsx +++ b/src/test/app.test.jsx @@ -20,6 +20,7 @@ import { RequestKeys, RequestStates } from 'data/constants/requests'; import reducers from 'data/redux'; import messages from 'i18n'; import { selectors } from 'data/redux'; +import { cardId as genCardId } from 'data/redux/app/reducer'; import App from 'App'; import Inspector from './inspector'; @@ -79,8 +80,10 @@ const mockApi = () => { (resolve, reject) => { resolveFns.init = { success: () => resolve({ - enrollments: fakeData.courseRunData, - entitlements: fakeData.entitlementData, + courses: [ + ...fakeData.courseRunData, + ...fakeData.entitlementData, + ], ...fakeData.globalData, }), }; @@ -130,27 +133,28 @@ describe('ESG app integration tests', () => { await inspector.findByText(fakeData.courseRunData[0].course.title); const cards = inspector.get.courseCards; - let card = cards.at(0); - let courseNumber; + let cardId; let courseData; let cardDetails; await getState(); // Card 1 is Audit, pending, and can upgrade - courseNumber = state.app.enrollments[0]; - courseData = state.app.courseData[courseNumber]; + cardId = genCardId(0); + courseData = state.app.courseData[cardId]; expect(courseData.enrollment.isAudit).toEqual(true); - expect(courseData.courseRun.isPending).toEqual(true); + expect(courseData.courseRun.isStarted).toEqual(false); expect(courseData.enrollment.canUpgrade).toEqual(true); + + let card = cards.at(0); + inspector.verifyText( inspector.get.card.header(card), courseData.course.title, ); cardDetails = inspector.get.card.details(card); - [ courseData.provider.name, - courseNumber, + courseData.course.courseNumber, appMessages.withValues.CourseCardDetails.accessExpires({ accessExpirationDate: courseData.enrollment.accessExpirationDate, }), diff --git a/src/test/inspector.js b/src/test/inspector.js index 3f68e96..c04bf5e 100644 --- a/src/test/inspector.js +++ b/src/test/inspector.js @@ -27,9 +27,9 @@ class Inspector { card: { header: (card) => within(card).getByTestId('CourseCardTitle'), details: (card) => within(card).getByTestId('CourseCardDetails'), - banners: (card) => within(card).getByTestId('CourseCardBanners'), - programsBadge: (card) => within(card).getByTestId('RelatedProgramsBadge'), - actions: (card) => within(card).getByTestId('CourseCardActions'), + // banners: (card) => within(card).getByTestId('CourseCardBanners'), + // programsBadge: (card) => within(card).getByTestId('RelatedProgramsBadge'), + // actions: (card) => within(card).getByTestId('CourseCardActions'), }, }; }