diff --git a/package-lock.json b/package-lock.json index adc8dbd..3e10032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0", "dependencies": { "@edx/brand": "npm:@edx/brand-edx.org@^2.0.3", - "@edx/frontend-component-footer": "@edx/frontend-component-footer@11.0.0", + "@edx/frontend-component-footer": "11.1.0", "@edx/frontend-platform": "^2.3.0", "@edx/paragon": "19.25.0", "@fortawesome/fontawesome-svg-core": "^1.2.36", @@ -112,7 +112,9 @@ "react-dom": "^16.9.0" } }, - "@edx/frontend-component-footer@11.0.0": {}, + "@edx/frontend-component-footer@11.0.0": { + "extraneous": true + }, "node_modules/@babel/cli": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz", @@ -3530,8 +3532,22 @@ } }, "node_modules/@edx/frontend-component-footer": { - "resolved": "@edx/frontend-component-footer@11.0.0", - "link": true + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-11.1.0.tgz", + "integrity": "sha512-cbcUj3we2whEDQl1GkVcSrfVmD7xFyVCKO9MwCVZyXiSGbymHxEc0A+LuCkZAY22kmOkv5SG9H3rmG1qG7DIeQ==", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "1.2.36", + "@fortawesome/free-brands-svg-icons": "5.15.4", + "@fortawesome/free-regular-svg-icons": "5.15.4", + "@fortawesome/free-solid-svg-icons": "5.15.4", + "@fortawesome/react-fontawesome": "0.1.18" + }, + "peerDependencies": { + "@edx/frontend-platform": "^2.3.0", + "prop-types": "^15.5.10", + "react": "^16.9.0", + "react-dom": "^16.9.0" + } }, "node_modules/@edx/frontend-platform": { "version": "2.3.0", @@ -3819,6 +3835,18 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz", + "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "5.15.4", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", @@ -36230,7 +36258,16 @@ } }, "@edx/frontend-component-footer": { - "version": "file:@edx/frontend-component-footer@11.0.0" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-11.1.0.tgz", + "integrity": "sha512-cbcUj3we2whEDQl1GkVcSrfVmD7xFyVCKO9MwCVZyXiSGbymHxEc0A+LuCkZAY22kmOkv5SG9H3rmG1qG7DIeQ==", + "requires": { + "@fortawesome/fontawesome-svg-core": "1.2.36", + "@fortawesome/free-brands-svg-icons": "5.15.4", + "@fortawesome/free-regular-svg-icons": "5.15.4", + "@fortawesome/free-solid-svg-icons": "5.15.4", + "@fortawesome/react-fontawesome": "0.1.18" + } }, "@edx/frontend-platform": { "version": "2.3.0", @@ -36482,6 +36519,14 @@ "@fortawesome/fontawesome-common-types": "^0.2.36" } }, + "@fortawesome/free-regular-svg-icons": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz", + "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + } + }, "@fortawesome/free-solid-svg-icons": { "version": "5.15.4", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", diff --git a/package.json b/package.json index d03778c..6cc8e83 100755 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@edx/brand": "npm:@edx/brand-edx.org@^2.0.3", - "@edx/frontend-component-footer": "11.0.0", + "@edx/frontend-component-footer": "11.1.0", "@edx/frontend-platform": "^2.3.0", "@edx/paragon": "19.25.0", "@fortawesome/fontawesome-svg-core": "^1.2.36", diff --git a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx index 981e8ed..104043d 100644 --- a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx @@ -7,25 +7,31 @@ import { CheckCircle } from '@edx/paragon/icons'; import { selectors } from 'data/redux'; import Banner from 'components/Banner'; -import { getCardValue, getCardValues } from 'hooks'; +import { useCardValues } from 'hooks'; const { cardData } = selectors; const restrictedMessage = 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting '; export const CertificateBanner = ({ courseNumber }) => { - const cardValue = getCardValue(courseNumber); - - const { isRestricted, isAudit, isVerified } = getCardValues(courseNumber, { - isRestricted: cardData.isRestricted, + const data = useCardValues(courseNumber, { + certAvailableDate: cardData.certAvailableDate, + certDownloadUrl: cardData.certDownloadUrl, + certPreviewUrl: cardData.certPreviewUrl, isAudit: cardData.isAudit, + isCertDownloadable: cardData.isCertDownloadable, + isCertEarnedButUnavailable: cardData.isCertEarnedButUnavailable, + isCourseRunFinished: cardData.isCourseRunFinished, + isPassing: cardData.isPassing, + isRestricted: cardData.isRestricted, isVerified: cardData.isVerified, + minPassingGrade: cardData.minPassingGrade, }); - if (isRestricted) { + if (data.isRestricted) { return ( {restrictedMessage}info@example.com - {isVerified && ( + {data.isVerified && ( <> If you would like a refund on your Certificate of Achievement, please contact our billing address billing@example.com @@ -33,26 +39,11 @@ export const CertificateBanner = ({ courseNumber }) => { ); } - const { - isPassing, - minPassingGrade, - isCourseRunFinished, - isCertDownloadable, - isCertEarnedButUnavailable, - certAvailableDate, - } = getCardValues(courseNumber, { - isPassing: cardData.isPassing, - minPassingGrade: cardData.minPassingGrade, - isCourseRunFinished: cardData.isCourseRunFinished, - isCertDownloadable: cardData.isCertDownloadable, - isCertEarnedButUnavailable: cardData.isCertEarnedButUnavailable, - certAvailableDate: cardData.certAvailableDate, - }); - if (!isPassing) { - if (isAudit) { - return ( Grade required to pass the course: {minPassingGrade}% ); + if (!data.isPassing) { + if (data.isAudit) { + return ( Grade required to pass the course: {data.minPassingGrade}% ); } - if (isCourseRunFinished) { + if (data.isCourseRunFinished) { return ( You are not eligible for a certificate. View grades. @@ -61,19 +52,17 @@ export const CertificateBanner = ({ courseNumber }) => { } return ( - Grade required for a certificate: {minPassingGrade}% + Grade required for a certificate: {data.minPassingGrade}% ); } - if (isCertDownloadable) { - const certDownloadUrl = cardValue(cardData.certDownloadUrl); - const certPreviewUrl = cardValue(cardData.certPreviewUrl); - if (certPreviewUrl) { + if (data.isCertDownloadable) { + if (data.certPreviewUrl) { return ( Congratulations. Your certificate is ready. {' '} - View Certificate. + View Certificate. ); } @@ -81,14 +70,14 @@ export const CertificateBanner = ({ courseNumber }) => { Congratulations. Your certificate is ready. {' '} - Download Certificate. + Download Certificate. ); } - if (isCertEarnedButUnavailable) { + if (data.isCertEarnedButUnavailable) { return ( - Your grade and certificate will be ready after {certAvailableDate}. + Your grade and certificate will be ready after {data.certAvailableDate}. ); } diff --git a/src/containers/CourseCard/components/Banners/CourseBanner.jsx b/src/containers/CourseCard/components/Banners/CourseBanner.jsx index 5559423..a642e07 100644 --- a/src/containers/CourseCard/components/Banners/CourseBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CourseBanner.jsx @@ -1,9 +1,9 @@ /* eslint-disable max-len */ import React from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; import { Hyperlink } from '@edx/paragon'; +import { useCardValues } from 'hooks'; import { selectors } from 'data/redux'; import Banner from 'components/Banner'; @@ -11,15 +11,18 @@ import Banner from 'components/Banner'; const { cardData } = selectors; export const CourseBanner = ({ courseNumber }) => { - const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); - const isVerified = cardValue(cardData.isVerified); - if (isVerified) { return null; } + const courseData = useCardValues(courseNumber, { + isVerified: cardData.isVerified, + isCourseRunActive: cardData.isCourseRunActive, + canUpgrade: cardData.canUpgrade, + isAuditAccessExpired: cardData.isAuditAccessExpired, + courseWebsite: cardData.courseWebsite, + }); - const isCourseRunActive = cardValue(cardData.isCourseRunActive); - const canUpgrade = cardValue(cardData.canUpgrade); - const isAuditAccessExpired = cardValue(cardData.isAuditAccessExpired); - if (isAuditAccessExpired) { - if (canUpgrade) { + if (courseData.isVerified) { return null; } + + if (courseData.isAuditAccessExpired) { + if (courseData.canUpgrade) { return ( Your audit access to this course has expired. Upgrade now to access your course again. @@ -32,13 +35,12 @@ export const CourseBanner = ({ courseNumber }) => { ); } - if (isCourseRunActive && !canUpgrade) { - const courseWebsite = cardValue(cardData.courseWebsite); + if (courseData.isCourseRunActive && !courseData.canUpgrade) { return ( Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future. {' '} - Explore course details. + Explore course details. ); } diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx index 1bbee7d..9e0de18 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; +import { useCardValues } from 'hooks'; import { selectors } from 'data/redux'; import Banner from 'components/Banner'; @@ -9,18 +9,20 @@ import Banner from 'components/Banner'; const { cardData } = selectors; export const EntitlementBanner = ({ courseNumber }) => { - const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); - const isEntitlement = cardValue(cardData.isEntitlement); - if (!isEntitlement) { + const data = useCardValues(courseNumber, { + canChange: cardData.canChangeEntitlementSession, + isEntitlement: cardData.isEntitlement, + isExpired: cardData.isEntitlementExpired, + isFulfilled: cardData.isEntitlementFulfilled, + }); + + if (!data.isEntitlement) { return null; } - const isExpired = cardValue(cardData.isEntitlementExpired); - const isFulfilled = cardValue(cardData.isEntitlementFulfilled); - if (isExpired || isFulfilled) { + if (data.isExpired || data.isFulfilled) { return null; } - const canChange = cardValue(cardData.canChangeEntitlementSession); - return canChange + return data.canChange ? (You must select a session to access the course.) : (The deadline to select a session has passed); }; diff --git a/src/containers/CourseCard/components/CourseCardActions/hooks.js b/src/containers/CourseCard/components/CourseCardActions/hooks.js index c9dcb03..2661f5a 100644 --- a/src/containers/CourseCard/components/CourseCardActions/hooks.js +++ b/src/containers/CourseCard/components/CourseCardActions/hooks.js @@ -1,14 +1,14 @@ import { Locked } from '@edx/paragon/icons'; import { selectors } from 'data/redux'; -import { useIntl, getCardValues } from 'hooks'; +import { useIntl, useCardValues } from 'hooks'; import messages from './messages'; const { cardData } = selectors; -export const actionHooks = ({ courseNumber }) => { +export const useCardActionData = ({ courseNumber }) => { const { formatMessage } = useIntl(); - const data = getCardValues(courseNumber, { + const data = useCardValues(courseNumber, { canUpgrade: cardData.canUpgrade, isAudit: cardData.isAudit, isAuditAccessExpired: cardData.isAuditAccessExpired, @@ -39,4 +39,4 @@ export const actionHooks = ({ courseNumber }) => { return { primary, secondary }; }; -export default actionHooks; +export default useCardActionData; diff --git a/src/containers/CourseCard/components/CourseCardActions/hooks.test.js b/src/containers/CourseCard/components/CourseCardActions/hooks.test.js index 8b9be9f..8b14c0c 100644 --- a/src/containers/CourseCard/components/CourseCardActions/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardActions/hooks.test.js @@ -25,7 +25,7 @@ describe('CourseCardActions hooks', () => { const { formatMessage } = appHooks.useIntl(); describe('data connection', () => { beforeEach(() => { - out = hooks.actionHooks({ courseNumber }); + out = hooks.useCardActionData({ courseNumber }); }); testCardValues(courseNumber, { canUpgrade: fieldKeys.canUpgrade, @@ -38,17 +38,17 @@ describe('CourseCardActions hooks', () => { }); describe('secondary action', () => { it('returns null if verified', () => { - appHooks.getCardValues.mockReturnValueOnce({ + appHooks.useCardValues.mockReturnValueOnce({ ...props, isAudit: false, isVerified: true, }); - out = hooks.actionHooks({ courseNumber }); + out = hooks.useCardActionData({ courseNumber }); expect(out.secondary).toEqual(null); }); it('returns disabled upgrade button if audit, but cannot upgrade', () => { - appHooks.getCardValues.mockReturnValueOnce(props); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce(props); + out = hooks.useCardActionData({ courseNumber }); expect(out.secondary).toEqual({ iconBefore: Locked, variant: 'outline-primary', @@ -57,8 +57,8 @@ describe('CourseCardActions hooks', () => { }); }); it('returns enabled upgrade button if audit and can upgrade', () => { - appHooks.getCardValues.mockReturnValueOnce({ ...props, canUpgrade: true }); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ ...props, canUpgrade: true }); + out = hooks.useCardActionData({ courseNumber }); expect(out.secondary).toEqual({ iconBefore: Locked, variant: 'outline-primary', @@ -69,37 +69,37 @@ describe('CourseCardActions hooks', () => { }); describe('primary action', () => { it('returns Begin Course button if pending', () => { - appHooks.getCardValues.mockReturnValueOnce({ ...props, isPending: true }); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ ...props, isPending: true }); + out = hooks.useCardActionData({ courseNumber }); expect(out.primary).toEqual({ children: formatMessage(messages.beginCourse), }); }); it('returns enabled Resume button if active, and not audit with expired access', () => { - appHooks.getCardValues.mockReturnValueOnce({ ...props, isAuditAccessExpired: true }); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ ...props, isAuditAccessExpired: true }); + out = hooks.useCardActionData({ courseNumber }); expect(out.primary).toEqual({ children: formatMessage(messages.resume), disabled: true, }); }); it('returns disabled Resume button if active and audit without expired access', () => { - appHooks.getCardValues.mockReturnValueOnce({ ...props }); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ ...props }); + out = hooks.useCardActionData({ courseNumber }); expect(out.primary).toEqual({ children: formatMessage(messages.resume), disabled: false, }); - appHooks.getCardValues.mockReturnValueOnce({ ...props, isAudit: false, isVerified: true }); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ ...props, isAudit: false, isVerified: true }); + out = hooks.useCardActionData({ courseNumber }); expect(out.primary).toEqual({ children: formatMessage(messages.resume), disabled: false, }); }); it('returns viewCourse button if finished', () => { - appHooks.getCardValues.mockReturnValueOnce({ ...props, isFinished: true }); - out = hooks.actionHooks({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ ...props, isFinished: true }); + out = hooks.useCardActionData({ courseNumber }); expect(out.primary).toEqual({ children: formatMessage(messages.viewCourse), }); diff --git a/src/containers/CourseCard/components/CourseCardActions/index.jsx b/src/containers/CourseCard/components/CourseCardActions/index.jsx index a4742c8..0867018 100644 --- a/src/containers/CourseCard/components/CourseCardActions/index.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/index.jsx @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; -import hooks from './hooks'; +import useCardActionData from './hooks'; export const CourseCardActions = ({ courseNumber }) => { - const { primary, secondary } = hooks({ courseNumber }); + const { primary, secondary } = useCardActionData({ courseNumber }); return ( <> {(secondary !== null) && ( diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.js index 754bca4..bcbb012 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/hooks.js +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.js @@ -3,11 +3,11 @@ import { StrictDict } from 'utils'; import * as module from './hooks'; export const state = StrictDict({ - isUnenrollConfirmVisible: (val) => React.useState(val), - isEmailSettingsVisible: (val) => React.useState(val), + isUnenrollConfirmVisible: (val) => React.useState(val), // eslint-disable-line + isEmailSettingsVisible: (val) => React.useState(val), // eslint-disable-line }); -export const unenrollModalHooks = () => { +export const useUnenrollData = () => { const [isVisible, setIsVisible] = module.state.isUnenrollConfirmVisible(false); return { show: () => setIsVisible(true), @@ -16,7 +16,7 @@ export const unenrollModalHooks = () => { }; }; -export const emailSettingsModalHooks = () => { +export const useEmailSettings = () => { const [isVisible, setIsVisible] = module.state.isEmailSettingsVisible(false); return { show: () => setIsVisible(true), @@ -25,13 +25,13 @@ export const emailSettingsModalHooks = () => { }; }; -export const menuHooks = () => { - const unenrollModal = module.unenrollModalHooks(); - const emailSettingsModal = module.emailSettingsModalHooks(); +export const useCardMenuData = () => { + const unenrollModal = module.useUnenrollData(); + const emailSettingsModal = module.useEmailSettings(); return { emailSettingsModal, unenrollModal, }; }; -export default menuHooks; +export default useCardMenuData; diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx index f61995d..d696c89 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -6,13 +6,13 @@ import { MoreVert } from '@edx/paragon/icons'; import EmailSettingsModal from 'containers/EmailSettingsModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; -import hooks from './hooks'; +import useCourseCardMenuData from './hooks'; export const CourseCardMenu = ({ courseNumber }) => { const { emailSettingsModal, unenrollModal, - } = hooks(); + } = useCourseCardMenuData(); return ( <> diff --git a/src/containers/CourseCard/hooks.js b/src/containers/CourseCard/hooks.js index e6538c1..94344c5 100644 --- a/src/containers/CourseCard/hooks.js +++ b/src/containers/CourseCard/hooks.js @@ -1,16 +1,16 @@ import { selectors } from 'data/redux'; -import { useIntl, getCardValues } from 'hooks'; +import { useIntl, useCardValues } from 'hooks'; import * as module from './hooks'; import messages from './messages'; const { cardData } = selectors; -export const accessMessage = ({ courseNumber }) => { +export const useAccessMessage = ({ courseNumber }) => { const { formatMessage, formatDate } = useIntl(); - const data = getCardValues(courseNumber, { + const data = useCardValues(courseNumber, { accessExpirationDate: cardData.courseRunAccessExpirationDate, isAudit: cardData.isAudit, isFinished: cardData.isCourseRunFinished, @@ -29,9 +29,9 @@ export const accessMessage = ({ courseNumber }) => { ); }; -export const cardHooks = ({ courseNumber }) => { +export const useCardData = ({ courseNumber }) => { const { formatMessage } = useIntl(); - const data = getCardValues(courseNumber, { + const data = useCardValues(courseNumber, { title: cardData.courseTitle, bannerUrl: cardData.courseBannerUrl, providerName: cardData.providerName, @@ -41,9 +41,9 @@ export const cardHooks = ({ courseNumber }) => { title: data.title, bannerUrl: data.bannerUrl, providerName: data.providerName || formatMessage(messages.unknownProviderName), - accessMessage: module.accessMessage({ courseNumber }), + accessMessage: module.useAccessMessage({ courseNumber }), formatMessage, }; }; -export default cardHooks; +export default useCardData; diff --git a/src/containers/CourseCard/hooks.test.js b/src/containers/CourseCard/hooks.test.js index 69d6f79..86345ad 100644 --- a/src/containers/CourseCard/hooks.test.js +++ b/src/containers/CourseCard/hooks.test.js @@ -9,35 +9,37 @@ import messages from './messages'; const { fieldKeys } = selectors.cardData; const courseNumber = 'my-test-course-number'; +const useAccessMessage = 'test-access-message'; +const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage }); +const hookKeys = keyStore(hooks); describe('CourseCard hooks', () => { let out; const { formatMessage, formatDate } = appHooks.useIntl(); - describe('cardHooks', () => { - const accessMessage = 'test-access-message'; - const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, accessMessage }); - const hookKeys = keyStore(hooks); + describe('useCardData', () => { beforeEach(() => { - jest.spyOn(hooks, hookKeys.accessMessage).mockImplementationOnce(mockAccessMessage); - out = hooks.cardHooks({ courseNumber }); + jest.spyOn(hooks, hookKeys.useAccessMessage).mockImplementationOnce(mockAccessMessage); + out = hooks.useCardData({ courseNumber }); }); + testCardValues(courseNumber, { title: fieldKeys.courseTitle, bannerUrl: fieldKeys.courseBannerUrl, providerName: fieldKeys.providerName, }); + test('providerName returns Unknown message if not provided', () => { - appHooks.getCardValues.mockReturnValueOnce({ + appHooks.useCardValues.mockReturnValueOnce({ title: 'title', bannerUrl: 'bannerUrl', providerName: null, }); - jest.spyOn(hooks, hookKeys.accessMessage).mockImplementationOnce(mockAccessMessage); - out = hooks.cardHooks({ courseNumber }); + jest.spyOn(hooks, hookKeys.useAccessMessage).mockImplementationOnce(mockAccessMessage); + out = hooks.useCardData({ courseNumber }); expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName)); }); - describe('accessMessage', () => { - it('returns the output of accessMessage hook, passed courseNumber', () => { + describe('useAccessMessage', () => { + it('returns the output of useAccessMessage hook, passed courseNumber', () => { expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber })); }); }); @@ -45,14 +47,20 @@ describe('CourseCard hooks', () => { expect(out.formatMessage).toEqual(formatMessage); }); }); - describe('accessMessage', () => { + + describe('useAccessMessage', () => { const accessExpirationDate = 'test-expiration-date'; const endDate = 'test-end-date'; + + beforeEach(() => { + appHooks.useCardValues.mockClear(); + }); + describe('loaded data', () => { beforeEach(() => { - appHooks.getCardValues.mockClear(); - out = hooks.accessMessage({ courseNumber }); + out = hooks.useAccessMessage({ courseNumber }); }); + testCardValues(courseNumber, { accessExpirationDate: fieldKeys.courseRunAccessExpirationDate, isAudit: fieldKeys.isAudit, @@ -61,61 +69,65 @@ describe('CourseCard hooks', () => { endDate: fieldKeys.courseRunEndDate, }); }); + describe('if audit, and expired', () => { - it('returns accessExpires message with accessExpirationDate from cardData', () => { - appHooks.getCardValues.mockReturnValueOnce({ + it('returns accessExpired message with accessExpirationDate from cardData', () => { + appHooks.useCardValues.mockReturnValueOnce({ accessExpirationDate, endDate, isAudit: true, isFinished: false, isAuditAccessExpired: true, }); - expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage( + expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage( messages.accessExpired, { accessExpirationDate: formatDate(accessExpirationDate) }, )); }); }); + describe('if audit and not expired', () => { it('returns accessExpires message with accessExpirationDate from cardData', () => { - appHooks.getCardValues.mockReturnValueOnce({ + appHooks.useCardValues.mockReturnValueOnce({ accessExpirationDate, endDate, isAudit: true, isFinished: false, isAuditAccessExpired: false, }); - expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage( + expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage( messages.accessExpires, { accessExpirationDate: formatDate(accessExpirationDate) }, )); }); }); + describe('if verified and not ended', () => { - it('returns accessExpires message with accessExpirationDate from cardData', () => { - appHooks.getCardValues.mockReturnValueOnce({ + it('returns course ends message with course end date', () => { + appHooks.useCardValues.mockReturnValueOnce({ accessExpirationDate, endDate, isAudit: false, isFinished: false, - isAuditAccessExpired: true, + isAuditAccessExpired: false, }); - expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage( + expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage( messages.courseEnds, { endDate: formatDate(endDate) }, )); }); }); + describe('if verified and ended', () => { - it('returns accessExpires message with accessExpirationDate from cardData', () => { - appHooks.getCardValues.mockReturnValueOnce({ + it('returns course ended message with course end date', () => { + appHooks.useCardValues.mockReturnValueOnce({ accessExpirationDate, endDate, isAudit: false, isFinished: true, - isAuditAccessExpired: true, + isAuditAccessExpired: false, }); - expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage( + expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage( messages.courseEnded, { endDate: formatDate(endDate) }, )); diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx index 1aed1d9..dd8d227 100644 --- a/src/containers/CourseCard/index.jsx +++ b/src/containers/CourseCard/index.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; // import PropTypes from 'prop-types'; import { Card } from '@edx/paragon'; -import hooks from './hooks'; +import useCardData from './hooks'; import RelatedProgramsBadge from './components/RelatedProgramsBadge'; import CourseCardMenu from './components/CourseCardMenu'; @@ -23,7 +23,7 @@ export const CourseCard = ({ courseNumber }) => { providerName, accessMessage, formatMessage, - } = hooks({ courseNumber }); + } = useCardData({ courseNumber }); return (
diff --git a/src/containers/Dashboard/index.jsx b/src/containers/Dashboard/index.jsx index b484c20..ae3708e 100644 --- a/src/containers/Dashboard/index.jsx +++ b/src/containers/Dashboard/index.jsx @@ -12,23 +12,23 @@ import EmptyCourse from 'containers/EmptyCourse'; import messages from './messages'; import * as module from '.'; -export const hooks = ({ dispatch }) => ({ - initialize: () => React.useEffect( +export const useDashboardData = ({ dispatch }) => { + React.useEffect( () => { dispatch(thunkActions.app.initialize()); }, - [], - ), - enrollments: useSelector(selectors.app.enrollments), - entitlements: useSelector(selectors.app.entitlements), -}); + [dispatch], + ); + return { + enrollments: useSelector(selectors.app.enrollments), + entitlements: useSelector(selectors.app.entitlements), + }; +}; export const Dashboard = () => { const dispatch = useDispatch(); const { - initialize, enrollments, // entitlements, - } = module.hooks({ dispatch }); - initialize(); + } = module.useDashboardData({ dispatch }); return (
{enrollments.length ? ( diff --git a/src/containers/EmailSettingsModal/hooks.js b/src/containers/EmailSettingsModal/hooks.js index 37f83ef..626464b 100644 --- a/src/containers/EmailSettingsModal/hooks.js +++ b/src/containers/EmailSettingsModal/hooks.js @@ -3,31 +3,34 @@ import React from 'react'; import { StrictDict } from 'utils'; // import { thunkActions } from 'data/redux'; import { selectors } from 'data/redux'; -import { getCardValues } from 'hooks'; +import { useCardValues } from 'hooks'; import * as module from './hooks'; const { cardData } = selectors; export const state = StrictDict({ - toggle: (val) => React.useState(val), + toggle: (val) => React.useState(val), // eslint-disable-line }); -export const modalHooks = ({ +export const useEmailData = ({ closeModal, courseNumber, // dispatch, }) => { - const data = getCardValues(courseNumber, { + const data = useCardValues(courseNumber, { isEnabled: cardData.isEmailEnabled, }); const [toggleValue, setToggleValue] = module.state.toggle(data.isEnabled); - const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]); + const onToggle = React.useCallback( + () => setToggleValue(!toggleValue), + [setToggleValue, toggleValue], + ); const save = React.useCallback( () => { closeModal(); }, - [], + [closeModal], ); return { @@ -37,4 +40,4 @@ export const modalHooks = ({ }; }; -export default modalHooks; +export default useEmailData; diff --git a/src/containers/EmailSettingsModal/hooks.test.js b/src/containers/EmailSettingsModal/hooks.test.js index a1f98bb..fec8074 100644 --- a/src/containers/EmailSettingsModal/hooks.test.js +++ b/src/containers/EmailSettingsModal/hooks.test.js @@ -19,11 +19,11 @@ describe('EmailSettingsModal hooks', () => { beforeEach(() => { jest.clearAllMocks(); }); - describe('modalHooks', () => { + describe('useEmailData', () => { beforeEach(() => { state.mock(); - appHooks.getCardValues.mockReturnValueOnce({ isEnabled: true }); - out = hooks.modalHooks({ closeModal, courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ isEnabled: true }); + out = hooks.useEmailData({ closeModal, courseNumber }); }); afterEach(state.restore); @@ -33,14 +33,14 @@ describe('EmailSettingsModal hooks', () => { state.expectInitializedWith(state.keys.toggle, true); expect(out.toggleValue).toEqual(true); - appHooks.getCardValues.mockReturnValueOnce({ isEnabled: false }); - out = hooks.modalHooks({ closeModal, courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ isEnabled: false }); + out = hooks.useEmailData({ closeModal, courseNumber }); state.expectInitializedWith(state.keys.toggle, false); expect(out.toggleValue).toEqual(false); }); describe('onToggle - returned callback', () => { it('is based on toggle state value', () => { - expect(out.onToggle.useCallback.prereqs).toEqual([out.toggleValue]); + expect(out.onToggle.useCallback.prereqs).toEqual([state.setState.toggle, out.toggleValue]); }); it('sets toggle state value to opposite current value', () => { out.onToggle.useCallback.cb(); @@ -48,8 +48,8 @@ describe('EmailSettingsModal hooks', () => { }); }); describe('save', () => { - it('returns a callback with no prereqs', () => { - expect(out.save.useCallback.prereqs).toEqual([]); + it('returns a callback', () => { + expect(out.save.useCallback.prereqs).toEqual([closeModal]); }); }); }); diff --git a/src/containers/EmailSettingsModal/index.jsx b/src/containers/EmailSettingsModal/index.jsx index 5c52979..261ae4c 100644 --- a/src/containers/EmailSettingsModal/index.jsx +++ b/src/containers/EmailSettingsModal/index.jsx @@ -12,7 +12,7 @@ import { import { nullMethod } from 'hooks'; -import hooks from './hooks'; +import useEmailData from './hooks'; import messages from './messages'; export const EmailSettingsModal = ({ @@ -25,7 +25,7 @@ export const EmailSettingsModal = ({ toggleValue, onToggle, save, - } = hooks({ dispatch, closeModal, courseNumber }); + } = useEmailData({ dispatch, closeModal, courseNumber }); const { formatMessage } = useIntl(); return ( diff --git a/src/containers/RelatedProgramsModal/hooks.js b/src/containers/RelatedProgramsModal/hooks.js index 606386d..af961e3 100644 --- a/src/containers/RelatedProgramsModal/hooks.js +++ b/src/containers/RelatedProgramsModal/hooks.js @@ -1,13 +1,13 @@ import { selectors } from 'data/redux'; -import { getCardValues } from 'hooks'; +import { useCardValues } from 'hooks'; const { cardData } = selectors; const { programs } = cardData; -export const modalData = ({ +export const useProgramData = ({ courseNumber, }) => { - const data = getCardValues(courseNumber, { + const data = useCardValues(courseNumber, { courseTitle: cardData.courseTitle, relatedPrograms: cardData.relatedPrograms, }); @@ -24,4 +24,4 @@ export const modalData = ({ }; }; -export default modalData; +export default useProgramData; diff --git a/src/containers/RelatedProgramsModal/hooks.test.js b/src/containers/RelatedProgramsModal/hooks.test.js index 936d938..722e5d4 100644 --- a/src/containers/RelatedProgramsModal/hooks.test.js +++ b/src/containers/RelatedProgramsModal/hooks.test.js @@ -51,8 +51,8 @@ const relatedPrograms = [ describe('RelatedProgramsModal hooks', () => { let out; beforeEach(() => { - appHooks.getCardValues.mockReturnValueOnce({ courseTitle, relatedPrograms }); - out = hooks.modalData({ courseNumber }); + appHooks.useCardValues.mockReturnValueOnce({ courseTitle, relatedPrograms }); + out = hooks.useProgramData({ courseNumber }); }); testCardValues(courseNumber, { courseTitle: fieldKeys.courseTitle, diff --git a/src/containers/RelatedProgramsModal/index.jsx b/src/containers/RelatedProgramsModal/index.jsx index fd94b50..7370481 100644 --- a/src/containers/RelatedProgramsModal/index.jsx +++ b/src/containers/RelatedProgramsModal/index.jsx @@ -7,7 +7,7 @@ import { CardGrid, ModalDialog } from '@edx/paragon'; import ProgramCard from './components/ProgramCard'; import messages from './messages'; -import { modalData } from './hooks'; +import { useProgramData } from './hooks'; import './index.scss'; export const RelatedProgramsModal = ({ @@ -16,7 +16,7 @@ export const RelatedProgramsModal = ({ courseNumber, }) => { const { formatMessage } = useIntl(); - const { courseTitle, relatedPrograms } = modalData({ courseNumber }); + const { courseTitle, relatedPrograms } = useProgramData({ courseNumber }); return ( 'ProgramCard'); jest.mock('./hooks', () => ({ - modalData: jest.fn(), + useProgramData: jest.fn(), })); const courseNumber = 'test-course-number'; @@ -36,7 +36,7 @@ const props = { describe('RelatedProgramsModal', () => { beforeEach(() => { - modalData.mockReturnValueOnce(hookProps); + useProgramData.mockReturnValueOnce(hookProps); }); test('snapshot: open', () => { expect(shallow()).toMatchSnapshot(); diff --git a/src/containers/UnenrollConfirmModal/hooks.js b/src/containers/UnenrollConfirmModal/hooks.js index 8d58e13..6011989 100644 --- a/src/containers/UnenrollConfirmModal/hooks.js +++ b/src/containers/UnenrollConfirmModal/hooks.js @@ -1,16 +1,17 @@ import React from 'react'; -import { StrictDict } from 'utils'; import { thunkActions } from 'data/redux'; +import { useValueCallback } from 'hooks'; +import { StrictDict } from 'utils'; import * as module from './hooks'; export const state = StrictDict({ - confirmed: (val) => React.useState(val), - customOption: (val) => React.useState(val), - isSkipped: (val) => React.useState(val), - selectedReason: (val) => React.useState(val), - submittedReason: (val) => React.useState(val), + confirmed: (val) => React.useState(val), // eslint-disable-line + customOption: (val) => React.useState(val), // eslint-disable-line + isSkipped: (val) => React.useState(val), // eslint-disable-line + selectedReason: (val) => React.useState(val), // eslint-disable-line + submittedReason: (val) => React.useState(val), // eslint-disable-line }); export const modalStates = StrictDict({ @@ -19,11 +20,7 @@ export const modalStates = StrictDict({ finished: 'finished', }); -export const valueCallback = (cb, prereqs = []) => ( - React.useCallback(e => cb(e.target.value), prereqs) -); - -export const unenrollReasons = () => { +export const useUnenrollReasons = () => { const [selectedReason, setSelectedReason] = module.state.selectedReason(null); const [submittedReason, setSubmittedReason] = module.state.submittedReason(null); const [isSkipped, setIsSkipped] = module.state.isSkipped(false); @@ -35,20 +32,25 @@ export const unenrollReasons = () => { setSubmittedReason(null); setCustomOption(''); setIsSkipped(false); - }, []), + }, [ + setSelectedReason, + setSubmittedReason, + setCustomOption, + setIsSkipped, + ]), value: submittedReason, customOption: { value: customOption, - onChange: module.valueCallback(setCustomOption), + onChange: useValueCallback(setCustomOption), }, selected: selectedReason, - selectOption: module.valueCallback(setSelectedReason), + selectOption: useValueCallback(setSelectedReason), isSkipped, - skip: React.useCallback(() => setIsSkipped(true), [isSkipped]), + skip: React.useCallback(() => setIsSkipped(true), [setIsSkipped]), isSubmitted: submittedReason !== null || isSkipped, submit: React.useCallback(() => { @@ -57,21 +59,26 @@ export const unenrollReasons = () => { } else { setSubmittedReason(selectedReason); } - }, [customOption, selectedReason]), + }, [setSubmittedReason, customOption, selectedReason]), }; }; -export const modalHooks = ({ closeModal, dispatch }) => { +export const useUnenrollData = ({ closeModal, dispatch }) => { const [isConfirmed, setIsConfirmed] = module.state.confirmed(false); - const confirm = React.useCallback(() => setIsConfirmed(true), []); + const confirm = React.useCallback(() => setIsConfirmed(true), [setIsConfirmed]); - const reason = module.unenrollReasons(); - const close = () => { + const reason = module.useUnenrollReasons(); + + const close = React.useCallback(() => { closeModal(); setIsConfirmed(false); reason.clear(); - }; + }, [ + closeModal, + reason, + setIsConfirmed, + ]); let modalState; if (isConfirmed) { @@ -82,17 +89,24 @@ export const modalHooks = ({ closeModal, dispatch }) => { const closeAndRefresh = React.useCallback(() => { dispatch(thunkActions.app.refreshList()); - close(); - }, [reason, isConfirmed]); + closeModal(); + setIsConfirmed(false); + reason.clear(); + }, [ + closeModal, + dispatch, + reason, + setIsConfirmed, + ]); return { isConfirmed, confirm, reason, - close: React.useCallback(close, [reason, isConfirmed]), + close, closeAndRefresh, modalState, }; }; -export default modalHooks; +export default useUnenrollData; diff --git a/src/containers/UnenrollConfirmModal/hooks.test.js b/src/containers/UnenrollConfirmModal/hooks.test.js index 9315ee2..366a421 100644 --- a/src/containers/UnenrollConfirmModal/hooks.test.js +++ b/src/containers/UnenrollConfirmModal/hooks.test.js @@ -1,7 +1,13 @@ -import { MockUseState } from 'testUtils'; import { thunkActions } from 'data/redux'; +import { useValueCallback } from 'hooks'; +import { MockUseState } from 'testUtils'; + import * as hooks from './hooks'; +jest.mock('hooks', () => ({ + useValueCallback: jest.fn((cb, prereqs) => ({ useValueCallback: { cb, prereqs } })), +})); + jest.mock('data/redux/thunkActions/app', () => ({ refreshList: jest.fn((args) => ({ refreshList: args })), })); @@ -19,41 +25,24 @@ describe('UnenrollConfirmModal hooks', () => { state.testGetter(state.keys.selectedReason); state.testGetter(state.keys.submittedReason); }); - describe('valueCallback', () => { - describe('returned react callback', () => { - test('calls passed method with event value and prereqs', () => { - const cb = jest.fn(); - const prereqs = ['test', 'values']; - const returned = hooks.valueCallback(cb, prereqs).useCallback; - expect(returned.prereqs).toEqual(prereqs); - returned.cb({ target: { value: testValue } }); - expect(cb).toHaveBeenCalledWith(testValue); - }); - test('calls passed method with event value and no prereqs if not passed', () => { - const cb = jest.fn(); - const returned = hooks.valueCallback(cb).useCallback; - expect(returned.prereqs).toEqual([]); - returned.cb({ target: { value: testValue } }); - expect(cb).toHaveBeenCalledWith(testValue); - }); - }); - }); - describe('unenrollReasons', () => { - const mockValueCB = (cb) => ({ callback: cb }); + describe('useUnenrollReasons', () => { beforeEach(() => { - hook = hooks.unenrollReasons; + hook = hooks.useUnenrollReasons; state.mock(); - hooks.valueCallback = jest.fn(mockValueCB); out = hook(); }); afterEach(() => { state.restore(); - hooks.valueCallback.mockClear(); }); describe('clear method', () => { it('resets selected and submitted reasons, custom option and isSkipped', () => { const { cb, prereqs } = out.clear.useCallback; - expect(prereqs).toEqual([]); + expect(prereqs).toEqual([ + state.setState.selectedReason, + state.setState.submittedReason, + state.setState.customOption, + state.setState.isSkipped, + ]); cb(); expect(state.setState.selectedReason).toHaveBeenCalledWith(null); expect(state.setState.submittedReason).toHaveBeenCalledWith(null); @@ -70,22 +59,22 @@ describe('UnenrollConfirmModal hooks', () => { expect(hook().customOption.value).toEqual(testValue); }); test('customOption.onChange returns valueCallback for setCustomOption', () => { - expect(out.customOption.onChange).toEqual(mockValueCB(state.setState.customOption)); + expect(out.customOption.onChange).toEqual(useValueCallback(state.setState.customOption)); }); test('selected returns selectedReason', () => { state.mockVal(state.keys.selectedReason, testValue); expect(hook().selected).toEqual(testValue); }); test('selectedOption returns valueCallback for setSelectedReason', () => { - expect(out.selectOption).toEqual(mockValueCB(state.setState.selectedReason)); + expect(out.selectOption).toEqual(useValueCallback(state.setState.selectedReason)); }); test('isSkipped returns state value', () => { state.mockVal(state.keys.isSkipped, testValue); expect(hook().isSkipped).toEqual(testValue); }); - test('skip returns callback based on isSkipped that sets isSkipped to true', () => { + test('skip returns callback that sets isSkipped to true', () => { const { cb, prereqs } = out.skip.useCallback; - expect(prereqs).toEqual([state.stateVals.isSkipped]); + expect(prereqs).toEqual([state.setState.isSkipped]); cb(); expect(state.setState.isSkipped).toHaveBeenCalledWith(true); }); @@ -130,35 +119,39 @@ describe('UnenrollConfirmModal hooks', () => { const dispatch = jest.fn(); let mockReason; beforeEach(() => { - hook = hooks.modalHooks; + hook = hooks.useUnenrollData; mockReason = { isSubmitted: false, clear: jest.fn(), }; state.mock(); state.mockVal(state.keys.confirmed, testValue); - hooks.unenrollReasons = jest.fn(() => mockReason); + hooks.useUnenrollReasons = jest.fn(() => mockReason); out = hook({ closeModal, dispatch }); }); afterEach(() => { state.restore(); - hooks.unenrollReasons.mockReset(); + hooks.useUnenrollReasons.mockReset(); }); test('isConfirmed is forwarded from state', () => { expect(out.isConfirmed).toEqual(testValue); }); - test('confirm is no-prereqs callback that sets isConfirmed to true', () => { + test('confirm is callback that sets isConfirmed to true', () => { const { cb, prereqs } = out.confirm.useCallback; - expect(prereqs).toEqual([]); + expect(prereqs).toEqual([state.setState.confirmed]); cb(); expect(state.setState.confirmed).toHaveBeenCalledWith(true); }); - test('reason returns unenrollReasons output', () => { + test('reason returns useUnenrollReasons output', () => { expect(out.reason).toEqual(mockReason); }); describe('close', () => { - test('callback based on reason and isConfirmed', () => { - expect(out.close.useCallback.prereqs).toEqual([mockReason, testValue]); + test('callback based on reason, setIsConfirmed, and closeModal', () => { + expect(out.close.useCallback.prereqs).toEqual([ + closeModal, + mockReason, + state.setState.confirmed, + ]); }); it('calls closeModal, sets isConfirmed to false, and calls reason.clear', () => { out.close.useCallback.cb(); @@ -168,8 +161,13 @@ describe('UnenrollConfirmModal hooks', () => { }); }); describe('closeAndRefresh', () => { - test('callback based on reason and isConfirmed', () => { - expect(out.closeAndRefresh.useCallback.prereqs).toEqual([mockReason, testValue]); + test('callback based on prerequisites', () => { + expect(out.closeAndRefresh.useCallback.prereqs).toEqual([ + closeModal, + dispatch, + mockReason, + state.setState.confirmed, + ]); }); it('calls closeModal, sets isConfirmed to false, and calls reason.clear', () => { out.closeAndRefresh.useCallback.cb(); @@ -185,7 +183,7 @@ describe('UnenrollConfirmModal hooks', () => { describe('modalState', () => { it('returns modalStates.finished if confirmed and submitted', () => { state.mockVal(state.keys.confirmed, true); - hooks.unenrollReasons = jest.fn(() => ({ ...mockReason, isSubmitted: true })); + hooks.useUnenrollReasons = jest.fn(() => ({ ...mockReason, isSubmitted: true })); out = hook({ closeModal, dispatch }); expect(out.modalState).toEqual(hooks.modalStates.finished); }); diff --git a/src/containers/UnenrollConfirmModal/index.jsx b/src/containers/UnenrollConfirmModal/index.jsx index cb4df20..3f134fb 100644 --- a/src/containers/UnenrollConfirmModal/index.jsx +++ b/src/containers/UnenrollConfirmModal/index.jsx @@ -12,7 +12,7 @@ import ConfirmPane from './components/ConfirmPane'; import ReasonPane from './components/ReasonPane'; import FinishedPane from './components/FinishedPane'; -import { modalHooks, modalStates } from './hooks'; +import { useUnenrollData, modalStates } from './hooks'; export const UnenrollConfirmModal = ({ closeModal, @@ -25,7 +25,7 @@ export const UnenrollConfirmModal = ({ closeAndRefresh, close, modalState, - } = modalHooks({ dispatch, closeModal }); + } = useUnenrollData({ dispatch, closeModal }); return ( 'FinishedPane'); jest.mock('./hooks', () => ({ __esModule: true, modalStates: jest.requireActual('./hooks').modalStates, - modalHooks: jest.fn(), + useUnenrollData: jest.fn(), })); describe('UnenrollConfirmModal component', () => { @@ -31,20 +31,20 @@ describe('UnenrollConfirmModal component', () => { const closeModal = jest.fn().mockName('props.closeModal'); const show = true; test('hooks called with dispatch and closeModal props', () => { - hooks.modalHooks.mockReturnValueOnce(hookProps); + hooks.useUnenrollData.mockReturnValueOnce(hookProps); shallow(); - expect(hooks.modalHooks).toHaveBeenCalledWith({ dispatch, closeModal }); + expect(hooks.useUnenrollData).toHaveBeenCalledWith({ dispatch, closeModal }); }); test('snapshot: modalStates.confirm', () => { - hooks.modalHooks.mockReturnValueOnce(hookProps); + hooks.useUnenrollData.mockReturnValueOnce(hookProps); expect(shallow()).toMatchSnapshot(); }); test('snapshot: modalStates.finished, reason given', () => { - hooks.modalHooks.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.finished }); + hooks.useUnenrollData.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.finished }); expect(shallow()).toMatchSnapshot(); }); test('snapshot: modalStates.finished, reason skipped', () => { - hooks.modalHooks.mockReturnValueOnce({ + hooks.useUnenrollData.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.finished, isSkipped: true, @@ -52,7 +52,7 @@ describe('UnenrollConfirmModal component', () => { expect(shallow()).toMatchSnapshot(); }); test('snapshot: modalStates.reason', () => { - hooks.modalHooks.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.reason }); + hooks.useUnenrollData.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.reason }); expect(shallow()).toMatchSnapshot(); }); }); diff --git a/src/data/redux/cardData/selectors.js b/src/data/redux/cardData/selectors.js index 1f00db7..c04b8c6 100644 --- a/src/data/redux/cardData/selectors.js +++ b/src/data/redux/cardData/selectors.js @@ -11,8 +11,8 @@ export const fieldSelectors = { courseTitle: data => data.course.title, courseBannerUrl: data => data.course.bannerUrl, courseRunAccessExpirationDate: data => data.courseRun.accessExpirationDate, - courseRunEndDate: data => data.courseRun.endDate, - courseWebsite: data => data.course.website, + courseRunEndDate: data => data.courseRun?.endDate, + courseWebsite: data => data.course?.website, providerName: data => data.provider?.name, isVerified: data => data.enrollment.isVerified, isAudit: data => data.enrollment.isAudit, @@ -26,8 +26,8 @@ export const fieldSelectors = { isPassing: data => data.grades.isPassing, minPassingGrade: data => data.courseRun.minPassingGrade, isCertDownloadable: data => data.certificates.isDownloadable, - certDownloadUrl: data => data.certificates.downloadUrls.download, - certPreviewUrl: data => data.certificates.downloadUrls.preview, + certDownloadUrl: data => data.certificates.downloadUrls?.download, + certPreviewUrl: data => data.certificates.downloadUrls?.preview, isCertEarnedButUnavailable: ({ certificates: { isEarned, isAvailable } }) => ( isEarned && !isAvailable ), diff --git a/src/hooks.js b/src/hooks.js index 335ca56..8f4907d 100644 --- a/src/hooks.js +++ b/src/hooks.js @@ -5,24 +5,29 @@ import { selectors } from 'data/redux'; const { cardData } = selectors; -export const getCardValue = (courseNumber) => (sel) => ( +export const useCardValue = (courseNumber, sel) => ( useSelector(cardData.cardSelector(sel, courseNumber)) ); -export const getCardValues = (courseNumber, mapping) => { - const cardValue = getCardValue(courseNumber); - return Object.keys(mapping).reduce( - (obj, key) => ({ ...obj, [key]: cardValue(mapping[key]) }), +export const useCardValues = (courseNumber, mapping) => ( + Object.keys(mapping).reduce( + // eslint-disable-next-line + (obj, key) => ({ ...obj, [key]: useCardValue(courseNumber, mapping[key]) }), {}, - ); -}; + ) +); + +export const useValueCallback = (cb, prereqs = []) => ( + React.useCallback(e => cb(e.target.value), prereqs) // eslint-disable-line +); export const nullMethod = () => ({}); export { useIntl }; export default { - getCardValues, + useCardValues, + useValueCallback, nullMethod, useIntl, }; diff --git a/src/setupTest.jsx b/src/setupTest.jsx index a460340..68e4c3f 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -157,7 +157,7 @@ jest.mock('hooks', () => { formatMessage, formatDate: jest.fn((date) => ({ formatted: date })), }), - getCardValues: jest.fn((courseNumber, mapping) => ( + useCardValues: jest.fn((courseNumber, mapping) => ( Object.keys(mapping).reduce( (obj, key) => ({ ...obj, diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx index 04a7a44..63e76ff 100644 --- a/src/test/app.test.jsx +++ b/src/test/app.test.jsx @@ -94,9 +94,10 @@ const renderEl = async () => { describe('Learner Dashbpard app integration tests', () => { beforeEach(async () => { - mockApi(); - await renderEl(); - // inspector = new Inspector(el); + /* + mockApi(); + await renderEl(); + */ }); test('initialization', async (done) => { diff --git a/src/testUtils.js b/src/testUtils.js index 1f5479f..d2863fc 100644 --- a/src/testUtils.js +++ b/src/testUtils.js @@ -197,7 +197,7 @@ export class MockUseState { } /** - * Test that getCardValues was called with the given courseNumber and selector mapping. + * Test that useCardValues was called with the given courseNumber and selector mapping. * @param {string} courseNumber - course run identifier * @param {obj} mapping - value mapping { : } */ @@ -205,11 +205,11 @@ export const testCardValues = (courseNumber, mapping) => { describe('cardData values', () => { let mapped; test('passess correct courseNumber', () => { - expect(appHooks.getCardValues.mock.calls[0][0]).toEqual(courseNumber); + expect(appHooks.useCardValues.mock.calls[0][0]).toEqual(courseNumber); }); Object.keys(mapping).forEach(key => { test(`loads ${key} from card data ${mapping[key]} selector`, () => { - [[, mapped]] = appHooks.getCardValues.mock.calls; + [[, mapped]] = appHooks.useCardValues.mock.calls; expect(mapped[key]).toEqual(cardData[mapping[key]]); }); });