Merge pull request #2 from edx/bw/banner-tests
chore: Banner tests and api updates
This commit is contained in:
@@ -2,82 +2,93 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { MailtoLink, Hyperlink } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
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,
|
||||
});
|
||||
if (data.isRestricted) {
|
||||
const certificate = appHooks.useCardCertificateData(courseNumber);
|
||||
const {
|
||||
isAudit,
|
||||
isVerified,
|
||||
hasFinished,
|
||||
} = appHooks.useCardEnrollmentData(courseNumber);
|
||||
const { isPassing } = appHooks.useCardGradeData(courseNumber);
|
||||
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(courseNumber);
|
||||
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{restrictedMessage}<Hyperlink destination="info@example.com">info@example.com</Hyperlink>
|
||||
{data.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>
|
||||
</>
|
||||
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
|
||||
{isVerified && ' '}
|
||||
{isVerified && formatMessage(
|
||||
messages.certRefundContactBilling,
|
||||
{ billingEmail: emailLink(billingEmail) },
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (!data.isPassing) {
|
||||
if (data.isAudit) {
|
||||
return (<Banner> Grade required to pass the course: {data.minPassingGrade}% </Banner>);
|
||||
if (!isPassing) {
|
||||
if (isAudit) {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.passingGrade, { minPassingGrade })}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (data.isCourseRunFinished) {
|
||||
if (hasFinished) {
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
You are not eligible for a certificate. <Hyperlink destination="">View grades.</Hyperlink>
|
||||
{formatMessage(messages.notEligibleForCert)}.
|
||||
{' '}
|
||||
<Hyperlink destination={progressUrl}>{formatMessage(messages.viewGrades)}</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
Grade required for a certificate: {data.minPassingGrade}%
|
||||
{formatMessage(messages.certMinGrade, { minPassingGrade })}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (data.isCertDownloadable) {
|
||||
if (data.certPreviewUrl) {
|
||||
if (certificate.isDownloadable) {
|
||||
if (certificate.certPreviewUrl) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
Congratulations. Your certificate is ready.
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink destination={data.certPreviewUrl}>View Certificate.</Hyperlink>
|
||||
<Hyperlink destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
Congratulations. Your certificate is ready.
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink destination={data.certDownloadUrl}>Download Certificate.</Hyperlink>
|
||||
<Hyperlink destination={certificate.certDownloadUrl}>
|
||||
{formatMessage(messages.downloadCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (data.isCertEarnedButUnavailable) {
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
Your grade and certificate will be ready after {data.certAvailableDate}.
|
||||
{formatMessage(
|
||||
messages.gradeAndCertReadyAfter,
|
||||
{ availableDate: certificate.availableDate },
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { CourseBanner } from './CourseBanner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
let el;
|
||||
|
||||
const enrollmentData = {
|
||||
isVerified: false,
|
||||
canUpgrade: false,
|
||||
isAuditAccessExpired: false,
|
||||
};
|
||||
const courseRunData = {
|
||||
isActive: false,
|
||||
};
|
||||
const courseData = {
|
||||
website: 'test-course-website',
|
||||
};
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const {
|
||||
course = {},
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
} = overrides;
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...courseData,
|
||||
...course,
|
||||
});
|
||||
appHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
});
|
||||
appHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
...enrollmentData,
|
||||
...enrollment,
|
||||
});
|
||||
el = shallow(<CourseBanner courseNumber={courseNumber} />);
|
||||
};
|
||||
|
||||
describe('CourseBanner', () => {
|
||||
it('initializes data with course number from enrollment, course and course run data', () => {
|
||||
render();
|
||||
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
|
||||
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
|
||||
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
|
||||
});
|
||||
test('no display if learner is verified', () => {
|
||||
render({ enrollment: { isVerified: true } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
describe('audit access expired, can upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('audit access expired, cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('course run active and cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ courseRun: { isActive: true } });
|
||||
});
|
||||
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage);
|
||||
const link = el.find(Hyperlink);
|
||||
expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage);
|
||||
expect(link.props().destination).toEqual(courseData.website);
|
||||
});
|
||||
});
|
||||
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -2,45 +2,50 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
const { cardData } = selectors;
|
||||
import messages from './messages';
|
||||
|
||||
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>
|
||||
Your audit access to this course has expired. Upgrade now to access your course again.
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{formatMessage(messages.upgradeToAccess)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Banner>
|
||||
Your audit access to this course has expired. <Hyperlink destination="">Find another course</Hyperlink>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
<Hyperlink destination="">{formatMessage(messages.findAnotherCourse)}</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (courseData.isCourseRunActive && !courseData.canUpgrade) {
|
||||
if (courseRun.isActive && !canUpgrade) {
|
||||
return (
|
||||
<Banner>
|
||||
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
|
||||
{formatMessage(messages.upgradeDeadlinePassed)}
|
||||
{' '}
|
||||
<Hyperlink destination={courseData.courseWebsite || ''}>Explore course details.</Hyperlink>
|
||||
<Hyperlink destination={course.website || ''}>
|
||||
{formatMessage(messages.exploreCourseDetails)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { CourseBanner } from './CourseBanner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
let el;
|
||||
|
||||
const enrollmentData = {
|
||||
isVerified: false,
|
||||
canUpgrade: false,
|
||||
isAuditAccessExpired: false,
|
||||
};
|
||||
const courseRunData = {
|
||||
isActive: false,
|
||||
};
|
||||
const courseData = {
|
||||
website: 'test-course-website',
|
||||
};
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const {
|
||||
course = {},
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
} = overrides;
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...courseData,
|
||||
...course,
|
||||
});
|
||||
appHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
});
|
||||
appHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
...enrollmentData,
|
||||
...enrollment,
|
||||
});
|
||||
el = shallow(<CourseBanner courseNumber={courseNumber} />);
|
||||
};
|
||||
|
||||
describe('CourseBanner', () => {
|
||||
it('initializes data with course number from enrollment, course and course run data', () => {
|
||||
render();
|
||||
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
|
||||
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
|
||||
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
|
||||
});
|
||||
test('no display if learner is verified', () => {
|
||||
render({ enrollment: { isVerified: true } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
describe('audit access expired, can upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('audit access expired, cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('course run active and cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ courseRun: { isActive: true } });
|
||||
});
|
||||
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage);
|
||||
const link = el.find(Hyperlink);
|
||||
expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage);
|
||||
expect(link.props().destination).toEqual(courseData.website);
|
||||
});
|
||||
});
|
||||
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +1,52 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@edx/paragon';
|
||||
|
||||
import { useCardValues } from 'hooks';
|
||||
import { selectors } from 'data/redux';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
const { cardData } = selectors;
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ courseNumber }) => {
|
||||
const data = useCardValues(courseNumber, {
|
||||
canChange: cardData.canChangeEntitlementSession,
|
||||
isEntitlement: cardData.isEntitlement,
|
||||
isExpired: cardData.isEntitlementExpired,
|
||||
isFulfilled: cardData.isEntitlementFulfilled,
|
||||
});
|
||||
const {
|
||||
isEntitlement,
|
||||
hasSessions,
|
||||
isFulfilled,
|
||||
changeDeadline,
|
||||
showExpirationWarning,
|
||||
} = appHooks.useCardEntitlementsData(courseNumber);
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatDate, formatMessage } = useIntl();
|
||||
|
||||
if (!data.isEntitlement) {
|
||||
if (!isEntitlement) {
|
||||
return null;
|
||||
}
|
||||
if (data.isExpired || data.isFulfilled) {
|
||||
return null;
|
||||
|
||||
if (!hasSessions && !isFulfilled) {
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
{formatMessage(messages.entitlementsUnavailable, {
|
||||
emailLink: supportEmail && <MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>,
|
||||
})}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return data.canChange
|
||||
? (<Banner>You must select a session to access the course.</Banner>)
|
||||
: (<Banner>The deadline to select a session has passed</Banner>);
|
||||
if (showExpirationWarning) {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.entitlementsExpiringSoon, {
|
||||
changeDeadline: formatDate(changeDeadline),
|
||||
selectSessionButton: (
|
||||
<Button variant="link" size="inline" className="m-0 p-0">
|
||||
{formatMessage(messages.selectSession)}
|
||||
</Button>
|
||||
),
|
||||
})}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
EntitlementBanner.propTypes = {
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: jest.fn(),
|
||||
}));
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useCardEntitlementsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
let el;
|
||||
|
||||
const entitlementsData = {
|
||||
isEntitlement: true,
|
||||
hasSessions: true,
|
||||
isFulfilled: false,
|
||||
changeDeadline: 'test-deadline',
|
||||
showExpirationWarning: false,
|
||||
};
|
||||
const platformData = { supportEmail: 'test-support-email' };
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const { entitlements = {} } = overrides;
|
||||
appHooks.useCardEntitlementsData.mockReturnValueOnce({ ...entitlementsData, ...entitlements });
|
||||
appHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
|
||||
el = shallow(<EntitlementBanner courseNumber={courseNumber} />);
|
||||
};
|
||||
|
||||
describe('EntitlementBanner', () => {
|
||||
beforeEach(() => {
|
||||
useIntl.mockReturnValue({
|
||||
formatDate: (date) => date,
|
||||
formatMessage: (message, values) => <div {...{ message, values }} />,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('initializes data with course number from entitlements', () => {
|
||||
render();
|
||||
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber);
|
||||
});
|
||||
test('no display if not an entitlement', () => {
|
||||
render({ entitlements: { isEntitlement: false } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot: no sessions available', () => {
|
||||
render({ entitlements: { isFulfilled: false, hasSessions: false } });
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: expiration warning', () => {
|
||||
render({ entitlements: { showExpirationWarning: true } });
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('no display if sessions available and not displaying warning', () => {
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = `
|
||||
<Banner>
|
||||
Your audit access to this course has expired.
|
||||
|
||||
Upgrade now to access your course again.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
|
||||
<Banner>
|
||||
Your audit access to this course has expired.
|
||||
|
||||
<Hyperlink
|
||||
destination=""
|
||||
>
|
||||
Find another course
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = `
|
||||
<Banner>
|
||||
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
|
||||
|
||||
<Hyperlink
|
||||
destination="test-course-website"
|
||||
>
|
||||
Explore course details.
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = `
|
||||
<Banner>
|
||||
Your audit access to this course has expired.
|
||||
|
||||
Upgrade now to access your course again.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
|
||||
<Banner>
|
||||
Your audit access to this course has expired.
|
||||
|
||||
<Hyperlink
|
||||
destination=""
|
||||
>
|
||||
Find another course
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = `
|
||||
<Banner>
|
||||
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
|
||||
|
||||
<Hyperlink
|
||||
destination="test-course-website"
|
||||
>
|
||||
Explore course details.
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
`;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EntitlementBanner snapshot: expiration warning 1`] = `
|
||||
<Banner>
|
||||
<div
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
|
||||
"description": "Entitlements course message when the entitlement is expiring soon.",
|
||||
"id": "learner-dash.courseCard.banners.entitlementsExpiringSoon",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"changeDeadline": "test-deadline",
|
||||
"selectSessionButton": <Button
|
||||
className="m-0 p-0"
|
||||
size="inline"
|
||||
variant="link"
|
||||
>
|
||||
<div
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "select a session",
|
||||
"description": "Entitlements session selection link text",
|
||||
"id": "learner-dash.courseCard.banners.selectSession",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Button>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`EntitlementBanner snapshot: no sessions available 1`] = `
|
||||
<Banner
|
||||
variant="warning"
|
||||
>
|
||||
<div
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
|
||||
"description": "Entitlements course message when no sessions are available",
|
||||
"id": "learner-dash.courseCard.banners.entitlementsUnavailable",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"emailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
test-support-email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
96
src/containers/CourseCard/components/Banners/messages.js
Normal file
96
src/containers/CourseCard/components/Banners/messages.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
auditAccessExpired: {
|
||||
id: 'learner-dash.courseCard.banners.auditAccessExpired',
|
||||
description: 'Audit access expiration banner message',
|
||||
defaultMessage: 'Your audit access to this course has expired.',
|
||||
},
|
||||
upgradeToAccess: {
|
||||
id: 'learner-dash.courseCard.banners.upgradeToAccess',
|
||||
description: 'Upgrade prompt for audit-expired learners that can still upgrade',
|
||||
defaultMessage: 'Upgrade now to access your course again.',
|
||||
},
|
||||
findAnotherCourse: {
|
||||
id: 'learner-dash.courseCard.banners.findAnotherCourse',
|
||||
description: 'Action prompt taking learners to course exploration',
|
||||
defaultMessage: 'Find another course',
|
||||
},
|
||||
upgradeDeadlinePassed: {
|
||||
id: 'learner-dash.courseCard.banners.upgradeDeadlinePassed',
|
||||
description: 'Audit upgrade deadline passed banner message',
|
||||
defaultMessage: 'Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.',
|
||||
},
|
||||
exploreCourseDetails: {
|
||||
id: 'learner-dash.courseCard.banners.exploreCourseDetails',
|
||||
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 {supportEmail}.',
|
||||
},
|
||||
certRefundContactBilling: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRefundContactBilling',
|
||||
description: 'Message to learners to contact billing for certificate refunds',
|
||||
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}',
|
||||
},
|
||||
passingGrade: {
|
||||
id: 'learner-dash.courseCard.banners.passingGrade',
|
||||
description: 'Message to learners with minimum passing grade for the course',
|
||||
defaultMessage: 'Grade required to pass the course: {minPassingGrade}',
|
||||
},
|
||||
notEligibleForCert: {
|
||||
id: 'learner-dash.courseCard.banners.notEligibleForCert',
|
||||
description: 'Certificate inelligibility message',
|
||||
defaultMessage: 'You are not eligible for a certificate.',
|
||||
},
|
||||
viewGrades: {
|
||||
id: 'learner-dash.courseCard.banners.viewGrades',
|
||||
description: 'Gradses link text',
|
||||
defaultMessage: 'View grades.',
|
||||
},
|
||||
certReady: {
|
||||
id: 'learner-dash.courseCard.banners.certReady',
|
||||
description: 'Certificate ready message',
|
||||
defaultMessage: 'Congratulations. Your certificate is ready.',
|
||||
},
|
||||
viewCertificate: {
|
||||
id: 'learner-dash.courseCard.banners.viewCertificate',
|
||||
description: 'Certificate link text',
|
||||
defaultMessage: 'View Certificate.',
|
||||
},
|
||||
certMinGrade: {
|
||||
id: 'learner-dash.courseCard.banners.certMinGrade',
|
||||
description: 'Passing grade requirement message',
|
||||
defaultMessage: 'Grade required for a certificate: {minPassingGrade}',
|
||||
},
|
||||
downloadCertificate: {
|
||||
id: 'learner-dash.courseCard.banners.downloadCertificate',
|
||||
description: 'Certificate download link text',
|
||||
defaultMessage: 'Download Certificate.',
|
||||
},
|
||||
gradeAndCertReadyAfter: {
|
||||
id: 'learner-dash.courseCard.banners.gradseAndCertReadyAfter',
|
||||
description: 'Grade and certificate availability date message',
|
||||
defaultMessage: 'Your grade and certificate will be ready after {availableDate}.',
|
||||
},
|
||||
entitlementsUnavailable: {
|
||||
id: 'learner-dash.courseCard.banners.entitlementsUnavailable',
|
||||
description: 'Entitlements course message when no sessions are available',
|
||||
defaultMessage: 'There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.',
|
||||
},
|
||||
entitlementsExpiringSoon: {
|
||||
id: 'learner-dash.courseCard.banners.entitlementsExpiringSoon',
|
||||
description: 'Entitlements course message when the entitlement is expiring soon.',
|
||||
defaultMessage: 'You must {selectSessionButton} by {changeDeadline} to access the course.',
|
||||
},
|
||||
selectSession: {
|
||||
id: 'learner-dash.courseCard.banners.selectSession',
|
||||
description: 'Entitlements session selection link text',
|
||||
defaultMessage: 'select a session',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,37 +1,35 @@
|
||||
import { Locked } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { useIntl, 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) };
|
||||
|
||||
@@ -1,54 +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 } = appHooks.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 { formatMessage } = useIntl();
|
||||
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',
|
||||
@@ -57,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',
|
||||
@@ -69,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),
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,46 +1,40 @@
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import { useIntl, useCardValues } from 'hooks';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -1,12 +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';
|
||||
@@ -15,121 +22,115 @@ const hookKeys = keyStore(hooks);
|
||||
|
||||
describe('CourseCard hooks', () => {
|
||||
let out;
|
||||
const { formatMessage, formatDate } = appHooks.useIntl();
|
||||
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: { isArchived: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.courseEnded,
|
||||
{ endDate: formatDate(endDate) },
|
||||
{ endDate: formatDate(courseRunData.endDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,38 +16,28 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="Receive course emails?"
|
||||
/>
|
||||
Receive course emails?
|
||||
</h4>
|
||||
<Form.Switch
|
||||
checked={true}
|
||||
onChange={[MockFunction hooks.onToggle]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Course emails are on"
|
||||
/>
|
||||
Course emails are on
|
||||
</Form.Switch>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Course emailsi include important information about your course."
|
||||
/>
|
||||
Course emailsi include important information about your course.
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction closeModal]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Nevermind"
|
||||
/>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.save]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Save settings"
|
||||
/>
|
||||
Save settings
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
@@ -70,38 +60,28 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: true 1`] = `
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="Receive course emails?"
|
||||
/>
|
||||
Receive course emails?
|
||||
</h4>
|
||||
<Form.Switch
|
||||
checked={true}
|
||||
onChange={[MockFunction hooks.onToggle]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Course emails are on"
|
||||
/>
|
||||
Course emails are on
|
||||
</Form.Switch>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Course emailsi include important information about your course."
|
||||
/>
|
||||
Course emailsi include important information about your course.
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction closeModal]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Nevermind"
|
||||
/>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.save]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Save settings"
|
||||
/>
|
||||
Save settings
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
@@ -124,38 +104,28 @@ exports[`EmailSettingsModal render snapshot: emails enabled, show: true 1`] = `
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="Receive course emails?"
|
||||
/>
|
||||
Receive course emails?
|
||||
</h4>
|
||||
<Form.Switch
|
||||
checked={false}
|
||||
onChange={[MockFunction hooks.onToggle]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Course emails are off"
|
||||
/>
|
||||
Course emails are off
|
||||
</Form.Switch>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Course emailsi include important information about your course."
|
||||
/>
|
||||
Course emailsi include important information about your course.
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction closeModal]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Nevermind"
|
||||
/>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.save]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Save settings"
|
||||
/>
|
||||
Save settings
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,19 +9,13 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
|
||||
isOpen={false}
|
||||
onClose={[MockFunction props.closeModal]}
|
||||
size="lg"
|
||||
title={
|
||||
<formatMessage
|
||||
msg="Related Programs"
|
||||
/>
|
||||
}
|
||||
title="Related Programs"
|
||||
>
|
||||
<ModalDialog.Header
|
||||
as="h3"
|
||||
className="programs-title m-0 p-0"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Related Programs"
|
||||
/>
|
||||
Related Programs
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Header
|
||||
as="h4"
|
||||
@@ -33,9 +27,7 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
|
||||
className="pl-0"
|
||||
>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in"
|
||||
/>
|
||||
Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in
|
||||
</p>
|
||||
<CardGrid
|
||||
columnSizes={
|
||||
@@ -93,19 +85,13 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
|
||||
isOpen={true}
|
||||
onClose={[MockFunction props.closeModal]}
|
||||
size="lg"
|
||||
title={
|
||||
<formatMessage
|
||||
msg="Related Programs"
|
||||
/>
|
||||
}
|
||||
title="Related Programs"
|
||||
>
|
||||
<ModalDialog.Header
|
||||
as="h3"
|
||||
className="programs-title m-0 p-0"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Related Programs"
|
||||
/>
|
||||
Related Programs
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Header
|
||||
as="h4"
|
||||
@@ -117,9 +103,7 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
|
||||
className="pl-0"
|
||||
>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in"
|
||||
/>
|
||||
Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in
|
||||
</p>
|
||||
<CardGrid
|
||||
columnSizes={
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,18 +12,10 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
|
||||
>
|
||||
<Card.ImageCap
|
||||
className="program-card-banner"
|
||||
logoAlt={
|
||||
<formatMessage
|
||||
msg="Provider logo"
|
||||
/>
|
||||
}
|
||||
logoAlt="Provider logo"
|
||||
logoSrc="props.data.logoUrl"
|
||||
src="props.data.bannerUrl"
|
||||
srcAlt={
|
||||
<formatMessage
|
||||
msg="Programm banner"
|
||||
/>
|
||||
}
|
||||
srcAlt="Programm banner"
|
||||
/>
|
||||
<Card.Header
|
||||
subtitle={
|
||||
@@ -57,13 +49,9 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
|
||||
<div
|
||||
className="program-summary mt-2"
|
||||
>
|
||||
<formatMessage
|
||||
msg="{numCourses} Courses"
|
||||
/>
|
||||
2 Courses
|
||||
•
|
||||
<formatMessage
|
||||
msg="{numWeeks} Weeks"
|
||||
/>
|
||||
1 Weeks
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,30 +3,22 @@
|
||||
exports[`UnenrollConfirmModal ConfirmPane snapshot 1`] = `
|
||||
<Fragment>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="Unenroll from course?"
|
||||
/>
|
||||
Unenroll from course?
|
||||
</h4>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Progress that you've made so far will not be saved"
|
||||
/>
|
||||
Progress that you've made so far will not be saved
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction props.handleClose]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Nevermind"
|
||||
/>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction props.handleConfirm]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Unenroll"
|
||||
/>
|
||||
Unenroll
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Fragment>
|
||||
|
||||
@@ -3,22 +3,16 @@
|
||||
exports[`UnenrollConfirmModal FinishedPane snapshot: did not give reason 1`] = `
|
||||
<Fragment>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="You are unenrolled"
|
||||
/>
|
||||
You are unenrolled
|
||||
</h4>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="This course will be removed from your dashboard."
|
||||
/>
|
||||
This course will be removed from your dashboard.
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction props.handleClose]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Return to dashboard"
|
||||
/>
|
||||
Return to dashboard
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Fragment>
|
||||
@@ -27,25 +21,17 @@ exports[`UnenrollConfirmModal FinishedPane snapshot: did not give reason 1`] = `
|
||||
exports[`UnenrollConfirmModal FinishedPane snapshot: gave reason 1`] = `
|
||||
<Fragment>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="You are unenrolled"
|
||||
/>
|
||||
You are unenrolled
|
||||
</h4>
|
||||
<p>
|
||||
<formatMessage
|
||||
msg="Thank you for sharing your reason for unenrolling. "
|
||||
/>
|
||||
<formatMessage
|
||||
msg="This course will be removed from your dashboard."
|
||||
/>
|
||||
Thank you for sharing your reason for unenrolling.
|
||||
This course will be removed from your dashboard.
|
||||
</p>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction props.handleClose]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Return to dashboard"
|
||||
/>
|
||||
Return to dashboard
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Fragment>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
exports[`UnenrollConfirmModal ReasonPane snapshot 1`] = `
|
||||
<Fragment>
|
||||
<h4>
|
||||
<formatMessage
|
||||
msg="What's your main reason for unenrolling?"
|
||||
/>
|
||||
What's your main reason for unenrolling?
|
||||
</h4>
|
||||
<Form.RadioSet
|
||||
name="unenrollReason"
|
||||
@@ -16,84 +14,62 @@ exports[`UnenrollConfirmModal ReasonPane snapshot 1`] = `
|
||||
key="prereqs"
|
||||
value="prereqs"
|
||||
>
|
||||
<formatMessage
|
||||
msg="I don't have the academic or language prerequisites"
|
||||
/>
|
||||
I don't have the academic or language prerequisites
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="difficulty"
|
||||
value="difficulty"
|
||||
>
|
||||
<formatMessage
|
||||
msg="The course material was too hard"
|
||||
/>
|
||||
The course material was too hard
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="goals"
|
||||
value="goals"
|
||||
>
|
||||
<formatMessage
|
||||
msg="This won't help me reach my goals"
|
||||
/>
|
||||
This won't help me reach my goals
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="broken"
|
||||
value="broken"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Something was broken"
|
||||
/>
|
||||
Something was broken
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="time"
|
||||
value="time"
|
||||
>
|
||||
<formatMessage
|
||||
msg="I don't have the time"
|
||||
/>
|
||||
I don't have the time
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="browse"
|
||||
value="browse"
|
||||
>
|
||||
<formatMessage
|
||||
msg="I just wanted to browse the material"
|
||||
/>
|
||||
I just wanted to browse the material
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="support"
|
||||
value="support"
|
||||
>
|
||||
<formatMessage
|
||||
msg="I don't have enough support"
|
||||
/>
|
||||
I don't have enough support
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="quality"
|
||||
value="quality"
|
||||
>
|
||||
<formatMessage
|
||||
msg="I am not happy with the quality of the content"
|
||||
/>
|
||||
I am not happy with the quality of the content
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="easy"
|
||||
value="easy"
|
||||
>
|
||||
<formatMessage
|
||||
msg="The course material was too easy"
|
||||
/>
|
||||
The course material was too easy
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
value="custom"
|
||||
>
|
||||
<Form.Control
|
||||
onChange={[MockFunction props.reason.customOption.onChange]}
|
||||
placeholder={
|
||||
<formatMessage
|
||||
msg="Other"
|
||||
/>
|
||||
}
|
||||
placeholder="Other"
|
||||
value="props.reason.customOption.value"
|
||||
/>
|
||||
</Form.Radio>
|
||||
@@ -103,16 +79,12 @@ exports[`UnenrollConfirmModal ReasonPane snapshot 1`] = `
|
||||
onClick={[MockFunction props.reason.skip]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<formatMessage
|
||||
msg="Skip"
|
||||
/>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction props.reason.submit]}
|
||||
>
|
||||
<formatMessage
|
||||
msg="Submit"
|
||||
/>
|
||||
Submit
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Fragment>
|
||||
|
||||
@@ -5,6 +5,11 @@ const initialState = {
|
||||
enrollments: [],
|
||||
courseData: {},
|
||||
entitlements: [],
|
||||
emailConfirmation: {},
|
||||
enterpriseDashboards: {},
|
||||
platformSettings: {},
|
||||
suggestedCourses: {},
|
||||
filterState: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -12,18 +17,30 @@ const app = createSlice({
|
||||
name: 'app',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadEnrollments: (state, { payload }) => ({
|
||||
loadCourses: (state, { payload: { enrollments, entitlements } }) => ({
|
||||
...state,
|
||||
enrollments: payload.map(curr => curr.courseRun.courseNumber),
|
||||
courseData: payload.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr.courseRun.courseNumber]: curr,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
enrollments: [
|
||||
...enrollments.map(curr => curr.courseRun.courseNumber),
|
||||
...entitlements.map(curr => curr.courseRun.courseNumber),
|
||||
],
|
||||
courseData: {
|
||||
...entitlements.reduce(
|
||||
(obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
|
||||
{},
|
||||
),
|
||||
...enrollments.reduce(
|
||||
(obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
|
||||
{},
|
||||
),
|
||||
},
|
||||
}),
|
||||
loadGlobalData: (state, { payload }) => ({
|
||||
...state,
|
||||
emailConfirmation: payload.emailConfirmation,
|
||||
enterpriseDashboards: payload.enterpriseDashboards,
|
||||
platformSettings: payload.platformSettings,
|
||||
suggestedCourses: payload.suggestedCourses,
|
||||
}),
|
||||
loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -20,45 +20,66 @@ describe('app reducer', () => {
|
||||
});
|
||||
};
|
||||
describe('action handlers', () => {
|
||||
test('loadEntitlements loads entitlements from payload', () => {
|
||||
testAction(
|
||||
actions.loadEntitlements(testValue),
|
||||
{ entitlements: testValue },
|
||||
);
|
||||
});
|
||||
describe('loadEnrollments', () => {
|
||||
const enrollments = [
|
||||
describe('loadCourses', () => {
|
||||
const courseIds = [
|
||||
'course-1',
|
||||
'course-2',
|
||||
'course-3',
|
||||
];
|
||||
const courseData = {
|
||||
[enrollments[0]]: {
|
||||
courseRun: { courseNumber: enrollments[0] },
|
||||
const entitlementIds = [
|
||||
'entitlement-course-1',
|
||||
'entitlement-course-2',
|
||||
];
|
||||
const enrollmentData = [
|
||||
{
|
||||
courseRun: { courseNumber: courseIds[0] },
|
||||
course: 1,
|
||||
some: 'data',
|
||||
},
|
||||
[enrollments[1]]: {
|
||||
courseRun: { courseNumber: enrollments[1] },
|
||||
{
|
||||
courseRun: { courseNumber: courseIds[1] },
|
||||
course: 2,
|
||||
some: 'other data',
|
||||
},
|
||||
[enrollments[2]]: {
|
||||
courseRun: { courseNumber: enrollments[2] },
|
||||
{
|
||||
courseRun: { courseNumber: courseIds[2] },
|
||||
course: 3,
|
||||
some: 'still different data',
|
||||
},
|
||||
};
|
||||
const enrollmentData = enrollments.map(v => courseData[v]);
|
||||
];
|
||||
const entitlementData = [
|
||||
{
|
||||
courseRun: { courseNumber: entitlementIds[0] },
|
||||
course: 4,
|
||||
some: 'STILL different data',
|
||||
},
|
||||
{
|
||||
courseRun: { courseNumber: entitlementIds[1] },
|
||||
course: 5,
|
||||
some: 'still DIFFERENT data',
|
||||
},
|
||||
];
|
||||
let out;
|
||||
beforeEach(() => {
|
||||
out = reducer(testState, actions.loadEnrollments(enrollmentData));
|
||||
out = reducer(testState, actions.loadCourses({
|
||||
enrollments: enrollmentData,
|
||||
entitlements: entitlementData,
|
||||
}));
|
||||
});
|
||||
it('loads list of courseRun ids into enrollments field', () => {
|
||||
expect(out.enrollments).toEqual(enrollments);
|
||||
expect(out.enrollments).toEqual([
|
||||
...courseIds,
|
||||
...entitlementIds,
|
||||
]);
|
||||
});
|
||||
it('loads object keyed by courseRun ids into courseData field', () => {
|
||||
expect(out.courseData).toEqual(courseData);
|
||||
expect(out.courseData).toEqual({
|
||||
[courseIds[0]]: enrollmentData[0],
|
||||
[courseIds[1]]: enrollmentData[1],
|
||||
[courseIds[2]]: enrollmentData[2],
|
||||
[entitlementIds[0]]: entitlementData[0],
|
||||
[entitlementIds[1]]: entitlementData[1],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,13 +13,87 @@ 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))
|
||||
);
|
||||
|
||||
const dateSixMonthsFromNow = new Date();
|
||||
dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180);
|
||||
|
||||
export const courseCard = StrictDict({
|
||||
certificates: mkCardSelector(({ certificates }) => ({
|
||||
availableDate: certificates.availableDate,
|
||||
certDownloadUrl: certificates.certDownloadUrl,
|
||||
honorCertDownloadUrl: certificates.honorCertDownloadUrl,
|
||||
certPreviewUrl: certificates.certPreviewUrl,
|
||||
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 }) => {
|
||||
const deadline = new Date(entitlements.changeDeadline);
|
||||
const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow;
|
||||
return {
|
||||
canChange: entitlements.canChange,
|
||||
entitlementSessions: entitlements.availableSessions,
|
||||
isEntitlement: entitlements.isEntitlement,
|
||||
isExpired: entitlements.isExpired,
|
||||
isFulfilled: entitlements.isFulfilled,
|
||||
hasSessions: entitlements.availableSessions?.length > 0,
|
||||
changeDeadline: entitlements.changeDeadline,
|
||||
showExpirationWarning,
|
||||
};
|
||||
}),
|
||||
grades: mkCardSelector(({ grades }) => ({ isPassing: grades.isPassing })),
|
||||
provider: mkCardSelector(({ provider }) => ({ name: provider?.name })),
|
||||
relatedPrograms: mkCardSelector(({ relatedPrograms }) => ({
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as selectors } from './selectors';
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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
23
src/data/redux/hooks.js
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
@@ -14,18 +14,18 @@ import requests from './requests';
|
||||
*/
|
||||
export const initialize = () => (dispatch) => (
|
||||
dispatch(requests.initializeList({
|
||||
onSuccess: (({ enrollments, entitlements }) => {
|
||||
dispatch(actions.app.loadEnrollments(enrollments));
|
||||
dispatch(actions.app.loadEntitlements(entitlements));
|
||||
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
|
||||
dispatch(actions.app.loadCourses({ enrollments, entitlements }));
|
||||
dispatch(actions.app.loadGlobalData(globalData));
|
||||
}),
|
||||
}))
|
||||
);
|
||||
|
||||
export const refreshList = () => (dispatch) => (
|
||||
dispatch(requests.initializeList({
|
||||
onSuccess: (({ enrollments, entitlements }) => {
|
||||
dispatch(actions.app.loadEnrollments(enrollments));
|
||||
dispatch(actions.app.loadEntitlements(entitlements));
|
||||
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
|
||||
dispatch(actions.app.loadCourses({ enrollments, entitlements }));
|
||||
dispatch(actions.app.loadGlobalData(globalData));
|
||||
}),
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
*********************************************************************************/
|
||||
const initializeList = () => Promise.resolve({
|
||||
enrollments: fakeData.courseRunData,
|
||||
entitlements: fakeData.entitlementCourses,
|
||||
entitlements: fakeData.entitlementData,
|
||||
...fakeData.globalData,
|
||||
});
|
||||
|
||||
export default { initializeList };
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -49,24 +48,52 @@ const logos = {
|
||||
|
||||
const pastDate = '11/11/2000';
|
||||
const futureDate = '11/11/3030';
|
||||
const soonDate = new Date();
|
||||
soonDate.setDate(soonDate.getDate() + 60);
|
||||
const soonDateStr = soonDate.toDateString();
|
||||
|
||||
const globalData = {
|
||||
emailConfirmation: {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -76,7 +103,9 @@ export const genCertificateData = (data = {}) => ({
|
||||
isAvailable: false,
|
||||
isEarned: false,
|
||||
isDownloadable: false,
|
||||
downloadUrls: null, // { preview, download }
|
||||
certPreviewUrl: 'edx.com/courses/my-course-url/cert-preview',
|
||||
certDownloadUrl: 'edx.com/courses/my-course-url/cert-download',
|
||||
honorCertDownloadUrl: 'edx.com/courses/my-course-url/honor-cert-download',
|
||||
...data,
|
||||
});
|
||||
|
||||
@@ -167,7 +196,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 },
|
||||
},
|
||||
@@ -200,10 +229,8 @@ export const courseRuns = [
|
||||
isAvailable: true,
|
||||
isDownloadable: true,
|
||||
availableDate: pastDate,
|
||||
downloadUrls: {
|
||||
preview: logos.edx,
|
||||
download: logos.social,
|
||||
},
|
||||
certDownloadUrl: logos.social,
|
||||
certPreviewUrl: logos.edx,
|
||||
}),
|
||||
entitlements: { isEntitlement: false },
|
||||
},
|
||||
@@ -217,9 +244,7 @@ export const courseRuns = [
|
||||
isAvailable: true,
|
||||
isDownloadable: true,
|
||||
availableDate: pastDate,
|
||||
downloadUrls: {
|
||||
download: logos.social,
|
||||
},
|
||||
certDownloadUrl: logos.social,
|
||||
}),
|
||||
entitlements: { isEntitlement: false },
|
||||
},
|
||||
@@ -277,7 +302,6 @@ export const courseRuns = [
|
||||
grades: { isPassing: true },
|
||||
courseRun: {
|
||||
isStarted: true,
|
||||
isFinished: true,
|
||||
isArchived: true,
|
||||
endDate: pastDate,
|
||||
},
|
||||
@@ -296,7 +320,6 @@ export const courseRuns = [
|
||||
|
||||
export const entitlementCourses = [
|
||||
{
|
||||
course: { title: genCourseTitle(100) },
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
@@ -308,7 +331,17 @@ export const entitlementCourses = [
|
||||
isExpired: false,
|
||||
},
|
||||
}, {
|
||||
course: { title: genCourseTitle(101) },
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
isRefundable: true,
|
||||
isFulfilled: false,
|
||||
canViewCourse: false,
|
||||
changeDeadline: soonDateStr,
|
||||
canChange: true,
|
||||
isExpired: false,
|
||||
},
|
||||
}, {
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
@@ -320,10 +353,9 @@ export const entitlementCourses = [
|
||||
isExpired: false,
|
||||
},
|
||||
}, {
|
||||
course: { title: genCourseTitle(102) },
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
availableSessions: [],
|
||||
isRefundable: true,
|
||||
isFulfilled: false,
|
||||
canViewCourse: false,
|
||||
@@ -364,12 +396,45 @@ export const courseRunData = courseRuns.map(
|
||||
...data,
|
||||
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
|
||||
...iteratedData[providerIndex],
|
||||
credit: { isPurchased: false, requestStatus: null },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const entitlementData = entitlementCourses.map(
|
||||
(data, index) => {
|
||||
const title = genCourseTitle(100 + index);
|
||||
const courseNumber = genCourseID(100 + index);
|
||||
const providerIndex = index % 3;
|
||||
const iteratedData = [
|
||||
{
|
||||
provider: providers.edx,
|
||||
course: { title, bannerUrl: logos.edx },
|
||||
relatedPrograms,
|
||||
},
|
||||
{
|
||||
provider: providers.mit,
|
||||
course: { title, bannerUrl: logos.science },
|
||||
relatedPrograms: [relatedPrograms[0]],
|
||||
},
|
||||
{
|
||||
provider: null,
|
||||
course: { title, bannerUrl: logos.social },
|
||||
relatedPrograms: [],
|
||||
},
|
||||
];
|
||||
return {
|
||||
...data,
|
||||
enrollment: genEnrollmentData(),
|
||||
grades: { isPassing: true },
|
||||
certificates: genCertificateData(),
|
||||
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
|
||||
...iteratedData[providerIndex],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export default {
|
||||
courseRunData,
|
||||
entitlementCourses,
|
||||
entitlementData,
|
||||
globalData,
|
||||
};
|
||||
|
||||
14
src/hooks.js
14
src/hooks.js
@@ -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,
|
||||
|
||||
@@ -18,19 +18,16 @@ jest.mock('react', () => ({
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
const PropTypes = jest.requireActual('prop-types');
|
||||
const { formatMessage } = jest.requireActual('./testUtils');
|
||||
const formatDate = jest.fn(date => date).mockName('useIntl.formatDate');
|
||||
return {
|
||||
...i18n,
|
||||
intlShape: PropTypes.shape({
|
||||
formatMessage: PropTypes.func,
|
||||
}),
|
||||
useIntl: () => ({
|
||||
formatMessage: (msg) => (
|
||||
<formatMessage
|
||||
msg={msg.defaultMessage}
|
||||
{...(msg.values && { values: msg.values })}
|
||||
/>
|
||||
),
|
||||
formatDate: jest.fn().mockName('useIntl.formatDate'),
|
||||
formatMessage,
|
||||
formatDate,
|
||||
}),
|
||||
IntlProvider: () => 'IntlProvider',
|
||||
defineMessages: m => m,
|
||||
@@ -89,6 +86,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Hyperlink: 'Hyperlink',
|
||||
Icon: 'Icon',
|
||||
IconButton: 'IconButton',
|
||||
MailtoLink: 'MailtoLink',
|
||||
ModalDialog: {
|
||||
Header: 'ModalDialog.Header',
|
||||
Body: 'ModalDialog.Body',
|
||||
@@ -149,23 +147,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'),
|
||||
}));
|
||||
|
||||
@@ -80,7 +80,8 @@ const mockApi = () => {
|
||||
resolveFns.init = {
|
||||
success: () => resolve({
|
||||
enrollments: fakeData.courseRunData,
|
||||
entitlements: fakeData.entitlementCourses,
|
||||
entitlements: fakeData.entitlementData,
|
||||
...fakeData.globalData,
|
||||
}),
|
||||
};
|
||||
}));
|
||||
@@ -147,11 +148,12 @@ describe('ESG app integration tests', () => {
|
||||
courseData.course.title,
|
||||
);
|
||||
cardDetails = inspector.get.card.details(card);
|
||||
|
||||
[
|
||||
courseData.provider.name,
|
||||
courseNumber,
|
||||
appMessages.withValues.CourseCard.accessExpires({
|
||||
accessExpirationDate: courseData.courseRun.accessExpirationDate,
|
||||
accessExpirationDate: courseData.enrollment.accessExpirationDate,
|
||||
}),
|
||||
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));
|
||||
|
||||
|
||||
@@ -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,23 +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> }
|
||||
*/
|
||||
export const testCardValues = (courseNumber, mapping) => {
|
||||
describe('cardData values', () => {
|
||||
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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user