chore: test and api updates
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { MailtoLink, Hyperlink } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -19,61 +19,76 @@ export const CertificateBanner = ({ courseNumber }) => {
|
||||
hasFinished,
|
||||
} = appHooks.useCardEnrollmentData(courseNumber);
|
||||
const { isPassing } = appHooks.useCardGradeData(courseNumber);
|
||||
const { minPassingGrade } = appHooks.useCardCourseRunData(courseNumber);
|
||||
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(courseNumber);
|
||||
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{formatMessage(messages.certRestricted)}
|
||||
<Hyperlink destination="info@example.com">info@example.com</Hyperlink>
|
||||
{isVerified && (
|
||||
<>
|
||||
If you would like a refund on your Certificate of Achievement, please contact our billing address <Hyperlink destination="billing@example.com">billing@example.com</Hyperlink>
|
||||
</>
|
||||
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
|
||||
{isVerified && ' '}
|
||||
{isVerified && formatMessage(
|
||||
messages.certRefundContactBilling,
|
||||
{ billingEmail: emailLink(billingEmail) },
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (!isPassing) {
|
||||
if (isAudit) {
|
||||
return (<Banner> Grade required to pass the course: {minPassingGrade}% </Banner>);
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.passingGrade, { minPassingGrade })}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
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: {minPassingGrade}%
|
||||
{formatMessage(messages.certMinGrade, { minPassingGrade })}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
if (certificate.previewUrl) {
|
||||
if (certificate.certPreviewUrl) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
Congratulations. Your certificate is ready.
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink destination={certificate.previewUrl}>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={certificate.downloadUrl}>Download Certificate.</Hyperlink>
|
||||
<Hyperlink destination={certificate.certDownloadUrl}>
|
||||
{formatMessage(messages.downloadCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
Your grade and certificate will be ready after {certificate.availableDate}.
|
||||
{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);
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,52 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ courseNumber }) => {
|
||||
const {
|
||||
canChange,
|
||||
isEntitlement,
|
||||
isExpired,
|
||||
hasSessions,
|
||||
isFulfilled,
|
||||
changeDeadline,
|
||||
showExpirationWarning,
|
||||
} = appHooks.useCardEntitlementsData(courseNumber);
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatDate, formatMessage } = useIntl();
|
||||
|
||||
if (!isEntitlement) {
|
||||
return null;
|
||||
}
|
||||
if (isExpired || isFulfilled) {
|
||||
return null;
|
||||
|
||||
if (!hasSessions && !isFulfilled) {
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
{formatMessage(messages.entitlementsUnavailable, {
|
||||
emailLink: supportEmail && <MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>,
|
||||
})}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return 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,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>
|
||||
`;
|
||||
@@ -29,7 +29,67 @@ export const messages = StrictDict({
|
||||
certRestricted: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRestricted',
|
||||
description: 'Restricted certificate warning message',
|
||||
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting ',
|
||||
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.',
|
||||
},
|
||||
certRefundContactBilling: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRefundContactBilling',
|
||||
description: 'Message to learners to contact billing for certificate refunds',
|
||||
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}',
|
||||
},
|
||||
passingGrade: {
|
||||
id: 'learner-dash.courseCard.banners.passingGrade',
|
||||
description: 'Message to learners with minimum passing grade for the course',
|
||||
defaultMessage: 'Grade required to pass the course: {minPassingGrade}',
|
||||
},
|
||||
notEligibleForCert: {
|
||||
id: 'learner-dash.courseCard.banners.notEligibleForCert',
|
||||
description: 'Certificate inelligibility message',
|
||||
defaultMessage: 'You are not eligible for a certificate.',
|
||||
},
|
||||
viewGrades: {
|
||||
id: 'learner-dash.courseCard.banners.viewGrades',
|
||||
description: 'Gradses link text',
|
||||
defaultMessage: 'View grades.',
|
||||
},
|
||||
certReady: {
|
||||
id: 'learner-dash.courseCard.banners.certReady',
|
||||
description: 'Certificate ready message',
|
||||
defaultMessage: 'Congratulations. Your certificate is ready.',
|
||||
},
|
||||
viewCertificate: {
|
||||
id: 'learner-dash.courseCard.banners.viewCertificate',
|
||||
description: 'Certificate link text',
|
||||
defaultMessage: 'View Certificate.',
|
||||
},
|
||||
certMinGrade: {
|
||||
id: 'learner-dash.courseCard.banners.certMinGrade',
|
||||
description: 'Passing grade requirement message',
|
||||
defaultMessage: 'Grade required for a certificate: {minPassingGrade}',
|
||||
},
|
||||
downloadCertificate: {
|
||||
id: 'learner-dash.courseCard.banners.downloadCertificate',
|
||||
description: 'Certificate download link text',
|
||||
defaultMessage: 'Download Certificate.',
|
||||
},
|
||||
gradeAndCertReadyAfter: {
|
||||
id: 'learner-dash.courseCard.banners.gradseAndCertReadyAfter',
|
||||
description: 'Grade and certificate availability date message',
|
||||
defaultMessage: 'Your grade and certificate will be ready after {availableDate}.',
|
||||
},
|
||||
entitlementsUnavailable: {
|
||||
id: 'learner-dash.courseCard.banners.entitlementsUnavailable',
|
||||
description: 'Entitlements course message when no sessions are available',
|
||||
defaultMessage: 'There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.',
|
||||
},
|
||||
entitlementsExpiringSoon: {
|
||||
id: 'learner-dash.courseCard.banners.entitlementsExpiringSoon',
|
||||
description: 'Entitlements course message when the entitlement is expiring soon.',
|
||||
defaultMessage: 'You must {selectSessionButton} by {changeDeadline} to access the course.',
|
||||
},
|
||||
selectSession: {
|
||||
id: 'learner-dash.courseCard.banners.selectSession',
|
||||
description: 'Entitlements session selection link text',
|
||||
defaultMessage: 'select a session',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('CourseCard hooks', () => {
|
||||
|
||||
describe('if verified and ended', () => {
|
||||
it('returns course ended message with course end date', () => {
|
||||
runHook({ courseRun: { isFinished: true } });
|
||||
runHook({ courseRun: { isArchived: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.courseEnded,
|
||||
{ endDate: formatDate(courseRunData.endDate) },
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import selectors from './selectors';
|
||||
|
||||
const { courseCard } = selectors;
|
||||
|
||||
export const useEmailConfirmationData = () => useSelector(selectors.emailConfirmation);
|
||||
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboards);
|
||||
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const useCourseCardData = (selector) => (courseNumber) => useSelector(
|
||||
(state) => selector(selectors.courseData(state)[courseNumber]),
|
||||
);
|
||||
|
||||
export const useCardCertificateData = useCourseCardData(courseCard.certificates);
|
||||
export const useCardCourseData = useCourseCardData(courseCard.course);
|
||||
export const useCardCourseRunData = useCourseCardData(courseCard.courseRun);
|
||||
export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment);
|
||||
export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements);
|
||||
export const useCardGradesData = useCourseCardData(courseCard.grades);
|
||||
export const useCardProviderData = useCourseCardData(courseCard.provider);
|
||||
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);
|
||||
@@ -9,6 +9,7 @@ const initialState = {
|
||||
enterpriseDashboards: {},
|
||||
platformSettings: {},
|
||||
suggestedCourses: {},
|
||||
filterState: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -16,18 +17,23 @@ const app = createSlice({
|
||||
name: 'app',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadEnrollments: (state, { payload }) => ({
|
||||
loadCourses: (state, { payload: { enrollments, entitlements } }) => ({
|
||||
...state,
|
||||
enrollments: payload.map(curr => curr.courseRun.courseNumber),
|
||||
courseData: payload.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr.courseRun.courseNumber]: curr,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
enrollments: [
|
||||
...enrollments.map(curr => curr.courseRun.courseNumber),
|
||||
...entitlements.map(curr => curr.courseRun.courseNumber),
|
||||
],
|
||||
courseData: {
|
||||
...entitlements.reduce(
|
||||
(obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
|
||||
{},
|
||||
),
|
||||
...enrollments.reduce(
|
||||
(obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
|
||||
{},
|
||||
),
|
||||
},
|
||||
}),
|
||||
loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }),
|
||||
loadGlobalData: (state, { payload }) => ({
|
||||
...state,
|
||||
emailConfirmation: payload.emailConfirmation,
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,11 +26,15 @@ const mkCardSelector = (sel) => (state, courseNumber) => (
|
||||
sel(courseCardData(state, courseNumber))
|
||||
);
|
||||
|
||||
const dateSixMonthsFromNow = new Date();
|
||||
dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180);
|
||||
|
||||
export const courseCard = StrictDict({
|
||||
certificates: mkCardSelector(({ certificates }) => ({
|
||||
availableDate: certificates.availableDate,
|
||||
downloadUrl: certificates.downloadUrls?.download,
|
||||
previewUrl: certificates.downloadUrls?.preview,
|
||||
certDownloadUrl: certificates.certDownloadUrl,
|
||||
honorCertDownloadUrl: certificates.honorCertDownloadUrl,
|
||||
certPreviewUrl: certificates.certPreviewUrl,
|
||||
isDownloadable: certificates.isDownloadable,
|
||||
isEarnedButUnavailable: certificates.isEarned && !certificates.isAvailable,
|
||||
isRestricted: certificates.isRestricted,
|
||||
@@ -58,13 +62,20 @@ export const courseCard = StrictDict({
|
||||
lastEnrolled: enrollment.lastEnrollment,
|
||||
isEnrolled: enrollment.isEnrolled,
|
||||
})),
|
||||
entitlements: mkCardSelector(({ entitlements }) => ({
|
||||
canChange: entitlements.canChange,
|
||||
entitlementSessions: entitlements.availableSessions,
|
||||
isEntitlement: entitlements.isEntitlement,
|
||||
isExpired: entitlements.isExpired,
|
||||
isFulfilled: entitlements.isFulfilled,
|
||||
})),
|
||||
entitlements: mkCardSelector(({ entitlements }) => {
|
||||
const deadline = new Date(entitlements.changeDeadline);
|
||||
const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow;
|
||||
return {
|
||||
canChange: entitlements.canChange,
|
||||
entitlementSessions: entitlements.availableSessions,
|
||||
isEntitlement: entitlements.isEntitlement,
|
||||
isExpired: entitlements.isExpired,
|
||||
isFulfilled: entitlements.isFulfilled,
|
||||
hasSessions: entitlements.availableSessions?.length > 0,
|
||||
changeDeadline: entitlements.changeDeadline,
|
||||
showExpirationWarning,
|
||||
};
|
||||
}),
|
||||
grades: mkCardSelector(({ grades }) => ({ isPassing: grades.isPassing })),
|
||||
provider: mkCardSelector(({ provider }) => ({ name: provider?.name })),
|
||||
relatedPrograms: mkCardSelector(({ relatedPrograms }) => ({
|
||||
|
||||
@@ -15,8 +15,7 @@ import requests from './requests';
|
||||
export const initialize = () => (dispatch) => (
|
||||
dispatch(requests.initializeList({
|
||||
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
|
||||
dispatch(actions.app.loadEnrollments(enrollments));
|
||||
dispatch(actions.app.loadEntitlements(entitlements));
|
||||
dispatch(actions.app.loadCourses({ enrollments, entitlements }));
|
||||
dispatch(actions.app.loadGlobalData(globalData));
|
||||
}),
|
||||
}))
|
||||
@@ -25,8 +24,7 @@ export const initialize = () => (dispatch) => (
|
||||
export const refreshList = () => (dispatch) => (
|
||||
dispatch(requests.initializeList({
|
||||
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
|
||||
dispatch(actions.app.loadEnrollments(enrollments));
|
||||
dispatch(actions.app.loadEntitlements(entitlements));
|
||||
dispatch(actions.app.loadCourses({ enrollments, entitlements }));
|
||||
dispatch(actions.app.loadGlobalData(globalData));
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
*********************************************************************************/
|
||||
const initializeList = () => Promise.resolve({
|
||||
enrollments: fakeData.courseRunData,
|
||||
entitlements: fakeData.entitlementCourses,
|
||||
entitlements: fakeData.entitlementData,
|
||||
...fakeData.globalData,
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ const logos = {
|
||||
|
||||
const pastDate = '11/11/2000';
|
||||
const futureDate = '11/11/3030';
|
||||
const soonDate = new Date();
|
||||
soonDate.setDate(soonDate.getDate() + 60);
|
||||
const soonDateStr = soonDate.toDateString();
|
||||
|
||||
const globalData = {
|
||||
emailConfirmation: {
|
||||
@@ -100,7 +103,9 @@ export const genCertificateData = (data = {}) => ({
|
||||
isAvailable: false,
|
||||
isEarned: false,
|
||||
isDownloadable: false,
|
||||
downloadUrls: null, // { preview, download }
|
||||
certPreviewUrl: 'edx.com/courses/my-course-url/cert-preview',
|
||||
certDownloadUrl: 'edx.com/courses/my-course-url/cert-download',
|
||||
honorCertDownloadUrl: 'edx.com/courses/my-course-url/honor-cert-download',
|
||||
...data,
|
||||
});
|
||||
|
||||
@@ -224,10 +229,8 @@ export const courseRuns = [
|
||||
isAvailable: true,
|
||||
isDownloadable: true,
|
||||
availableDate: pastDate,
|
||||
downloadUrls: {
|
||||
preview: logos.edx,
|
||||
download: logos.social,
|
||||
},
|
||||
certDownloadUrl: logos.social,
|
||||
certPreviewUrl: logos.edx,
|
||||
}),
|
||||
entitlements: { isEntitlement: false },
|
||||
},
|
||||
@@ -241,9 +244,7 @@ export const courseRuns = [
|
||||
isAvailable: true,
|
||||
isDownloadable: true,
|
||||
availableDate: pastDate,
|
||||
downloadUrls: {
|
||||
download: logos.social,
|
||||
},
|
||||
certDownloadUrl: logos.social,
|
||||
}),
|
||||
entitlements: { isEntitlement: false },
|
||||
},
|
||||
@@ -319,7 +320,6 @@ export const courseRuns = [
|
||||
|
||||
export const entitlementCourses = [
|
||||
{
|
||||
course: { title: genCourseTitle(100) },
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
@@ -331,7 +331,17 @@ export const entitlementCourses = [
|
||||
isExpired: false,
|
||||
},
|
||||
}, {
|
||||
course: { title: genCourseTitle(101) },
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
isRefundable: true,
|
||||
isFulfilled: false,
|
||||
canViewCourse: false,
|
||||
changeDeadline: soonDateStr,
|
||||
canChange: true,
|
||||
isExpired: false,
|
||||
},
|
||||
}, {
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
@@ -343,10 +353,9 @@ export const entitlementCourses = [
|
||||
isExpired: false,
|
||||
},
|
||||
}, {
|
||||
course: { title: genCourseTitle(102) },
|
||||
entitlements: {
|
||||
isEntitlement: true,
|
||||
availableSessions,
|
||||
availableSessions: [],
|
||||
isRefundable: true,
|
||||
isFulfilled: false,
|
||||
canViewCourse: false,
|
||||
@@ -387,13 +396,45 @@ export const courseRunData = courseRuns.map(
|
||||
...data,
|
||||
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
|
||||
...iteratedData[providerIndex],
|
||||
credit: { isPurchased: false, requestStatus: null },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const entitlementData = entitlementCourses.map(
|
||||
(data, index) => {
|
||||
const title = genCourseTitle(100 + index);
|
||||
const courseNumber = genCourseID(100 + index);
|
||||
const providerIndex = index % 3;
|
||||
const iteratedData = [
|
||||
{
|
||||
provider: providers.edx,
|
||||
course: { title, bannerUrl: logos.edx },
|
||||
relatedPrograms,
|
||||
},
|
||||
{
|
||||
provider: providers.mit,
|
||||
course: { title, bannerUrl: logos.science },
|
||||
relatedPrograms: [relatedPrograms[0]],
|
||||
},
|
||||
{
|
||||
provider: null,
|
||||
course: { title, bannerUrl: logos.social },
|
||||
relatedPrograms: [],
|
||||
},
|
||||
];
|
||||
return {
|
||||
...data,
|
||||
enrollment: genEnrollmentData(),
|
||||
grades: { isPassing: true },
|
||||
certificates: genCertificateData(),
|
||||
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
|
||||
...iteratedData[providerIndex],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export default {
|
||||
courseRunData,
|
||||
entitlementCourses,
|
||||
entitlementData,
|
||||
globalData,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
const PropTypes = jest.requireActual('prop-types');
|
||||
const { formatMessage } = jest.requireActual('./testUtils');
|
||||
const formatDate = jest.fn().mockName('useIntl.formatDate');
|
||||
const formatDate = jest.fn(date => date).mockName('useIntl.formatDate');
|
||||
return {
|
||||
...i18n,
|
||||
intlShape: PropTypes.shape({
|
||||
@@ -86,6 +86,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Hyperlink: 'Hyperlink',
|
||||
Icon: 'Icon',
|
||||
IconButton: 'IconButton',
|
||||
MailtoLink: 'MailtoLink',
|
||||
ModalDialog: {
|
||||
Header: 'ModalDialog.Header',
|
||||
Body: 'ModalDialog.Body',
|
||||
|
||||
@@ -80,7 +80,8 @@ const mockApi = () => {
|
||||
resolveFns.init = {
|
||||
success: () => resolve({
|
||||
enrollments: fakeData.courseRunData,
|
||||
entitlements: fakeData.entitlementCourses,
|
||||
entitlements: fakeData.entitlementData,
|
||||
...fakeData.globalData,
|
||||
}),
|
||||
};
|
||||
}));
|
||||
@@ -148,7 +149,6 @@ describe('ESG app integration tests', () => {
|
||||
);
|
||||
cardDetails = inspector.get.card.details(card);
|
||||
|
||||
console.log({ enrollment: courseData.enrollment });
|
||||
[
|
||||
courseData.provider.name,
|
||||
courseNumber,
|
||||
|
||||
Reference in New Issue
Block a user