chore: clean up redux hook usage

This commit is contained in:
Ben Warzeski
2022-07-21 14:36:07 -04:00
parent 02e31e2598
commit 8edd4570b4
32 changed files with 463 additions and 780 deletions

View File

@@ -4,38 +4,30 @@ import PropTypes from 'prop-types';
import { Hyperlink } from '@edx/paragon';
import { CheckCircle } from '@edx/paragon/icons';
// import { useIntl } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import Banner from 'components/Banner';
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 ';
import messages from './messages';
export const CertificateBanner = ({ courseNumber }) => {
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,
});
const certificate = appHooks.useCardCertificateData(courseNumber);
const {
isAudit,
isVerified,
hasFinished,
} = appHooks.useCardEnrollmentData(courseNumber);
const { isPassing } = appHooks.useCardGradeData(courseNumber);
const { minPassingGrade } = appHooks.useCardCourseRunData(courseNumber);
const { formatMessage } = useIntl();
// const { formatMessage } = useIntl();
if (data.isRestricted) {
if (certificate.isRestricted) {
return (
<Banner variant="danger">
{restrictedMessage}<Hyperlink destination="info@example.com">info@example.com</Hyperlink>
{data.isVerified && (
{formatMessage(messages.certRestricted)}
<Hyperlink destination="info@example.com">info@example.com</Hyperlink>
{isVerified && (
<>
If you would like a refund on your Certificate of Achievement, please contact our billing address <Hyperlink destination="billing@example.com">billing@example.com</Hyperlink>
</>
@@ -43,11 +35,11 @@ export const CertificateBanner = ({ courseNumber }) => {
</Banner>
);
}
if (!data.isPassing) {
if (data.isAudit) {
return (<Banner> Grade required to pass the course: {data.minPassingGrade}% </Banner>);
if (!isPassing) {
if (isAudit) {
return (<Banner> Grade required to pass the course: {minPassingGrade}% </Banner>);
}
if (data.isCourseRunFinished) {
if (hasFinished) {
return (
<Banner variant="warning">
You are not eligible for a certificate. <Hyperlink destination="">View grades.</Hyperlink>
@@ -56,17 +48,17 @@ export const CertificateBanner = ({ courseNumber }) => {
}
return (
<Banner variant="warning">
Grade required for a certificate: {data.minPassingGrade}%
Grade required for a certificate: {minPassingGrade}%
</Banner>
);
}
if (data.isCertDownloadable) {
if (data.certPreviewUrl) {
if (certificate.isDownloadable) {
if (certificate.previewUrl) {
return (
<Banner variant="success" icon={CheckCircle}>
Congratulations. Your certificate is ready.
{' '}
<Hyperlink destination={data.certPreviewUrl}>View Certificate.</Hyperlink>
<Hyperlink destination={certificate.previewUrl}>View Certificate.</Hyperlink>
</Banner>
);
}
@@ -74,14 +66,14 @@ export const CertificateBanner = ({ courseNumber }) => {
<Banner variant="success" icon={CheckCircle}>
Congratulations. Your certificate is ready.
{' '}
<Hyperlink destination={data.certDownloadUrl}>Download Certificate.</Hyperlink>
<Hyperlink destination={certificate.downloadUrl}>Download Certificate.</Hyperlink>
</Banner>
);
}
if (data.isCertEarnedButUnavailable) {
if (certificate.isEarnedButUnavailable) {
return (
<Banner>
Your grade and certificate will be ready after {data.certAvailableDate}.
Your grade and certificate will be ready after {certificate.availableDate}.
</Banner>
);
}

View File

@@ -4,27 +4,24 @@ import PropTypes from 'prop-types';
import { Hyperlink } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useCardValues } from 'hooks';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import Banner from 'components/Banner';
import messages from './messages';
const { cardData } = selectors;
export const CourseBanner = ({ courseNumber }) => {
const courseData = useCardValues(courseNumber, {
isVerified: cardData.isVerified,
isCourseRunActive: cardData.isCourseRunActive,
canUpgrade: cardData.canUpgrade,
isAuditAccessExpired: cardData.isAuditAccessExpired,
courseWebsite: cardData.courseWebsite,
});
const {
isVerified,
isAuditAccessExpired,
canUpgrade,
} = appHooks.useCardEnrollmentData(courseNumber);
const courseRun = appHooks.useCardCourseRunData(courseNumber);
const course = appHooks.useCardCourseData(courseNumber);
const { formatMessage } = useIntl();
if (courseData.isVerified) { return null; }
if (isVerified) { return null; }
if (courseData.isAuditAccessExpired) {
if (courseData.canUpgrade) {
if (isAuditAccessExpired) {
if (canUpgrade) {
return (
<Banner>
{formatMessage(messages.auditAccessExpired)}
@@ -41,12 +38,12 @@ export const CourseBanner = ({ courseNumber }) => {
</Banner>
);
}
if (courseData.isCourseRunActive && !courseData.canUpgrade) {
if (courseRun.isActive && !canUpgrade) {
return (
<Banner>
{formatMessage(messages.upgradeDeadlinePassed)}
{' '}
<Hyperlink destination={courseData.courseWebsite || ''}>
<Hyperlink destination={course.website || ''}>
{formatMessage(messages.exploreCourseDetails)}
</Hyperlink>
</Banner>

View File

@@ -2,51 +2,71 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Hyperlink } from '@edx/paragon';
import * as appHooks from 'hooks';
import { testCardValues } from 'testUtils';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import { CourseBanner } from './CourseBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('data/redux', () => ({
hooks: {
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
}));
const { fieldKeys } = selectors.cardData;
const courseNumber = 'my-test-course-number';
let el;
const courseData = {
const enrollmentData = {
isVerified: false,
isCourseRunActive: false,
canUpgrade: false,
isAuditAccessExpired: false,
courseWebsite: 'test-course-website',
};
const courseRunData = {
isActive: false,
};
const courseData = {
website: 'test-course-website',
};
const render = (overrides = {}) => {
appHooks.useCardValues.mockReturnValueOnce({
const {
course = {},
courseRun = {},
enrollment = {},
} = overrides;
appHooks.useCardCourseData.mockReturnValueOnce({
...courseData,
...overrides,
...course,
});
appHooks.useCardCourseRunData.mockReturnValueOnce({
...courseRunData,
...courseRun,
});
appHooks.useCardEnrollmentData.mockReturnValueOnce({
...enrollmentData,
...enrollment,
});
el = shallow(<CourseBanner courseNumber={courseNumber} />);
};
describe('CourseBanner', () => {
testCardValues(courseNumber, {
isVerified: fieldKeys.isVerified,
isCourseRunActive: fieldKeys.isCourseRunActive,
canUpgrade: fieldKeys.canUpgrade,
isAuditAccessExpired: fieldKeys.isAuditAccessExpired,
courseWebsite: fieldKeys.courseWebsite,
}, render);
it('initializes data with course number from enrollment, course and course run data', () => {
render();
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
});
test('no display if learner is verified', () => {
render({ isVerified: true });
render({ enrollment: { isVerified: true } });
expect(el.isEmptyRender()).toEqual(true);
});
describe('audit access expired, can upgrade', () => {
beforeEach(() => {
render({ isAuditAccessExpired: true, canUpgrade: true });
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
});
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
expect(el).toMatchSnapshot();
@@ -58,7 +78,7 @@ describe('CourseBanner', () => {
});
describe('audit access expired, cannot upgrade', () => {
beforeEach(() => {
render({ isAuditAccessExpired: true });
render({ enrollment: { isAuditAccessExpired: true } });
});
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
expect(el).toMatchSnapshot();
@@ -70,7 +90,7 @@ describe('CourseBanner', () => {
});
describe('course run active and cannot upgrade', () => {
beforeEach(() => {
render({ isCourseRunActive: true });
render({ courseRun: { isActive: true } });
});
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el).toMatchSnapshot();
@@ -79,13 +99,13 @@ describe('CourseBanner', () => {
expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage);
const link = el.find(Hyperlink);
expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage);
expect(link.props().destination).toEqual(courseData.courseWebsite);
expect(link.props().destination).toEqual(courseData.website);
});
});
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
render();
expect(el.isEmptyRender()).toEqual(true);
render({ isCourseRunActive: true, canUpgrade: true });
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
expect(el.isEmptyRender()).toEqual(true);
});
});

View File

@@ -1,28 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useCardValues } from 'hooks';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import Banner from 'components/Banner';
const { cardData } = selectors;
export const EntitlementBanner = ({ courseNumber }) => {
const data = useCardValues(courseNumber, {
canChange: cardData.canChangeEntitlementSession,
isEntitlement: cardData.isEntitlement,
isExpired: cardData.isEntitlementExpired,
isFulfilled: cardData.isEntitlementFulfilled,
});
const {
canChange,
isEntitlement,
isExpired,
isFulfilled,
} = appHooks.useCardEntitlementsData(courseNumber);
if (!data.isEntitlement) {
if (!isEntitlement) {
return null;
}
if (data.isExpired || data.isFulfilled) {
if (isExpired || isFulfilled) {
return null;
}
return data.canChange
return canChange
? (<Banner>You must select a session to access the course.</Banner>)
: (<Banner>The deadline to select a session has passed</Banner>);
};

View File

@@ -26,6 +26,11 @@ export const messages = StrictDict({
description: 'Action prompt taking learners to course details page',
defaultMessage: 'Explore course details.',
},
certRestricted: {
id: 'learner-dash.courseCard.banners.certificateRestricted',
description: 'Restricted certificate warning message',
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting ',
},
});
export default messages;

View File

@@ -1,38 +1,35 @@
import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux';
import { useCardValues } from 'hooks';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
const { cardData } = selectors;
export const useCardActionData = ({ courseNumber }) => {
const { formatMessage } = useIntl();
const data = useCardValues(courseNumber, {
canUpgrade: cardData.canUpgrade,
isAudit: cardData.isAudit,
isAuditAccessExpired: cardData.isAuditAccessExpired,
isVerified: cardData.isVerified,
isPending: cardData.isCourseRunPending,
isFinished: cardData.isCourseRunFinished,
});
const {
canUpgrade,
isAudit,
isAuditAccessExpired,
isVerified,
} = appHooks.useCardEnrollmentData(courseNumber);
const { isPending, isArchived } = appHooks.useCardCourseRunData(courseNumber);
let primary;
let secondary = null;
if (!data.isVerified) {
if (!isVerified) {
secondary = {
iconBefore: Locked,
variant: 'outline-primary',
disabled: !data.canUpgrade,
disabled: !canUpgrade,
children: formatMessage(messages.upgrade),
};
}
if (data.isPending) {
if (isPending) {
primary = { children: formatMessage(messages.beginCourse) };
} else if (!data.isFinished) {
} else if (!isArchived) {
primary = {
children: formatMessage(messages.resume),
disabled: data.isAudit && data.isAuditAccessExpired,
disabled: isAudit && isAuditAccessExpired,
};
} else {
primary = { children: formatMessage(messages.viewCourse) };

View File

@@ -1,55 +1,48 @@
import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import * as appHooks from 'hooks';
import { testCardValues } from 'testUtils';
import * as hooks from './hooks';
import messages from './messages';
const courseNumber = 'my-test-course-number';
const { fieldKeys } = selectors.cardData;
jest.mock('data/redux', () => ({
hooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
}));
const props = {
const courseNumber = 'my-test-course-number';
const enrollmentData = {
canUpgrade: false,
isAudit: true,
isAuditAccessExpired: false,
isVerified: false,
};
const courseRunData = {
isPending: false,
isFinished: false,
isArchived: false,
};
describe('CourseCardActions hooks', () => {
let out;
const { formatMessage } = useIntl();
describe('data connection', () => {
beforeEach(() => {
out = hooks.useCardActionData({ courseNumber });
});
testCardValues(courseNumber, {
canUpgrade: fieldKeys.canUpgrade,
isAudit: fieldKeys.isAudit,
isAuditAccessExpired: fieldKeys.isAuditAccessExpired,
isVerified: fieldKeys.isVerified,
isPending: fieldKeys.isCourseRunPending,
isFinished: fieldKeys.isCourseRunFinished,
});
});
const runHook = (overrides = {}) => {
const { enrollment = {}, courseRun = {} } = overrides;
appHooks.useCardCourseRunData.mockReturnValueOnce({ ...courseRunData, ...courseRun });
appHooks.useCardEnrollmentData.mockReturnValueOnce({ ...enrollmentData, ...enrollment });
out = hooks.useCardActionData({ courseNumber });
};
describe('secondary action', () => {
it('returns null if verified', () => {
appHooks.useCardValues.mockReturnValueOnce({
...props,
isAudit: false,
isVerified: true,
});
out = hooks.useCardActionData({ courseNumber });
runHook({ enrollment: { isAudit: false, isVerified: true } });
expect(out.secondary).toEqual(null);
});
it('returns disabled upgrade button if audit, but cannot upgrade', () => {
appHooks.useCardValues.mockReturnValueOnce(props);
out = hooks.useCardActionData({ courseNumber });
runHook();
expect(out.secondary).toEqual({
iconBefore: Locked,
variant: 'outline-primary',
@@ -58,8 +51,7 @@ describe('CourseCardActions hooks', () => {
});
});
it('returns enabled upgrade button if audit and can upgrade', () => {
appHooks.useCardValues.mockReturnValueOnce({ ...props, canUpgrade: true });
out = hooks.useCardActionData({ courseNumber });
runHook({ enrollment: { canUpgrade: true } });
expect(out.secondary).toEqual({
iconBefore: Locked,
variant: 'outline-primary',
@@ -70,37 +62,30 @@ describe('CourseCardActions hooks', () => {
});
describe('primary action', () => {
it('returns Begin Course button if pending', () => {
appHooks.useCardValues.mockReturnValueOnce({ ...props, isPending: true });
out = hooks.useCardActionData({ courseNumber });
expect(out.primary).toEqual({
children: formatMessage(messages.beginCourse),
});
runHook({ courseRun: { isPending: true } });
expect(out.primary).toEqual({ children: formatMessage(messages.beginCourse) });
});
it('returns enabled Resume button if active, and not audit with expired access', () => {
appHooks.useCardValues.mockReturnValueOnce({ ...props, isAuditAccessExpired: true });
out = hooks.useCardActionData({ courseNumber });
runHook({ enrollment: { isAuditAccessExpired: true } });
expect(out.primary).toEqual({
children: formatMessage(messages.resume),
disabled: true,
});
});
it('returns disabled Resume button if active and audit without expired access', () => {
appHooks.useCardValues.mockReturnValueOnce({ ...props });
out = hooks.useCardActionData({ courseNumber });
runHook();
expect(out.primary).toEqual({
children: formatMessage(messages.resume),
disabled: false,
});
appHooks.useCardValues.mockReturnValueOnce({ ...props, isAudit: false, isVerified: true });
out = hooks.useCardActionData({ courseNumber });
runHook({ enrollment: { isAudit: false, isVerified: true } });
expect(out.primary).toEqual({
children: formatMessage(messages.resume),
disabled: false,
});
});
it('returns viewCourse button if finished', () => {
appHooks.useCardValues.mockReturnValueOnce({ ...props, isFinished: true });
out = hooks.useCardActionData({ courseNumber });
it('returns viewCourse button if archived', () => {
runHook({ courseRun: { isArchived: true } });
expect(out.primary).toEqual({
children: formatMessage(messages.viewCourse),
});

View File

@@ -2,14 +2,11 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { selectors } from 'data/redux';
import { useCardValues } from 'hooks';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
import * as module from './hooks';
const { cardData } = selectors;
export const state = StrictDict({
isOpen: (val) => React.useState(val), // eslint-disable-line
});
@@ -17,10 +14,7 @@ export const state = StrictDict({
export const useRelatedProgramsBadgeData = ({ courseNumber }) => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const { formatMessage } = useIntl();
const { numPrograms } = useCardValues(courseNumber, {
numPrograms: cardData.numRelatedPrograms,
});
const numPrograms = appHooks.useCardRelatedProgramsData(courseNumber).length;
let programsMessage = '';
if (numPrograms) {
programsMessage = formatMessage(

View File

@@ -1,13 +1,16 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { MockUseState } from 'testUtils';
import * as appHooks from 'hooks';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import * as hooks from './hooks';
import messages from './messages';
const { cardData } = selectors;
jest.mock('data/redux', () => ({
hooks: {
useCardRelatedProgramsData: jest.fn(),
},
}));
const courseNumber = 'my-test-course-number';
@@ -16,7 +19,7 @@ const numPrograms = 27;
const { formatMessage } = useIntl();
describe('EmailSettingsModal hooks', () => {
describe('RelatedProgramsBadge hooks', () => {
let out;
describe('state values', () => {
state.testGetter(state.keys.isOpen);
@@ -27,8 +30,8 @@ describe('EmailSettingsModal hooks', () => {
describe('useRelatedProgramsBadgeData', () => {
beforeEach(() => {
state.mock();
appHooks.useCardValues.mockReturnValueOnce({
numPrograms,
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({
length: numPrograms,
});
out = hooks.useRelatedProgramsBadgeData({ courseNumber });
});
@@ -52,21 +55,16 @@ describe('EmailSettingsModal hooks', () => {
expect(out.isOpen).toEqual(state.stateVals.isOpen);
});
test('returns numPrograms from useCardValues call on numRelatedPrograms', () => {
expect(appHooks.useCardValues).toHaveBeenCalledWith(
courseNumber,
{ numPrograms: cardData.numRelatedPrograms },
);
test('forwards numPrograms from relatedPrograms.length for the courseNumber', () => {
expect(out.numPrograms).toEqual(numPrograms);
});
test('returns empty programsMessage if no programs', () => {
appHooks.useCardValues.mockReturnValueOnce({ numPrograms: 0 });
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
out = hooks.useRelatedProgramsBadgeData({ courseNumber });
expect(out.programsMessage).toEqual('');
});
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
appHooks.useCardValues.mockReturnValueOnce({ numPrograms: 1 });
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
out = hooks.useRelatedProgramsBadgeData({ courseNumber });
expect(out.programsMessage).toEqual(formatMessage(
messages.badgeLabelSingular,

View File

@@ -1,47 +1,40 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux';
import { useCardValues } from 'hooks';
import { hooks as appHooks } from 'data/redux';
import * as module from './hooks';
import messages from './messages';
const { cardData } = selectors;
export const useAccessMessage = ({ courseNumber }) => {
const { formatMessage, formatDate } = useIntl();
const {
accessExpirationDate,
isAudit,
isAuditAccessExpired,
} = appHooks.useCardEnrollmentData(courseNumber);
const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber);
const data = useCardValues(courseNumber, {
accessExpirationDate: cardData.courseRunAccessExpirationDate,
isAudit: cardData.isAudit,
isFinished: cardData.isCourseRunFinished,
isAuditAccessExpired: cardData.isAuditAccessExpired,
endDate: cardData.courseRunEndDate,
});
if (data.isAudit) {
if (isAudit) {
return formatMessage(
data.isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
{ accessExpirationDate: formatDate(data.accessExpirationDate) },
isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
{ accessExpirationDate: formatDate(accessExpirationDate) },
);
}
return formatMessage(
data.isFinished ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(data.endDate) },
isArchived ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(endDate) },
);
};
export const useCardData = ({ courseNumber }) => {
const { formatMessage } = useIntl();
const data = useCardValues(courseNumber, {
title: cardData.courseTitle,
bannerUrl: cardData.courseBannerUrl,
providerName: cardData.providerName,
});
const { title, bannerUrl } = appHooks.useCardCourseData(courseNumber);
const providerName = appHooks.useCardProviderData(courseNumber).name;
return {
title: data.title,
bannerUrl: data.bannerUrl,
providerName: data.providerName || formatMessage(messages.unknownProviderName),
title,
bannerUrl,
providerName: providerName || formatMessage(messages.unknownProviderName),
accessMessage: module.useAccessMessage({ courseNumber }),
formatMessage,
};

View File

@@ -1,14 +1,19 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { keyStore } from 'utils';
import { selectors } from 'data/redux';
import * as appHooks from 'hooks';
import { testCardValues } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import * as hooks from './hooks';
import messages from './messages';
const { fieldKeys } = selectors.cardData;
jest.mock('data/redux', () => ({
hooks: {
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardProviderData: jest.fn(),
},
}));
const courseNumber = 'my-test-course-number';
const useAccessMessage = 'test-access-message';
@@ -18,120 +23,114 @@ const hookKeys = keyStore(hooks);
describe('CourseCard hooks', () => {
let out;
const { formatMessage, formatDate } = useIntl();
beforeEach(() => {
jest.clearAllMocks();
});
describe('useCardData', () => {
const courseData = {
title: 'fake-title',
bannerUrl: 'my-banner-url',
};
const providerData = {
name: 'my-provider-name',
};
const runHook = ({ course = {}, provider = {} }) => {
jest.spyOn(hooks, hookKeys.useAccessMessage)
.mockImplementationOnce(mockAccessMessage);
appHooks.useCardCourseData.mockReturnValueOnce({
...courseData,
...course,
});
appHooks.useCardProviderData.mockReturnValueOnce({
...providerData,
...provider,
});
out = hooks.useCardData({ courseNumber });
};
beforeEach(() => {
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.useCardValues.mockReturnValueOnce({
title: 'title',
bannerUrl: 'bannerUrl',
providerName: null,
});
jest.spyOn(hooks, hookKeys.useAccessMessage).mockImplementationOnce(mockAccessMessage);
out = hooks.useCardData({ courseNumber });
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
});
describe('useAccessMessage', () => {
it('returns the output of useAccessMessage hook, passed courseNumber', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber }));
});
runHook({});
});
it('forwards formatMessage from useIntl', () => {
expect(out.formatMessage).toEqual(formatMessage);
});
it('passes course title and banner URL form course data', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(out.title).toEqual(courseData.title);
expect(out.bannerUrl).toEqual(courseData.bannerUrl);
});
it('forwards useAccessMessage output, called with courseNumber', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber }));
});
it('forwards provider name if it exists, else formatted unknown provider name', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(out.providerName).toEqual(providerData.name);
runHook({ provider: { name: '' } });
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
});
});
describe('useAccessMessage', () => {
const accessExpirationDate = 'test-expiration-date';
const endDate = 'test-end-date';
beforeEach(() => {
appHooks.useCardValues.mockClear();
});
describe('loaded data', () => {
beforeEach(() => {
out = hooks.useAccessMessage({ courseNumber });
const enrollmentData = {
accessExpirationDate: 'test-expiration-date',
isAudit: false,
isAuditAccessExpired: false,
};
const courseRunData = {
isFinished: false,
endDate: 'test-end-date',
};
const runHook = ({ enrollment = {}, courseRun = {} }) => {
appHooks.useCardCourseRunData.mockReturnValueOnce({
...courseRunData,
...courseRun,
});
testCardValues(courseNumber, {
accessExpirationDate: fieldKeys.courseRunAccessExpirationDate,
isAudit: fieldKeys.isAudit,
isFinished: fieldKeys.isCourseRunFinished,
isAuditAccessExpired: fieldKeys.isAuditAccessExpired,
endDate: fieldKeys.courseRunEndDate,
appHooks.useCardEnrollmentData.mockReturnValueOnce({
...enrollmentData,
...enrollment,
});
out = hooks.useAccessMessage({ courseNumber });
};
it('loads data from enrollment and course run data based on course number', () => {
runHook({});
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
});
describe('if audit, and expired', () => {
it('returns accessExpired message with accessExpirationDate from cardData', () => {
appHooks.useCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: true,
isFinished: false,
isAuditAccessExpired: true,
});
expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage(
runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } });
expect(out).toEqual(formatMessage(
messages.accessExpired,
{ accessExpirationDate: formatDate(accessExpirationDate) },
{ accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) },
));
});
});
describe('if audit and not expired', () => {
it('returns accessExpires message with accessExpirationDate from cardData', () => {
appHooks.useCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: true,
isFinished: false,
isAuditAccessExpired: false,
});
expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage(
runHook({ enrollment: { isAudit: true } });
expect(out).toEqual(formatMessage(
messages.accessExpires,
{ accessExpirationDate: formatDate(accessExpirationDate) },
{ accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) },
));
});
});
describe('if verified and not ended', () => {
it('returns course ends message with course end date', () => {
appHooks.useCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: false,
isFinished: false,
isAuditAccessExpired: false,
});
expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage(
runHook({});
expect(out).toEqual(formatMessage(
messages.courseEnds,
{ endDate: formatDate(endDate) },
{ endDate: formatDate(courseRunData.endDate) },
));
});
});
describe('if verified and ended', () => {
it('returns course ended message with course end date', () => {
appHooks.useCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: false,
isFinished: true,
isAuditAccessExpired: false,
});
expect(hooks.useAccessMessage({ courseNumber })).toEqual(formatMessage(
runHook({ courseRun: { isFinished: true } });
expect(out).toEqual(formatMessage(
messages.courseEnded,
{ endDate: formatDate(endDate) },
{ endDate: formatDate(courseRunData.endDate) },
));
});
});

View File

@@ -2,13 +2,10 @@ import React from 'react';
import { StrictDict } from 'utils';
// import { thunkActions } from 'data/redux';
import { selectors } from 'data/redux';
import { useCardValues } from 'hooks';
import { hooks as appHooks } from 'data/redux';
import * as module from './hooks';
const { cardData } = selectors;
export const state = StrictDict({
toggle: (val) => React.useState(val), // eslint-disable-line
});
@@ -18,10 +15,8 @@ export const useEmailData = ({
courseNumber,
// dispatch,
}) => {
const data = useCardValues(courseNumber, {
isEnabled: cardData.isEmailEnabled,
});
const [toggleValue, setToggleValue] = module.state.toggle(data.isEnabled);
const { isEmailEnabled } = appHooks.useCardEnrollmentData(courseNumber);
const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled);
const onToggle = React.useCallback(
() => setToggleValue(!toggleValue),
[setToggleValue, toggleValue],

View File

@@ -1,10 +1,13 @@
import { MockUseState, testCardValues } from 'testUtils';
import * as appHooks from 'hooks';
import { selectors } from 'data/redux';
import { MockUseState } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import * as hooks from './hooks';
const { fieldKeys } = selectors.cardData;
jest.mock('data/redux', () => ({
hooks: {
useCardEnrollmentData: jest.fn(),
},
}));
const courseNumber = 'my-test-course-number';
const closeModal = jest.fn();
@@ -22,18 +25,20 @@ describe('EmailSettingsModal hooks', () => {
describe('useEmailData', () => {
beforeEach(() => {
state.mock();
appHooks.useCardValues.mockReturnValueOnce({ isEnabled: true });
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEmailEnabled: true });
out = hooks.useEmailData({ closeModal, courseNumber });
});
afterEach(state.restore);
testCardValues(courseNumber, { isEnabled: fieldKeys.isEmailEnabled });
test('loads enrollment data based on course number', () => {
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
});
test('initializes toggle value to cardData.isEmailEnabled', () => {
state.expectInitializedWith(state.keys.toggle, true);
expect(out.toggleValue).toEqual(true);
appHooks.useCardValues.mockReturnValueOnce({ isEnabled: false });
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEmailEnabled: false });
out = hooks.useEmailData({ closeModal, courseNumber });
state.expectInitializedWith(state.keys.toggle, false);
expect(out.toggleValue).toEqual(false);

View File

@@ -54,15 +54,14 @@ export const ProgramCard = ({ data }) => {
};
ProgramCard.propTypes = {
data: PropTypes.shape({
estimatedNumberOfWeeks: PropTypes.number,
numberOfCourses: PropTypes.number,
bannerUrl: PropTypes.string,
estimatedNumberOfWeeks: PropTypes.number,
logoUrl: PropTypes.string,
title: PropTypes.string,
provider: PropTypes.string,
numberOfCourses: PropTypes.number,
programType: PropTypes.string,
programUrl: PropTypes.string,
programTypeUrl: PropTypes.string,
provider: PropTypes.string,
title: PropTypes.string,
}).isRequired,
};

View File

@@ -1,27 +1,10 @@
import { selectors } from 'data/redux';
import { useCardValues } from 'hooks';
const { cardData } = selectors;
const { programs } = cardData;
import { hooks as appHooks } from 'data/redux';
export const useProgramData = ({
courseNumber,
}) => {
const data = useCardValues(courseNumber, {
courseTitle: cardData.courseTitle,
relatedPrograms: cardData.relatedPrograms,
});
return {
courseTitle: data.courseTitle,
relatedPrograms: data.relatedPrograms.map(program => ({
estimatedNumberOfWeeks: programs.estimatedNumberOfWeeks(program),
numberOfCourses: programs.numberOfCourses(program),
programType: programs.programType(program),
programTypeUrl: programs.programTypeUrl(program),
provider: programs.provider(program),
title: programs.title(program),
})),
};
};
}) => ({
courseTitle: appHooks.useCardCourseData(courseNumber).title,
relatedPrograms: appHooks.useCardRelatedProgramsData(courseNumber).list,
});
export default useProgramData;

View File

@@ -1,67 +1,26 @@
import { testCardValues } from 'testUtils';
import * as appHooks from 'hooks';
import { selectors } from 'data/redux';
import { hooks as appHooks } from 'data/redux';
import * as hooks from './hooks';
jest.mock('data/redux/cardData/selectors', () => ({
...jest.requireActual('data/redux/cardData/selectors'),
programs: {
estimatedNumberOfWeeks: (p) => p.estimatedNumberOfWeeks,
numberOfCourses: (p) => p.numberOfCourses,
programType: (p) => p.programType,
programTypeUrl: (p) => p.programTypeUrl,
provider: (p) => p.provider,
title: (p) => p.title,
jest.mock('data/redux', () => ({
hooks: {
useCardCourseData: jest.fn(),
useCardRelatedProgramsData: jest.fn(),
},
}));
const { fieldKeys } = selectors.cardData;
const courseNumber = 'test-course-number';
const courseTitle = 'test-course-title';
const relatedPrograms = [
{
estimatedNumberOfWeeks: 1,
numberOfCourses: 2,
programType: 'test-program-type-1',
programTypeUrl: 'test-program-type-1-url',
provider: 'test-provider-1',
title: 'test-program-title-1',
},
{
estimatedNumberOfWeeks: 2,
numberOfCourses: 3,
programType: 'test-program-type-2',
programTypeUrl: 'test-program-type-2-url',
provider: 'test-provider-2',
title: 'test-program-title-2',
},
{
estimatedNumberOfWeeks: 3,
numberOfCourses: 5,
programType: 'test-program-type-3',
programTypeUrl: 'test-program-type-3-url',
provider: 'test-provider-3',
title: 'test-program-title-3',
},
];
const relatedPrograms = ['some', 'programs'];
describe('RelatedProgramsModal hooks', () => {
let out;
beforeEach(() => {
appHooks.useCardValues.mockReturnValueOnce({ courseTitle, relatedPrograms });
out = hooks.useProgramData({ courseNumber });
});
testCardValues(courseNumber, {
courseTitle: fieldKeys.courseTitle,
relatedPrograms: fieldKeys.relatedPrograms,
});
test('courseTitle loads course title', () => {
expect(out.courseTitle).toEqual(courseTitle);
});
test('relatedPrograms loads from course run related programs', () => {
expect(out.relatedPrograms).toEqual(relatedPrograms);
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);
expect(out).toEqual({ courseTitle, relatedPrograms });
});
});

View File

@@ -40,7 +40,7 @@ export const RelatedProgramsModal = ({
columnSizes={{ lg: 6, xlg: 4, xs: 12 }}
>
{relatedPrograms.map((programData) => (
<ProgramCard key={`${programData.programUrl}`} data={programData} />
<ProgramCard key={programData.programUrl} data={programData} />
))}
</CardGrid>
</ModalDialog.Body>

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import selectors from './selectors';
const { courseCard } = selectors;
export const useEmailConfirmationData = () => useSelector(selectors.emailConfirmation);
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboards);
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (courseNumber) => useSelector(
(state) => selector(selectors.courseData(state)[courseNumber]),
);
export const useCardCertificateData = useCourseCardData(courseCard.certificates);
export const useCardCourseData = useCourseCardData(courseCard.course);
export const useCardCourseRunData = useCourseCardData(courseCard.courseRun);
export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment);
export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements);
export const useCardGradesData = useCourseCardData(courseCard.grades);
export const useCardProviderData = useCourseCardData(courseCard.provider);
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);

View File

@@ -5,6 +5,10 @@ const initialState = {
enrollments: [],
courseData: {},
entitlements: [],
emailConfirmation: {},
enterpriseDashboards: {},
platformSettings: {},
suggestedCourses: {},
};
// eslint-disable-next-line no-unused-vars
@@ -24,6 +28,13 @@ const app = createSlice({
),
}),
loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }),
loadGlobalData: (state, { payload }) => ({
...state,
emailConfirmation: payload.emailConfirmation,
enterpriseDashboards: payload.enterpriseDashboards,
platformSettings: payload.platformSettings,
suggestedCourses: payload.suggestedCourses,
}),
},
});

View File

@@ -13,13 +13,76 @@ export const simpleSelectors = {
enrollments: mkSimpleSelector(app => app.enrollments),
entitlements: mkSimpleSelector(app => app.entitlements),
courseData: mkSimpleSelector(app => app.courseData),
platformSettings: mkSimpleSelector(app => app.platformSettings),
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
enterpriseDashboards: mkSimpleSelector(app => app.enterpriseDashboards),
};
export const courseCardData = (state, courseNumber) => (
module.simpleSelectors.courseData(state)[courseNumber]
);
const mkCardSelector = (sel) => (state, courseNumber) => (
sel(courseCardData(state, courseNumber))
);
export const courseCard = StrictDict({
certificates: mkCardSelector(({ certificates }) => ({
availableDate: certificates.availableDate,
downloadUrl: certificates.downloadUrls?.download,
previewUrl: certificates.downloadUrls?.preview,
isDownloadable: certificates.isDownloadable,
isEarnedButUnavailable: certificates.isEarned && !certificates.isAvailable,
isRestricted: certificates.isRestricted,
})),
course: mkCardSelector(({ course }) => ({
bannerUrl: course.bannerUrl,
title: course.title,
website: course.website,
})),
courseRun: mkCardSelector(({ courseRun }) => ({
endDate: courseRun?.endDate,
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,
})),
entitlements: mkCardSelector(({ entitlements }) => ({
canChange: entitlements.canChange,
entitlementSessions: entitlements.availableSessions,
isEntitlement: entitlements.isEntitlement,
isExpired: entitlements.isExpired,
isFulfilled: entitlements.isFulfilled,
})),
grades: mkCardSelector(({ grades }) => ({ isPassing: grades.isPassing })),
provider: mkCardSelector(({ provider }) => ({ name: provider?.name })),
relatedPrograms: mkCardSelector(({ relatedPrograms }) => ({
list: relatedPrograms.map(program => ({
bannerUrl: program.bannerUrl,
estimatedNumberOfWeeks: program.estimatedNumberOfWeeks,
logoUrl: program.logoUrl,
numberOfCourses: program.numberOfCourses,
programType: program.programType,
programUrl: program.programUrl,
provider: program.provider,
title: program.title,
})),
length: relatedPrograms.length,
})),
});
export default StrictDict({
...simpleSelectors,
courseCardData,
courseCard,
});

View File

@@ -1,2 +0,0 @@
/* eslint-disable import/prefer-default-export */
export { default as selectors } from './selectors';

View File

@@ -1,67 +0,0 @@
import { keyStore, StrictDict } from 'utils';
import app from 'data/redux/app/selectors';
// import * as module from './selectors';
const mkCardSelector = (sel) => (state, courseNumber) => (
sel(app.courseCardData(state, courseNumber))
);
export const fieldSelectors = {
canChangeEntitlementSession: data => data.entitlements.canChange,
canUpgrade: data => data.enrollment.canUpgrade,
certAvailableDate: data => data.certificates.availableDate,
certDownloadUrl: data => data.certificates.downloadUrls?.download,
certPreviewUrl: data => data.certificates.downloadUrls?.preview,
courseBannerUrl: data => data.course.bannerUrl,
courseRunAccessExpirationDate: data => data.courseRun.accessExpirationDate,
courseRunEndDate: data => data.courseRun?.endDate,
courseTitle: data => data.course.title,
courseWebsite: data => data.course.website,
entitlementSessions: data => data.entitlements.availableSessions,
isAudit: data => data.enrollment.isAudit,
isAuditAccessExpired: data => data.enrollment.isAuditAccessExpired,
isCertDownloadable: data => data.certificates.isDownloadable,
isCertEarnedButUnavailable: ({ certificates: { isEarned, isAvailable } }) => (
isEarned && !isAvailable
),
isCourseRunPending: data => data.courseRun.isPending,
isCourseRunStarted: data => data.courseRun.isStarted,
isCourseRunFinished: data => data.courseRun.isFinished,
isEmailEnabled: data => data.enrollment.isEmailEnabled,
isEntitlement: data => data.entitlements.isEntitlement,
isEntitlementExpired: data => data.entitlements.isExpired,
isEntitlementFulfilled: data => data.entitlements.isFulfilled,
isVerified: data => data.enrollment.isVerified,
isRestricted: data => data.certificates.isRestricted,
isPassing: data => data.grades.isPassing,
minPassingGrade: data => data.courseRun.minPassingGrade,
providerName: data => data.provider?.name,
relatedPrograms: data => data.relatedPrograms,
numRelatedPrograms: data => data.relatedPrograms.length,
};
fieldSelectors.isCourseRunActive = data => (
fieldSelectors.isCourseRunStarted(data) && !fieldSelectors.isCourseRunFinished(data)
);
export const programs = StrictDict({
estimatedNumberOfWeeks: data => data.estimatedNumberOfWeeks,
numberOfCourses: data => data.numberOfCourses,
programType: data => data.programType,
programTypeUrl: data => data.programTypeUrl,
provider: data => data.provider,
title: data => data.title,
});
export const fieldKeys = keyStore(fieldSelectors);
export const cardSelectors = Object.keys(fieldSelectors).reduce(
(obj, key) => ({ ...obj, [key]: mkCardSelector(fieldSelectors[key]) }),
{},
);
export default StrictDict({
...cardSelectors,
programs,
fieldKeys,
});

View File

@@ -1,255 +0,0 @@
import app from 'data/redux/app/selectors';
import * as selectors from './selectors';
jest.mock('data/redux/app/selectors', () => ({
courseCardData: jest.fn(),
}));
const {
default: exported,
programs,
fieldKeys,
} = selectors;
const courseNumber = 'test-course-number';
const testState = { test: 'state' };
const testValue = 'my-test-value';
/**
* Test a field selector, both in basic definition and exported/connected card field
* @param {string} key - field keys to test
* @param {string} basicMessage - basic usage test message
* @param {obj} basicTest - { data, expected } - passed data and expected output.
* expected defaults to testValue as defined in this file.
* @param {object[]} [conditionTests] - (optional) extra tests for special conditions
* condition: <str> explanation of the condition
* data: <obj> data to be passed to the transform
* expected: <any> expected output
* message: <str> test message
*/
const testFieldSelector = (key, basicMessage, basicTest, conditionTests = []) => {
describe(`fieldSelector: ${key}`, () => {
describe('basic usage', () => {
const { data, expected = testValue } = basicTest;
test(basicMessage, () => {
expect(selectors.fieldSelectors[key](data)).toEqual(expected);
});
it('exports a card selector for the given key, binding to course card data', () => {
app.courseCardData.mockReturnValueOnce(data);
expect(exported[key](testState, courseNumber)).toEqual(expected);
expect(app.courseCardData).toHaveBeenCalledWith(testState, courseNumber);
});
});
conditionTests.forEach((conditionTest) => {
const { data, expected } = conditionTest;
describe(conditionTest.condition, () => {
test(conditionTest.message, () => {
expect(selectors.fieldSelectors[key](data)).toEqual(expected);
});
it('exports a card selector for the given key, binding to course card data', () => {
app.courseCardData.mockReturnValueOnce(data);
expect(exported[key](testState, courseNumber)).toEqual(expected);
expect(app.courseCardData).toHaveBeenCalledWith(testState, courseNumber);
});
});
});
});
};
describe('cardData selectors', () => {
describe('fieldSelectors', () => {
testFieldSelector(fieldKeys.canChangeEntitlementSession,
'returns the entitlements canChangeEntitlementsSession value',
{ data: { entitlements: { canChange: testValue } } });
testFieldSelector(fieldKeys.canUpgrade,
'returns the enrollment canUpgrade value',
{ data: { enrollment: { canUpgrade: testValue } } });
testFieldSelector(fieldKeys.certAvailableDate,
'returns the certificates availableDate value',
{ data: { certificates: { availableDate: testValue } } });
testFieldSelector(fieldKeys.certDownloadUrl,
'returns the certificates download url value',
{ data: { certificates: { downloadUrls: { download: testValue } } } },
[{
condition: 'if no download urls are provided for certificates',
data: { certificates: {} },
expected: undefined,
message: 'returns undefined',
}]);
testFieldSelector(fieldKeys.certPreviewUrl,
'returns the certificates preview url value',
{ data: { certificates: { downloadUrls: { preview: testValue } } } },
[{
condition: 'if no downloadUrls are provided for certificates',
data: { certificates: {} },
expected: undefined,
message: 'returns undefined',
}]);
testFieldSelector(fieldKeys.courseBannerUrl,
'returns the course banner url value',
{ data: { course: { bannerUrl: testValue } } });
testFieldSelector(fieldKeys.courseRunAccessExpirationDate,
'returns the course run access expiration date value',
{ data: { courseRun: { accessExpirationDate: testValue } } });
testFieldSelector(fieldKeys.courseRunEndDate,
'returns the course banner url value',
{ data: { courseRun: { endDate: testValue } } },
[{
condition: 'if course run is not defined',
data: {},
expected: undefined,
message: 'returns undefined if there is no course run data',
}]);
testFieldSelector(fieldKeys.courseTitle,
'returns the course title value',
{ data: { course: { title: testValue } } });
testFieldSelector(fieldKeys.courseWebsite,
'returns the course website value',
{ data: { course: { website: testValue } } });
testFieldSelector(fieldKeys.entitlementSessions,
'returns available entitlement sessions value',
{ data: { entitlements: { availableSessions: testValue } } });
testFieldSelector(fieldKeys.isAudit,
'returns enrollment isAudit value',
{ data: { enrollment: { isAudit: testValue } } });
testFieldSelector(fieldKeys.isAuditAccessExpired,
'returns enrollment isAudiAccessExpired value',
{ data: { enrollment: { isAuditAccessExpired: testValue } } });
testFieldSelector(fieldKeys.isCertDownloadable,
'returns certificates isDownloadable value',
{ data: { certificates: { isDownloadable: testValue } } });
testFieldSelector(fieldKeys.isCertEarnedButUnavailable,
'returns true if is certificate is earned but not available',
{
data: { certificates: { isEarned: true, isAvailable: false } },
expected: true,
},
[
{
condition: 'certificate is not earned',
data: { certificates: { isEarned: false, isAvailable: false } },
expected: false,
message: 'returns false',
},
{
condition: 'certificate is available',
data: { certificates: { isEarned: true, isAvailable: true } },
expected: false,
message: 'returns false',
},
]);
testFieldSelector(fieldKeys.isCourseRunPending,
'returns courseRun isPending value',
{ data: { courseRun: { isPending: testValue } } });
testFieldSelector(fieldKeys.isCourseRunStarted,
'returns courseRun isStarted value',
{ data: { courseRun: { isStarted: testValue } } });
testFieldSelector(fieldKeys.isCourseRunFinished,
'returns courseRun isFinished value',
{ data: { courseRun: { isFinished: testValue } } });
testFieldSelector(fieldKeys.isEmailEnabled,
'returns enrollment isEmailEnabled value',
{ data: { enrollment: { isEmailEnabled: testValue } } });
testFieldSelector(fieldKeys.isEntitlement,
'returns entitlements isEntitlement value',
{ data: { entitlements: { isEntitlement: testValue } } });
testFieldSelector(fieldKeys.isEntitlementExpired,
'returns entitlements isExpired value',
{ data: { entitlements: { isExpired: testValue } } });
testFieldSelector(fieldKeys.isEntitlementFulfilled,
'returns entitlements isFulfilled value',
{ data: { entitlements: { isFulfilled: testValue } } });
testFieldSelector(fieldKeys.isVerified,
'returns enrollments isVerified value',
{ data: { enrollment: { isVerified: testValue } } });
testFieldSelector(fieldKeys.isRestricted,
'returns certificates isRestricted value',
{ data: { certificates: { isRestricted: testValue } } });
testFieldSelector(fieldKeys.isPassing,
'returns grades isPassing value',
{ data: { grades: { isPassing: testValue } } });
testFieldSelector(fieldKeys.minPassingGrade,
'returns course run minPassingGrade value',
{ data: { courseRun: { minPassingGrade: testValue } } });
testFieldSelector(fieldKeys.providerName,
'returns provider name value',
{ data: { provider: { name: testValue } } },
[{
condition: 'provider is not known',
data: {},
expected: undefined,
message: 'returns undefined',
}]);
testFieldSelector(fieldKeys.relatedPrograms,
'returns relatedPrograms value',
{ data: { relatedPrograms: testValue } });
testFieldSelector(fieldKeys.isCourseRunActive,
'returns true if course run is started but not finished',
{
data: { courseRun: { isStarted: true, isFinished: false } },
expected: true,
},
[
{
condition: 'is not started',
data: { courseRun: { isStarted: false, isFinished: false } },
expected: false,
message: 'returns false',
}, {
condition: 'is finished',
data: { courseRun: { isStarted: true, isFinished: true } },
expected: false,
message: 'returns false',
}]);
});
describe('programs', () => {
test('estimatedNumberOfWeeks returns value from passed program data', () => {
expect(
programs.estimatedNumberOfWeeks({ estimatedNumberOfWeeks: testValue }),
).toEqual(testValue);
});
test('numberOfCourses returns value from passed program data', () => {
expect(programs.numberOfCourses({ numberOfCourses: testValue })).toEqual(testValue);
});
test('programType returns value from passed program data', () => {
expect(programs.programType({ programType: testValue })).toEqual(testValue);
});
test('programTypeUrl returns value from passed program data', () => {
expect(programs.programTypeUrl({ programTypeUrl: testValue })).toEqual(testValue);
});
test('provider returns value from passed program data', () => {
expect(programs.provider({ provider: testValue })).toEqual(testValue);
});
test('title returns value from passed program data', () => {
expect(programs.title({ title: testValue })).toEqual(testValue);
});
});
});

23
src/data/redux/hooks.js Normal file
View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import appSelectors from './app/selectors';
const { courseCard } = appSelectors;
export const useEmailConfirmationData = () => useSelector(appSelectors.emailConfirmation);
export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpriseDashboards);
export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (courseNumber) => useSelector(
(state) => selector(state, courseNumber),
);
export const useCardCertificateData = useCourseCardData(courseCard.certificates);
export const useCardCourseData = useCourseCardData(courseCard.course);
export const useCardCourseRunData = useCourseCardData(courseCard.courseRun);
export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment);
export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements);
export const useCardGradeData = useCourseCardData(courseCard.grades);
export const useCardProviderData = useCourseCardData(courseCard.provider);
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);

View File

@@ -4,14 +4,14 @@ import { StrictDict } from 'utils';
import * as app from './app';
import * as requests from './requests';
import * as cardData from './cardData';
import * as hooks from './hooks';
export { default as thunkActions } from './thunkActions';
const modules = {
app,
requests,
cardData,
};
const moduleProps = (propName) => Object.keys(modules).reduce(
@@ -28,6 +28,6 @@ const actions = StrictDict(moduleProps('actions'));
const selectors = StrictDict(moduleProps('selectors'));
export { actions, selectors };
export { actions, selectors, hooks };
export default rootReducer;

View File

@@ -14,18 +14,20 @@ import requests from './requests';
*/
export const initialize = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: (({ enrollments, entitlements }) => {
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
dispatch(actions.app.loadEnrollments(enrollments));
dispatch(actions.app.loadEntitlements(entitlements));
dispatch(actions.app.loadGlobalData(globalData));
}),
}))
);
export const refreshList = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: (({ enrollments, entitlements }) => {
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
dispatch(actions.app.loadEnrollments(enrollments));
dispatch(actions.app.loadEntitlements(entitlements));
dispatch(actions.app.loadGlobalData(globalData));
}),
}))
);

View File

@@ -18,6 +18,7 @@ import {
const initializeList = () => Promise.resolve({
enrollments: fakeData.courseRunData,
entitlements: fakeData.entitlementCourses,
...fakeData.globalData,
});
export default { initializeList };

View File

@@ -21,20 +21,19 @@ export const relatedPrograms = [
title: 'Relativity in Modern Mechanics',
programUrl: 'www.edx/my-program',
programType: 'MicroBachelors Program',
programTypeUrl: 'www.edx/my-program-type',
numberOfCourses: 3,
estimatedDuration: '4 weeks',
estimatedNumberOfWeeks: 4,
},
{
provider: 'University of Maryland',
bannerUrl: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
logoUrl: 'https://prod-discovery.edx-cdn.org/organization/certificate_logos/b9dc96da-b3fc-45a6-b6b7-b8e12eb79335-ac60112330e3.png',
title: 'Pandering for Modern Professionals',
programUrl: 'www.edx/my-program',
programUrl: 'www.edx/my-program-2',
programType: 'MicroBachelors Program',
programTypeUrl: 'www.edx/my-program-type',
numberOfCourses: 3,
estimatedDuration: '4 weeks',
estimatedNumberOfWeeks: 4,
},
];
@@ -50,23 +49,48 @@ const logos = {
const pastDate = '11/11/2000';
const futureDate = '11/11/3030';
const globalData = {
emailConfirmation: {
isNeeded: true,
sendEmailUrl: 'sendConfirmation@edx.org',
},
enterpriseDashboards: {
availableDashboards: [
{ label: 'edX', url: 'edx.org/edx-dashboard' },
{ label: 'harvard', url: 'edx.org/harvard-dashboard' },
],
mostRecentDashboard: { label: 'edX', url: 'edx.org/edx-dashboard' },
},
platformSettings: {
supportEmail: 'support@example.com',
billingEmail: 'billing@email.com',
courseSearchUrl: 'edx.com/course-search',
},
};
export const genCourseRunData = (data = {}) => ({
isPending: false,
isStarted: false,
isFinished: false,
isArchived: false,
accessExpirationDate: futureDate,
endDate: futureDate,
minPassingGrade: 70,
homeUrl: 'edx.com/courses/my-course-url/home',
marketingUrl: 'edx.com/courses/my-course-url/marketing',
progressUrl: 'edx.com/courses/my-course-url/progress',
unenrollUrl: 'edx.com/courses/my-course-url/unenroll',
resumeUrl: 'edx.com/courses/my-course-url/resume',
...data,
});
export const genEnrollmentData = (data = {}) => ({
isAudit: true,
isVerified: false,
accessExpirationDate: futureDate,
canUpgrade: data.verified ? null : true,
hasStarted: false,
isAudit: true,
isAuditAccessExpired: data.verified ? null : false,
isEmailEnabled: false,
isEnrolled: true,
isVerified: false,
lastEnrolled: pastDate,
...data,
});
@@ -167,7 +191,7 @@ export const courseRuns = [
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: false },
courseRun: { isFinished: true, endDate: pastDate },
courseRun: { isArchived: true, endDate: pastDate },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
@@ -277,7 +301,6 @@ export const courseRuns = [
grades: { isPassing: true },
courseRun: {
isStarted: true,
isFinished: true,
isArchived: true,
endDate: pastDate,
},
@@ -372,4 +395,5 @@ export const courseRunData = courseRuns.map(
export default {
courseRunData,
entitlementCourses,
globalData,
};

View File

@@ -1,19 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
export const useCardValue = (courseNumber, sel) => (
useSelector(state => sel(state, courseNumber))
);
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
);
@@ -23,7 +10,6 @@ export const nullMethod = () => ({});
export { useIntl };
export default {
useCardValues,
useValueCallback,
nullMethod,
useIntl,

View File

@@ -146,23 +146,7 @@ jest.mock('react-redux', () => {
};
});
jest.mock('hooks', () => {
const formatMessage = jest.fn((msg, values) => ({ formatted: { msg, values } }));
return {
...jest.requireActual('hooks'),
useIntl: () => ({
formatMessage,
formatDate: jest.fn((date) => ({ formatted: date })),
}),
useCardValues: jest.fn((courseNumber, mapping) => (
Object.keys(mapping).reduce(
(obj, key) => ({
...obj,
[key]: { selector: mapping[key], courseNumber },
}),
{},
)
)),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
};
});
jest.mock('hooks', () => ({
...jest.requireActual('hooks'),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
}));

View File

@@ -147,11 +147,13 @@ describe('ESG app integration tests', () => {
courseData.course.title,
);
cardDetails = inspector.get.card.details(card);
console.log({ enrollment: courseData.enrollment });
[
courseData.provider.name,
courseNumber,
appMessages.withValues.CourseCard.accessExpires({
accessExpirationDate: courseData.courseRun.accessExpirationDate,
accessExpirationDate: courseData.enrollment.accessExpirationDate,
}),
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));

View File

@@ -1,13 +1,7 @@
import react from 'react';
import { selectors } from 'data/redux';
import * as appHooks from 'hooks';
import { StrictDict } from 'utils';
const { cardData } = selectors;
/**
* Mocked formatMessage provided by react-intl
*/
@@ -195,27 +189,3 @@ export class MockUseState {
});
}
}
/**
* Test that useCardValues was called with the given courseNumber and selector mapping.
* @param {string} courseNumber - course run identifier
* @param {obj} mapping - value mapping { <requestedKey>: <selectorFieldKey> }
* @param {[func]} beforeEachFn - optional beforeEach method
*/
export const testCardValues = (courseNumber, mapping, beforeEachFn) => {
describe('cardData values', () => {
if (beforeEachFn) {
beforeEach(beforeEachFn);
}
let mapped;
test('passess correct 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.useCardValues.mock.calls;
expect(mapped[key]).toEqual(cardData[mapping[key]]);
});
});
});
};