chore: update course entitlement
chore: update requested change chore: move show select session dialog to redux app level
This commit is contained in:
committed by
leangseu-edx
parent
c8c4f8019c
commit
7f7625333d
25
package-lock.json
generated
25
package-lock.json
generated
@@ -20,7 +20,6 @@
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@zip.js/zip.js": "^2.4.6",
|
||||
"axios": "^0.21.4",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.16.2",
|
||||
@@ -34,6 +33,7 @@
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
@@ -7544,11 +7544,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.15.tgz",
|
||||
"integrity": "sha512-fAZkoF0qG8MCijvx4xCyVISAEwLWo8L/JCe5Mrl1zhHpZv+RK6hodIMnKoyZpT5MLGYgr7vJh/y5/1cF7WBUlw=="
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -21920,6 +21915,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moo": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
|
||||
@@ -39424,11 +39427,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"@zip.js/zip.js": {
|
||||
"version": "2.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.15.tgz",
|
||||
"integrity": "sha512-fAZkoF0qG8MCijvx4xCyVISAEwLWo8L/JCe5Mrl1zhHpZv+RK6hodIMnKoyZpT5MLGYgr7vJh/y5/1cF7WBUlw=="
|
||||
},
|
||||
"abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
@@ -50452,6 +50450,11 @@
|
||||
"integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==",
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"moo": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@zip.js/zip.js": "^2.4.6",
|
||||
"axios": "^0.21.4",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.16.2",
|
||||
@@ -51,6 +50,7 @@
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
|
||||
@@ -36,14 +36,9 @@ exports[`CourseCard component snapshot 1`] = `
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
<span
|
||||
data-testid="CourseCardDetails"
|
||||
>
|
||||
hooks.providerName
|
||||
•
|
||||
test-course-number
|
||||
•
|
||||
</span>
|
||||
<CourseCardDetails
|
||||
courseNumber="test-course-number"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
|
||||
@@ -4,8 +4,10 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { dateFormatter } from 'utils';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import useSelectSession from 'containers/SelectSession/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ courseNumber }) => {
|
||||
@@ -15,8 +17,10 @@ export const EntitlementBanner = ({ courseNumber }) => {
|
||||
isFulfilled,
|
||||
changeDeadline,
|
||||
showExpirationWarning,
|
||||
isExpired,
|
||||
} = appHooks.useCardEntitlementsData(courseNumber);
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
const { openSessionModal } = useSelectSession({ courseNumber });
|
||||
const { formatDate, formatMessage } = useIntl();
|
||||
|
||||
if (!isEntitlement) {
|
||||
@@ -36,9 +40,9 @@ export const EntitlementBanner = ({ courseNumber }) => {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.entitlementsExpiringSoon, {
|
||||
changeDeadline: formatDate(changeDeadline),
|
||||
changeDeadline: dateFormatter(formatDate, changeDeadline),
|
||||
selectSessionButton: (
|
||||
<Button variant="link" size="inline" className="m-0 p-0">
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
||||
{formatMessage(messages.selectSession)}
|
||||
</Button>
|
||||
),
|
||||
@@ -46,6 +50,13 @@ export const EntitlementBanner = ({ courseNumber }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (isExpired) {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.entitlementsExpired)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
EntitlementBanner.propTypes = {
|
||||
|
||||
@@ -11,6 +11,9 @@ jest.mock('data/redux', () => ({
|
||||
useCardEntitlementsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('containers/SelectSession/hooks', () => () => ({
|
||||
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
@@ -20,7 +23,7 @@ const entitlementsData = {
|
||||
isEntitlement: true,
|
||||
hasSessions: true,
|
||||
isFulfilled: false,
|
||||
changeDeadline: 'test-deadline',
|
||||
changeDeadline: '11/11/2022',
|
||||
showExpirationWarning: false,
|
||||
};
|
||||
const platformData = { supportEmail: 'test-support-email' };
|
||||
|
||||
@@ -12,9 +12,10 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"changeDeadline": "test-deadline",
|
||||
"changeDeadline": "11/11/2022",
|
||||
"selectSessionButton": <Button
|
||||
className="m-0 p-0"
|
||||
onClick={[MockFunction useSelectSession.openSessionModal]}
|
||||
size="inline"
|
||||
variant="link"
|
||||
>
|
||||
|
||||
@@ -86,6 +86,11 @@ export const messages = StrictDict({
|
||||
description: 'Entitlements course message when the entitlement is expiring soon.',
|
||||
defaultMessage: 'You must {selectSessionButton} by {changeDeadline} to access the course.',
|
||||
},
|
||||
entitlementsExpired: {
|
||||
id: 'learner-dash.courseCard.banners.entitlementsExpired',
|
||||
description: 'Entitlements course message when the entitlement is expired.',
|
||||
defaultMessage: 'You can no longer change sessions.',
|
||||
},
|
||||
selectSession: {
|
||||
id: 'learner-dash.courseCard.banners.selectSession',
|
||||
description: 'Entitlements session selection link text',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Locked } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import useSelectSession from 'containers/SelectSession/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useCardActionData = ({ courseNumber }) => {
|
||||
@@ -13,26 +14,50 @@ export const useCardActionData = ({ courseNumber }) => {
|
||||
isVerified,
|
||||
} = appHooks.useCardEnrollmentData(courseNumber);
|
||||
const { isPending, isArchived } = appHooks.useCardCourseRunData(courseNumber);
|
||||
const {
|
||||
isEntitlement,
|
||||
canViewCourse,
|
||||
isFulfilled,
|
||||
isExpired,
|
||||
canChange,
|
||||
hasSessions,
|
||||
} = appHooks.useCardEntitlementsData(courseNumber);
|
||||
const { openSessionModal } = useSelectSession({ courseNumber });
|
||||
|
||||
let primary;
|
||||
let secondary = null;
|
||||
if (!isVerified) {
|
||||
secondary = {
|
||||
iconBefore: Locked,
|
||||
variant: 'outline-primary',
|
||||
disabled: !canUpgrade,
|
||||
children: formatMessage(messages.upgrade),
|
||||
};
|
||||
}
|
||||
if (isPending) {
|
||||
primary = { children: formatMessage(messages.beginCourse) };
|
||||
} else if (!isArchived) {
|
||||
primary = {
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: isAudit && isAuditAccessExpired,
|
||||
};
|
||||
if (isEntitlement) {
|
||||
if (!isFulfilled) {
|
||||
primary = {
|
||||
children: formatMessage(messages.selectSession),
|
||||
disabled: !(canChange && hasSessions),
|
||||
onClick: openSessionModal,
|
||||
};
|
||||
} else {
|
||||
primary = {
|
||||
children: formatMessage(messages.viewCourse),
|
||||
disabled: !canViewCourse || isExpired,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
primary = { children: formatMessage(messages.viewCourse) };
|
||||
if (!isVerified) {
|
||||
secondary = {
|
||||
iconBefore: Locked,
|
||||
variant: 'outline-primary',
|
||||
disabled: !canUpgrade,
|
||||
children: formatMessage(messages.upgrade),
|
||||
};
|
||||
}
|
||||
if (isPending) {
|
||||
primary = { children: formatMessage(messages.beginCourse) };
|
||||
} else if (!isArchived) {
|
||||
primary = {
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: isAudit && isAuditAccessExpired,
|
||||
};
|
||||
} else {
|
||||
primary = { children: formatMessage(messages.viewCourse) };
|
||||
}
|
||||
}
|
||||
return { primary, secondary };
|
||||
};
|
||||
|
||||
@@ -11,9 +11,14 @@ jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('containers/SelectSession/hooks', () => () => ({
|
||||
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
const enrollmentData = {
|
||||
@@ -26,68 +31,133 @@ const courseRunData = {
|
||||
isPending: false,
|
||||
isArchived: false,
|
||||
};
|
||||
const entitlementData = {
|
||||
isEntitlement: false,
|
||||
canViewCourse: false,
|
||||
isFulfilled: false,
|
||||
isExpired: false,
|
||||
canChange: false,
|
||||
hasSessions: false,
|
||||
};
|
||||
|
||||
describe('CourseCardActions hooks', () => {
|
||||
let out;
|
||||
const { formatMessage } = useIntl();
|
||||
const runHook = (overrides = {}) => {
|
||||
const { enrollment = {}, courseRun = {} } = overrides;
|
||||
const { enrollment = {}, courseRun = {}, entitlement = {} } = overrides;
|
||||
appHooks.useCardCourseRunData.mockReturnValueOnce({ ...courseRunData, ...courseRun });
|
||||
appHooks.useCardEnrollmentData.mockReturnValueOnce({ ...enrollmentData, ...enrollment });
|
||||
appHooks.useCardEntitlementsData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
|
||||
out = hooks.useCardActionData({ courseNumber });
|
||||
};
|
||||
describe('secondary action', () => {
|
||||
it('returns null if verified', () => {
|
||||
runHook({ enrollment: { isAudit: false, isVerified: true } });
|
||||
expect(out.secondary).toEqual(null);
|
||||
});
|
||||
it('returns disabled upgrade button if audit, but cannot upgrade', () => {
|
||||
runHook();
|
||||
expect(out.secondary).toEqual({
|
||||
iconBefore: Locked,
|
||||
variant: 'outline-primary',
|
||||
disabled: true,
|
||||
children: formatMessage(messages.upgrade),
|
||||
describe('entitlement', () => {
|
||||
describe('secondary action', () => {
|
||||
it('return null on entitlement course', () => {
|
||||
runHook({ entitlement: { isEntitlement: true } });
|
||||
expect(out.secondary).toEqual(null);
|
||||
});
|
||||
});
|
||||
it('returns enabled upgrade button if audit and can upgrade', () => {
|
||||
runHook({ enrollment: { canUpgrade: true } });
|
||||
expect(out.secondary).toEqual({
|
||||
iconBefore: Locked,
|
||||
variant: 'outline-primary',
|
||||
disabled: false,
|
||||
children: formatMessage(messages.upgrade),
|
||||
describe('primary action', () => {
|
||||
describe('unfulfilled entitlment', () => {
|
||||
it('has select session text', () => {
|
||||
runHook({ entitlement: { isEntitlement: true, isFulfilled: false } });
|
||||
expect(out.primary.children).toEqual(formatMessage(messages.selectSession));
|
||||
});
|
||||
it('disabled when it cannot change or does not have sessions', () => {
|
||||
runHook({
|
||||
entitlement: {
|
||||
isEntitlement: true,
|
||||
isFulfilled: false,
|
||||
canChange: false,
|
||||
hasSessions: true,
|
||||
},
|
||||
});
|
||||
expect(out.primary.disabled).toEqual(true);
|
||||
runHook({
|
||||
entitlement: {
|
||||
isEntitlement: true,
|
||||
isFulfilled: false,
|
||||
canChange: true,
|
||||
hasSessions: false,
|
||||
},
|
||||
});
|
||||
expect(out.primary.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fulfilled entitlment', () => {
|
||||
it('has View Course text', () => {
|
||||
runHook({ entitlement: { isEntitlement: true, isFulfilled: true } });
|
||||
expect(out.primary.children).toEqual(formatMessage(messages.viewCourse));
|
||||
});
|
||||
it('disabled when it is expired and cannot View Course', () => {
|
||||
runHook({
|
||||
entitlement: {
|
||||
isEntitlement: true,
|
||||
isFulfilled: true,
|
||||
isExpired: true,
|
||||
canViewCourse: false,
|
||||
},
|
||||
});
|
||||
expect(out.primary.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('primary action', () => {
|
||||
it('returns Begin Course button if pending', () => {
|
||||
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', () => {
|
||||
runHook({ enrollment: { isAuditAccessExpired: true } });
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: true,
|
||||
describe('enrollment', () => {
|
||||
describe('secondary action', () => {
|
||||
it('returns null if verified', () => {
|
||||
runHook({ enrollment: { isAudit: false, isVerified: true } });
|
||||
expect(out.secondary).toEqual(null);
|
||||
});
|
||||
it('returns disabled upgrade button if audit, but cannot upgrade', () => {
|
||||
runHook();
|
||||
expect(out.secondary).toEqual({
|
||||
iconBefore: Locked,
|
||||
variant: 'outline-primary',
|
||||
disabled: true,
|
||||
children: formatMessage(messages.upgrade),
|
||||
});
|
||||
});
|
||||
it('returns enabled upgrade button if audit and can upgrade', () => {
|
||||
runHook({ enrollment: { canUpgrade: true } });
|
||||
expect(out.secondary).toEqual({
|
||||
iconBefore: Locked,
|
||||
variant: 'outline-primary',
|
||||
disabled: false,
|
||||
children: formatMessage(messages.upgrade),
|
||||
});
|
||||
});
|
||||
});
|
||||
it('returns disabled Resume button if active and audit without expired access', () => {
|
||||
runHook();
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: false,
|
||||
describe('primary action', () => {
|
||||
it('returns Begin Course button if pending', () => {
|
||||
runHook({ courseRun: { isPending: true } });
|
||||
expect(out.primary).toEqual({ children: formatMessage(messages.beginCourse) });
|
||||
});
|
||||
runHook({ enrollment: { isAudit: false, isVerified: true } });
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: false,
|
||||
it('returns enabled Resume button if active, and not audit with expired access', () => {
|
||||
runHook({ enrollment: { isAuditAccessExpired: true } });
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
it('returns viewCourse button if archived', () => {
|
||||
runHook({ courseRun: { isArchived: true } });
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.viewCourse),
|
||||
it('returns disabled Resume button if active and audit without expired access', () => {
|
||||
runHook();
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: false,
|
||||
});
|
||||
runHook({ enrollment: { isAudit: false, isVerified: true } });
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.resume),
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
it('returns viewCourse button if archived', () => {
|
||||
runHook({ courseRun: { isArchived: true } });
|
||||
expect(out.primary).toEqual({
|
||||
children: formatMessage(messages.viewCourse),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,11 @@ export const messages = StrictDict({
|
||||
description: 'Course card view-course button text',
|
||||
defaultMessage: 'View Course',
|
||||
},
|
||||
selectSession: {
|
||||
id: 'learner-dash.courseCard.actions.selectSession',
|
||||
description: 'Course card select-session button text',
|
||||
defaultMessage: 'Select Session',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCard Details component does not have change session button on regular course 1`] = `
|
||||
<span
|
||||
data-testid="CourseCardDetails"
|
||||
>
|
||||
provider-name
|
||||
•
|
||||
test-course-number
|
||||
•
|
||||
acess-message
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`CourseCard Details component has change session button on entitlement course 1`] = `
|
||||
<span
|
||||
data-testid="CourseCardDetails"
|
||||
>
|
||||
provider-name
|
||||
•
|
||||
test-course-number
|
||||
•
|
||||
access-message
|
||||
•
|
||||
<Button
|
||||
className="m-0 p-0"
|
||||
onClick={[MockFunction useSelectSession.openSessionModal]}
|
||||
size="inline"
|
||||
variant="link"
|
||||
>
|
||||
<div
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Change or leave session",
|
||||
"description": "Button for trigger change or leave session for entitlement course",
|
||||
"id": "learner-dash.courseCard.CourseCardDetails.changeOrLeaveSessionButton",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import useSelectSession from 'containers/SelectSession/hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useAccessMessage = ({ courseNumber }) => {
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const {
|
||||
accessExpirationDate,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
} = appHooks.useCardEnrollmentData(courseNumber);
|
||||
const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber);
|
||||
|
||||
if (isAudit) {
|
||||
return formatMessage(
|
||||
isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
|
||||
{ accessExpirationDate: formatDate(accessExpirationDate) },
|
||||
);
|
||||
}
|
||||
|
||||
return formatMessage(
|
||||
isArchived ? messages.courseEnded : messages.courseEnds,
|
||||
{ endDate: formatDate(endDate) },
|
||||
);
|
||||
};
|
||||
|
||||
export const useCardDetailsData = ({ courseNumber }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const providerName = appHooks.useCardProviderData(courseNumber).name;
|
||||
const {
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
} = appHooks.useCardEntitlementsData(courseNumber);
|
||||
|
||||
const { openSessionModalWithLeaveOption: openSessionModal } = useSelectSession({ courseNumber });
|
||||
|
||||
return {
|
||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||
accessMessage: module.useAccessMessage({ courseNumber }),
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
openSessionModal,
|
||||
formatMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCardDetailsData;
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { keyStore, dateFormatter } from 'utils';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementsData: jest.fn(),
|
||||
useCardProviderData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('containers/SelectSession/hooks', () => () => ({
|
||||
openSessionModalWithLeaveOption: jest.fn().mockName('useSelectSession.openSessionModalWithLeaveOptionFunction'),
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
const useAccessMessage = 'test-access-message';
|
||||
const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage });
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
describe('CourseCard hooks', () => {
|
||||
let out;
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useCardDetailsData', () => {
|
||||
const providerData = {
|
||||
name: 'my-provider-name',
|
||||
};
|
||||
const entitlementData = {
|
||||
isEntitlement: false,
|
||||
canViewCourse: false,
|
||||
isFulfilled: false,
|
||||
isExpired: false,
|
||||
canChange: false,
|
||||
hasSessions: false,
|
||||
};
|
||||
const runHook = ({ provider = {}, entitlement = {} }) => {
|
||||
jest.spyOn(hooks, hookKeys.useAccessMessage)
|
||||
.mockImplementationOnce(mockAccessMessage);
|
||||
appHooks.useCardProviderData.mockReturnValueOnce({
|
||||
...providerData,
|
||||
...provider,
|
||||
});
|
||||
appHooks.useCardEntitlementsData.mockReturnValueOnce({
|
||||
...entitlementData,
|
||||
...entitlement,
|
||||
});
|
||||
out = hooks.useCardDetailsData({ courseNumber });
|
||||
};
|
||||
beforeEach(() => {
|
||||
runHook({});
|
||||
});
|
||||
it('forwards formatMessage from useIntl', () => {
|
||||
expect(out.formatMessage).toEqual(formatMessage);
|
||||
});
|
||||
it('forwards useAccessMessage output, called with courseNumber', () => {
|
||||
expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber }));
|
||||
});
|
||||
it('forwards provider name if it exists, else formatted unknown provider name', () => {
|
||||
expect(out.providerName).toEqual(providerData.name);
|
||||
runHook({ provider: { name: '' } });
|
||||
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
|
||||
});
|
||||
});
|
||||
describe('useAccessMessage', () => {
|
||||
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,
|
||||
});
|
||||
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', () => {
|
||||
runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.accessExpired,
|
||||
{ accessExpirationDate: dateFormatter(formatDate, enrollmentData.accessExpirationDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('if audit and not expired', () => {
|
||||
it('returns accessExpires message with accessExpirationDate from cardData', () => {
|
||||
runHook({ enrollment: { isAudit: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.accessExpires,
|
||||
{ accessExpirationDate: dateFormatter(formatDate, enrollmentData.accessExpirationDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('if verified and not ended', () => {
|
||||
it('returns course ends message with course end date', () => {
|
||||
runHook({});
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.courseEnds,
|
||||
{ endDate: dateFormatter(formatDate, courseRunData.endDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('if verified and ended', () => {
|
||||
it('returns course ended message with course end date', () => {
|
||||
runHook({ courseRun: { isArchived: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.courseEnded,
|
||||
{ endDate: dateFormatter(formatDate, courseRunData.endDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import useCardDetailsData from './hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const CourseCardDetails = ({ courseNumber }) => {
|
||||
const {
|
||||
providerName,
|
||||
accessMessage,
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
openSessionModal,
|
||||
formatMessage,
|
||||
} = useCardDetailsData({ courseNumber });
|
||||
|
||||
return (
|
||||
<span data-testid="CourseCardDetails">
|
||||
{providerName} • {courseNumber} • {accessMessage}
|
||||
{isEntitlement && isFulfilled && canChange ? (
|
||||
<>
|
||||
{' • '}
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
||||
{formatMessage(messages.changeOrLeaveSessionButton)}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CourseCardDetails.propTypes = {
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseCardDetails.defaultProps = {};
|
||||
|
||||
export default CourseCardDetails;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import CourseCardDetails from '.';
|
||||
|
||||
import hooks from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const courseNumber = 'test-course-number';
|
||||
|
||||
describe('CourseCard Details component', () => {
|
||||
it('has change session button on entitlement course', () => {
|
||||
const mockHook = (args) => () => ({
|
||||
providerName: 'provider-name',
|
||||
accessMessage: 'access-message',
|
||||
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
|
||||
formatMessage: (message, values) => <div {...{ message, values }} />,
|
||||
isEntitlement: true,
|
||||
isFulfilled: true,
|
||||
canChange: true,
|
||||
...args,
|
||||
});
|
||||
hooks.mockImplementationOnce(mockHook({ isEntitlement: true }));
|
||||
const el = shallow(<CourseCardDetails courseNumber={courseNumber} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
// it has 3 separator, 4 column
|
||||
expect(el.text().match(/•/g)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('does not have change session button on regular course', () => {
|
||||
const mockHook = (args) => () => ({
|
||||
providerName: 'provider-name',
|
||||
accessMessage: 'acess-message',
|
||||
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
|
||||
formatMessage: (message, values) => <div {...{ message, values }} />,
|
||||
isEntitlement: true,
|
||||
isFulfilled: true,
|
||||
canChange: true,
|
||||
...args,
|
||||
});
|
||||
hooks.mockImplementationOnce(mockHook({ isEntitlement: false }));
|
||||
const el = shallow(<CourseCardDetails courseNumber={courseNumber} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
// it has 2 separator, 3 column
|
||||
expect(el.text().match(/•/g)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
accessExpired: {
|
||||
id: 'learner-dash.courseCard.CourseCardDetails.accessExpired',
|
||||
description: 'Course access expiration date message on course card for expired access.',
|
||||
defaultMessage: 'Access expired {accessExpirationDate}',
|
||||
},
|
||||
accessExpires: {
|
||||
id: 'learner-dash.courseCard.CourseCardDetails.accessExpires',
|
||||
description: 'Course access expiration date message on course card.',
|
||||
defaultMessage: 'Access expires {accessExpirationDate}',
|
||||
},
|
||||
courseEnded: {
|
||||
id: 'learner-dash.courseCard.CourseCardDetails.courseEnded',
|
||||
description: 'Course ended message on course card.',
|
||||
defaultMessage: 'Course ended {endDate}',
|
||||
},
|
||||
courseEnds: {
|
||||
id: 'learner-dash.courseCard.CourseCardDetails.courseEnds',
|
||||
description: 'Course ending message on course card.',
|
||||
defaultMessage: 'Course ends {endDate}',
|
||||
},
|
||||
unknownProviderName: {
|
||||
id: 'learner-dash.courseCard.CourseCardDetails.unknownProviderName',
|
||||
description: 'Provider name display when name is unknown',
|
||||
defaultMessage: 'Unknown',
|
||||
},
|
||||
changeOrLeaveSessionButton: {
|
||||
id: 'learner-dash.courseCard.CourseCardDetails.changeOrLeaveSessionButton',
|
||||
description: 'Button for trigger change or leave session for entitlement course',
|
||||
defaultMessage: 'Change or leave session',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,41 +1,13 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useAccessMessage = ({ courseNumber }) => {
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const {
|
||||
accessExpirationDate,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
} = appHooks.useCardEnrollmentData(courseNumber);
|
||||
const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber);
|
||||
|
||||
if (isAudit) {
|
||||
return formatMessage(
|
||||
isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
|
||||
{ accessExpirationDate: formatDate(accessExpirationDate) },
|
||||
);
|
||||
}
|
||||
|
||||
return formatMessage(
|
||||
isArchived ? messages.courseEnded : messages.courseEnds,
|
||||
{ endDate: formatDate(endDate) },
|
||||
);
|
||||
};
|
||||
|
||||
export const useCardData = ({ courseNumber }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { title, bannerUrl } = appHooks.useCardCourseData(courseNumber);
|
||||
const providerName = appHooks.useCardProviderData(courseNumber).name;
|
||||
|
||||
return {
|
||||
title,
|
||||
bannerUrl,
|
||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||
accessMessage: module.useAccessMessage({ courseNumber }),
|
||||
formatMessage,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
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';
|
||||
const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage });
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
describe('CourseCard hooks', () => {
|
||||
let out;
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const { formatMessage } = useIntl();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -32,20 +24,11 @@ describe('CourseCard hooks', () => {
|
||||
title: 'fake-title',
|
||||
bannerUrl: 'my-banner-url',
|
||||
};
|
||||
const providerData = {
|
||||
name: 'my-provider-name',
|
||||
};
|
||||
const runHook = ({ course = {}, provider = {} }) => {
|
||||
jest.spyOn(hooks, hookKeys.useAccessMessage)
|
||||
.mockImplementationOnce(mockAccessMessage);
|
||||
const runHook = ({ course = {} }) => {
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...courseData,
|
||||
...course,
|
||||
});
|
||||
appHooks.useCardProviderData.mockReturnValueOnce({
|
||||
...providerData,
|
||||
...provider,
|
||||
});
|
||||
out = hooks.useCardData({ courseNumber });
|
||||
};
|
||||
beforeEach(() => {
|
||||
@@ -59,80 +42,5 @@ describe('CourseCard hooks', () => {
|
||||
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 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,
|
||||
});
|
||||
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', () => {
|
||||
runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.accessExpired,
|
||||
{ accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('if audit and not expired', () => {
|
||||
it('returns accessExpires message with accessExpirationDate from cardData', () => {
|
||||
runHook({ enrollment: { isAudit: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.accessExpires,
|
||||
{ accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('if verified and not ended', () => {
|
||||
it('returns course ends message with course end date', () => {
|
||||
runHook({});
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.courseEnds,
|
||||
{ endDate: formatDate(courseRunData.endDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('if verified and ended', () => {
|
||||
it('returns course ended message with course end date', () => {
|
||||
runHook({ courseRun: { isArchived: true } });
|
||||
expect(out).toEqual(formatMessage(
|
||||
messages.courseEnded,
|
||||
{ endDate: formatDate(courseRunData.endDate) },
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,13 +15,12 @@ import {
|
||||
} from './components/Banners';
|
||||
import CourseCardActions from './components/CourseCardActions';
|
||||
import messages from './messages';
|
||||
import CourseCardDetails from './components/CourseCardDetails';
|
||||
|
||||
export const CourseCard = ({ courseNumber }) => {
|
||||
const {
|
||||
title,
|
||||
bannerUrl,
|
||||
providerName,
|
||||
accessMessage,
|
||||
formatMessage,
|
||||
} = useCardData({ courseNumber });
|
||||
return (
|
||||
@@ -37,9 +36,7 @@ export const CourseCard = ({ courseNumber }) => {
|
||||
actions={<CourseCardMenu courseNumber={courseNumber} />}
|
||||
/>
|
||||
<Card.Section>
|
||||
<span data-testid="CourseCardDetails">
|
||||
{providerName} • {courseNumber} • {accessMessage}
|
||||
</span>
|
||||
<CourseCardDetails courseNumber={courseNumber} />
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
|
||||
@@ -17,12 +17,11 @@ jest.mock('./components/Banners', () => ({
|
||||
EntitlementBanner: () => 'EntitlementBanner',
|
||||
}));
|
||||
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
|
||||
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
|
||||
|
||||
const dataProps = {
|
||||
title: 'hooks.title',
|
||||
bannerUrl: 'hooks.bannerUrl',
|
||||
providerName: 'hooks.providerName',
|
||||
accessMessagE: 'hooks.accessMessage',
|
||||
formatMessage: jest.fn(msg => ({ formatted: msg })),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,36 +1,11 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
accessExpired: {
|
||||
id: 'learner-dash.courseCard.accessExpired',
|
||||
description: 'Course access expiration date message on course card for expired access.',
|
||||
defaultMessage: 'Access expired {accessExpirationDate}',
|
||||
},
|
||||
accessExpires: {
|
||||
id: 'learner-dash.courseCard.accessExpires',
|
||||
description: 'Course access expiration date message on course card.',
|
||||
defaultMessage: 'Access expires {accessExpirationDate}',
|
||||
},
|
||||
courseEnded: {
|
||||
id: 'learner-dash.courseCard.courseEnded',
|
||||
description: 'Course ended message on course card.',
|
||||
defaultMessage: 'Course ended {endDate}',
|
||||
},
|
||||
courseEnds: {
|
||||
id: 'learner-dash.courseCard.courseEnds',
|
||||
description: 'Course ending message on course card.',
|
||||
defaultMessage: 'Course ends {endDate}',
|
||||
},
|
||||
bannerAlt: {
|
||||
id: 'learner-dash.courseCard.bannerAlt',
|
||||
description: 'Course card banner alt-text',
|
||||
defaultMessage: 'Course thumbnail',
|
||||
},
|
||||
unknownProviderName: {
|
||||
id: 'learner-dash.courseCard.unknownProviderName',
|
||||
description: 'Provider name display when name is unknown',
|
||||
defaultMessage: 'Unknown',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -2,12 +2,14 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import CourseCard from 'containers/CourseCard';
|
||||
import SelectSession from 'containers/SelectSession';
|
||||
|
||||
export const CourseList = ({ courseListData }) => (
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
{courseListData.map((courseNumber) => (
|
||||
<CourseCard key={courseNumber} courseNumber={courseNumber} />
|
||||
))}
|
||||
<SelectSession />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
82
src/containers/SelectSession/SelectSessionModal.jsx
Normal file
82
src/containers/SelectSession/SelectSessionModal.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, Form, ModalDialog,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { nullMethod } from 'hooks';
|
||||
import { dateFormatter } from 'utils';
|
||||
|
||||
import useSelectSession from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectSessionModal = ({ courseNumber }) => {
|
||||
const {
|
||||
entitlementSessions,
|
||||
showSessionModal,
|
||||
closeSessionModal,
|
||||
showLeaveSessionInSessionModal,
|
||||
courseTitle,
|
||||
} = useSelectSession({
|
||||
courseNumber,
|
||||
});
|
||||
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
|
||||
let header;
|
||||
let hint;
|
||||
if (showLeaveSessionInSessionModal) {
|
||||
header = formatMessage(messages.changeOrLeaveHeader);
|
||||
hint = formatMessage(messages.changeOrLeaveHint);
|
||||
} else {
|
||||
header = formatMessage(messages.selectSessionHeader, {
|
||||
courseTitle,
|
||||
});
|
||||
hint = formatMessage(messages.selectSessionHint);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
isOpen={showSessionModal}
|
||||
onClose={nullMethod}
|
||||
hasCloseButton={false}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
size="md"
|
||||
style={{ textAlign: 'start' }}
|
||||
>
|
||||
<h4>{header}</h4>
|
||||
<Form.Group>
|
||||
<Form.Label>{hint}</Form.Label>
|
||||
<Form.RadioSet name="sessions">
|
||||
{entitlementSessions?.map((entitle) => (
|
||||
<Form.Radio key={entitle.startDate} value={entitle.startDate}>
|
||||
{dateFormatter(formatDate, entitle.startDate)} - {dateFormatter(formatDate, entitle.endDate)}
|
||||
</Form.Radio>
|
||||
))}
|
||||
{showLeaveSessionInSessionModal ? (
|
||||
<Form.Radio value="leave">
|
||||
{formatMessage(messages.leaveSessionOption)}
|
||||
</Form.Radio>
|
||||
) : null}
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={closeSessionModal}>
|
||||
{formatMessage(messages.nevermind)}
|
||||
</Button>
|
||||
<Button>{formatMessage(messages.confirmSession)}</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
SelectSessionModal.propTypes = {
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SelectSessionModal;
|
||||
54
src/containers/SelectSession/SelectSessionModal.test.jsx
Normal file
54
src/containers/SelectSession/SelectSessionModal.test.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import hooks from './hooks';
|
||||
import SelectSessionModal from './SelectSessionModal';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const hookReturn = {
|
||||
entitlementSessions: [],
|
||||
showSessionModal: true,
|
||||
closeSessionModal: jest.fn().mockName('useSelectSession.closeSessionModal'),
|
||||
showLeaveSessionInSessionModal: true,
|
||||
courseTitle: 'course-title: unit test save life',
|
||||
};
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
const availableSessions = [
|
||||
{ startDate: '1/2/2000', endDate: '1/2/2020', courseNumber },
|
||||
{ startDate: '2/3/2000', endDate: '2/3/2020', courseNumber },
|
||||
{ startDate: '3/4/2000', endDate: '3/4/2020', courseNumber },
|
||||
];
|
||||
|
||||
describe('SelectSessionModal', () => {
|
||||
describe('snapshot', () => {
|
||||
test('empty modal with leave option ', () => {
|
||||
hooks.mockReturnValueOnce({
|
||||
...hookReturn,
|
||||
});
|
||||
expect(shallow(<SelectSessionModal courseNumber={courseNumber} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('modal with leave option ', () => {
|
||||
hooks.mockReturnValueOnce({
|
||||
...hookReturn,
|
||||
entitlementSessions: [...availableSessions],
|
||||
});
|
||||
expect(shallow(<SelectSessionModal courseNumber={courseNumber} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('modal without leave option ', () => {
|
||||
hooks.mockReturnValueOnce({
|
||||
...hookReturn,
|
||||
entitlementSessions: [...availableSessions],
|
||||
showLeaveSessionInSessionModal: false,
|
||||
});
|
||||
expect(shallow(<SelectSessionModal courseNumber={courseNumber} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSessionModal snapshot empty modal with leave option 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={false}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
size="md"
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
Change or leave session?
|
||||
</h4>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
When you change to a different session any course progress or grades from your current session will be lost.
|
||||
</Form.Label>
|
||||
<Form.RadioSet
|
||||
name="sessions"
|
||||
>
|
||||
<Form.Radio
|
||||
value="leave"
|
||||
>
|
||||
Leave session
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction useSelectSession.closeSessionModal]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button>
|
||||
Confirm Session
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionModal snapshot modal with leave option 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={false}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
size="md"
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
Change or leave session?
|
||||
</h4>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
When you change to a different session any course progress or grades from your current session will be lost.
|
||||
</Form.Label>
|
||||
<Form.RadioSet
|
||||
name="sessions"
|
||||
>
|
||||
<Form.Radio
|
||||
key="1/2/2000"
|
||||
value="1/2/2000"
|
||||
>
|
||||
1/2/2000
|
||||
-
|
||||
1/2/2020
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="2/3/2000"
|
||||
value="2/3/2000"
|
||||
>
|
||||
2/3/2000
|
||||
-
|
||||
2/3/2020
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="3/4/2000"
|
||||
value="3/4/2000"
|
||||
>
|
||||
3/4/2000
|
||||
-
|
||||
3/4/2020
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
value="leave"
|
||||
>
|
||||
Leave session
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction useSelectSession.closeSessionModal]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button>
|
||||
Confirm Session
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionModal snapshot modal without leave option 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={false}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
size="md"
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h4>
|
||||
Select a session to access course-title: unit test save life
|
||||
</h4>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement.
|
||||
</Form.Label>
|
||||
<Form.RadioSet
|
||||
name="sessions"
|
||||
>
|
||||
<Form.Radio
|
||||
key="1/2/2000"
|
||||
value="1/2/2000"
|
||||
>
|
||||
1/2/2000
|
||||
-
|
||||
1/2/2020
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="2/3/2000"
|
||||
value="2/3/2000"
|
||||
>
|
||||
2/3/2000
|
||||
-
|
||||
2/3/2020
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
key="3/4/2000"
|
||||
value="3/4/2000"
|
||||
>
|
||||
3/4/2000
|
||||
-
|
||||
3/4/2020
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction useSelectSession.closeSessionModal]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Nevermind
|
||||
</Button>
|
||||
<Button>
|
||||
Confirm Session
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSession snapshot has courseNumber 1`] = `
|
||||
<SelectSessionModal
|
||||
courseNumber="some course"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`SelectSession snapshot no courseNumber 1`] = `""`;
|
||||
35
src/containers/SelectSession/hooks.js
Normal file
35
src/containers/SelectSession/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { hooks as appHooks, actions } from 'data/redux';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useSelectSession = ({ courseNumber }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
showSessionModal,
|
||||
showLeaveSessionInSessionModal,
|
||||
} = appHooks.useSelectSessionsModalData();
|
||||
|
||||
const { entitlementSessions } = appHooks.useCardEntitlementsData(courseNumber);
|
||||
|
||||
const { title: courseTitle } = appHooks.useCardCourseData(courseNumber);
|
||||
|
||||
const updateSessionModal = (showModal, showLeaveOption = false) => dispatch(
|
||||
actions.app.updateSelectSessionModal({
|
||||
showSessionModal: showModal,
|
||||
showLeaveSessionInSessionModal: showLeaveOption,
|
||||
courseNumber,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
showSessionModal,
|
||||
closeSessionModal: () => updateSessionModal(false),
|
||||
openSessionModal: () => updateSessionModal(true),
|
||||
openSessionModalWithLeaveOption: () => updateSessionModal(true, true),
|
||||
showLeaveSessionInSessionModal,
|
||||
entitlementSessions,
|
||||
courseTitle,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSelectSession;
|
||||
85
src/containers/SelectSession/hooks.test.js
Normal file
85
src/containers/SelectSession/hooks.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { hooks as appHooks, actions } from 'data/redux';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardEntitlementsData: jest.fn(),
|
||||
useCardCourseData: jest.fn(),
|
||||
useSelectSessionsModalData: jest.fn(),
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
updateSelectSessionModal: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const courseNumber = 'my-test-course-number';
|
||||
|
||||
const entitlement = {
|
||||
showSessionModal: false,
|
||||
showLeaveSessionInSessionModal: false,
|
||||
};
|
||||
|
||||
const availableSessions = [
|
||||
{ startDate: '1/2/2000', endDate: '1/2/2020', courseNumber },
|
||||
{ startDate: '2/3/2000', endDate: '2/3/2020', courseNumber },
|
||||
{ startDate: '3/4/2000', endDate: '3/4/2020', courseNumber },
|
||||
];
|
||||
|
||||
const cardCourseData = {
|
||||
title: 'course-title: brown fox',
|
||||
};
|
||||
|
||||
describe('SelectSessionModal hooks', () => {
|
||||
let out;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('useSelectSession', () => {
|
||||
beforeEach(() => {
|
||||
appHooks.useSelectSessionsModalData.mockReturnValueOnce({ ...entitlement });
|
||||
appHooks.useCardEntitlementsData.mockReturnValueOnce({ entitlementSessions: availableSessions });
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({ ...cardCourseData });
|
||||
out = hooks.useSelectSession({ courseNumber });
|
||||
});
|
||||
|
||||
test('loads entitlement data based on course number', () => {
|
||||
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber);
|
||||
});
|
||||
|
||||
test('get course title based on course number', () => {
|
||||
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
|
||||
expect(out.courseTitle).toEqual(cardCourseData.title);
|
||||
});
|
||||
|
||||
test('open session modal', () => {
|
||||
out.openSessionModal();
|
||||
expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
|
||||
showSessionModal: true,
|
||||
showLeaveSessionInSessionModal: false,
|
||||
courseNumber,
|
||||
});
|
||||
});
|
||||
|
||||
test('open session modal with leave option', () => {
|
||||
out.openSessionModalWithLeaveOption();
|
||||
expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
|
||||
showSessionModal: true,
|
||||
showLeaveSessionInSessionModal: true,
|
||||
courseNumber,
|
||||
});
|
||||
});
|
||||
|
||||
test('close session modal', () => {
|
||||
out.closeSessionModal();
|
||||
expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
|
||||
showSessionModal: false,
|
||||
showLeaveSessionInSessionModal: false,
|
||||
courseNumber,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
12
src/containers/SelectSession/index.jsx
Normal file
12
src/containers/SelectSession/index.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import SelectSessionModal from './SelectSessionModal';
|
||||
|
||||
export const SelectSession = () => {
|
||||
const { courseNumber } = appHooks.useSelectSessionsModalData();
|
||||
return courseNumber ? <SelectSessionModal courseNumber={courseNumber} /> : null;
|
||||
};
|
||||
SelectSession.propTypes = {};
|
||||
|
||||
export default SelectSession;
|
||||
26
src/containers/SelectSession/index.test.jsx
Normal file
26
src/containers/SelectSession/index.test.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import SelectSession from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useSelectSessionsModalData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SelectSession', () => {
|
||||
describe('snapshot', () => {
|
||||
test('no courseNumber', () => {
|
||||
appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: null });
|
||||
expect(shallow(<SelectSession />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('has courseNumber', () => {
|
||||
appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: 'some course' });
|
||||
expect(shallow(<SelectSession />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
42
src/containers/SelectSession/messages.js
Normal file
42
src/containers/SelectSession/messages.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable quotes */
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
changeOrLeaveHeader: {
|
||||
id: 'learner-dash.selectSession.changeOrLeaveHeader',
|
||||
description: 'Header for session that allow leave option',
|
||||
defaultMessage: 'Change or leave session?',
|
||||
},
|
||||
selectSessionHeader: {
|
||||
id: 'learner-dash.selectSession.selectSessionHeader',
|
||||
description: 'Header for unfulfilled entitlement',
|
||||
defaultMessage: 'Select a session to access {courseTitle}',
|
||||
},
|
||||
changeOrLeaveHint: {
|
||||
id: 'learner-dash.selectSession.changeOrLeaveHint',
|
||||
description: 'Hint for session that allow leave option',
|
||||
defaultMessage: 'When you change to a different session any course progress or grades from your current session will be lost.',
|
||||
},
|
||||
selectSessionHint: {
|
||||
id: 'learner-dash.selectSession.selectSessionHint',
|
||||
description: 'Hint for session that does not allow leave option',
|
||||
defaultMessage: 'Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement.',
|
||||
},
|
||||
leaveSessionOption: {
|
||||
id: 'learner-dash.selectSession.leaveSessionOption',
|
||||
description: 'Radio option for leave session',
|
||||
defaultMessage: 'Leave session',
|
||||
},
|
||||
nevermind: {
|
||||
id: 'learner-dash.selectSession.nevermind',
|
||||
description: 'Cancel action for select session modal',
|
||||
defaultMessage: 'Nevermind',
|
||||
},
|
||||
confirmSession: {
|
||||
id: 'learner-dash.selectSession.confirmSession',
|
||||
description: 'Confirm action for select session modal',
|
||||
defaultMessage: 'Confirm Session',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -10,6 +10,7 @@ const initialState = {
|
||||
platformSettings: {},
|
||||
suggestedCourses: [],
|
||||
filterState: {},
|
||||
selectSessionsModal: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -41,6 +42,12 @@ const app = createSlice({
|
||||
platformSettings: payload.platformSettings,
|
||||
suggestedCourses: payload.suggestedCourses,
|
||||
}),
|
||||
updateSelectSessionModal: (state, { payload }) => ({
|
||||
...state,
|
||||
selectSessionsModal: {
|
||||
...payload,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ describe('app reducer', () => {
|
||||
},
|
||||
entitlements: [],
|
||||
};
|
||||
const testValue = 'my-test-value';
|
||||
const testAction = (action, expected) => {
|
||||
expect(reducer(testState, action)).toEqual({
|
||||
...testState,
|
||||
...expected,
|
||||
});
|
||||
};
|
||||
// const testValue = 'my-test-value';
|
||||
// const testAction = (action, expected) => {
|
||||
// expect(reducer(testState, action)).toEqual({
|
||||
// ...testState,
|
||||
// ...expected,
|
||||
// });
|
||||
// };
|
||||
describe('action handlers', () => {
|
||||
describe('loadCourses', () => {
|
||||
const courseIds = [
|
||||
|
||||
@@ -17,6 +17,7 @@ export const simpleSelectors = {
|
||||
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
|
||||
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
|
||||
enterpriseDashboards: mkSimpleSelector(app => app.enterpriseDashboards),
|
||||
selectSessionsModal: mkSimpleSelector(app => app.selectSessionsModal),
|
||||
};
|
||||
|
||||
export const courseCardData = (state, courseNumber) => (
|
||||
@@ -68,6 +69,7 @@ export const courseCard = StrictDict({
|
||||
const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow;
|
||||
return {
|
||||
canChange: entitlements.canChange,
|
||||
canViewCourse: entitlements.canViewCourse,
|
||||
entitlementSessions: entitlements.availableSessions,
|
||||
isEntitlement: entitlements.isEntitlement,
|
||||
isExpired: entitlements.isExpired,
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpr
|
||||
export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings);
|
||||
// suggested courses is max at 3 at the moment.
|
||||
export const useSuggestedCoursesData = () => useSelector(appSelectors.suggestedCourses).slice(0, 3);
|
||||
export const useSelectSessionsModalData = () => useSelector(appSelectors.selectSessionsModal);
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const useCourseCardData = (selector) => (courseNumber) => useSelector(
|
||||
|
||||
@@ -289,6 +289,7 @@ export const courseRuns = [
|
||||
canChange: true,
|
||||
changeDeadline: futureDate,
|
||||
isExpired: false,
|
||||
availableSessions,
|
||||
},
|
||||
},
|
||||
// Entitlement Course Run - Can View and Change
|
||||
@@ -305,6 +306,7 @@ export const courseRuns = [
|
||||
canChange: true,
|
||||
changeDeadline: futureDate,
|
||||
isExpired: false,
|
||||
availableSessions,
|
||||
},
|
||||
},
|
||||
// Entitlement Course Run - Can View but not Change
|
||||
@@ -345,6 +347,10 @@ export const courseRuns = [
|
||||
},
|
||||
];
|
||||
|
||||
// unfulfilled entitlement select session
|
||||
// unfulfilled entitlement select session with deadline
|
||||
// unfulfilled entitlement select session pass deadline with available session {banner different from 4th}
|
||||
// unfulfilled entitlement select session pass deadline without available session
|
||||
export const entitlementCourses = [
|
||||
{
|
||||
entitlements: {
|
||||
|
||||
@@ -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(date => date).mockName('useIntl.formatDate');
|
||||
const formatDate = jest.fn(date => new Date(date).toLocaleDateString()).mockName('useIntl.formatDate');
|
||||
return {
|
||||
...i18n,
|
||||
intlShape: PropTypes.shape({
|
||||
@@ -133,8 +133,6 @@ jest.mock('hooks', () => ({
|
||||
nullMethod: jest.fn().mockName('hooks.nullMethod'),
|
||||
}));
|
||||
|
||||
jest.mock('@zip.js/zip.js', () => ({}));
|
||||
|
||||
// Mock react-redux hooks
|
||||
// unmock for integration tests
|
||||
jest.mock('react-redux', () => {
|
||||
@@ -154,3 +152,10 @@ jest.mock('hooks', () => ({
|
||||
...jest.requireActual('hooks'),
|
||||
nullMethod: jest.fn().mockName('hooks.nullMethod'),
|
||||
}));
|
||||
|
||||
jest.mock('moment', () => ({
|
||||
__esModule: true,
|
||||
default: (date) => ({
|
||||
toDate: jest.fn().mockReturnValue(date),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -118,14 +118,13 @@ describe('ESG app integration tests', () => {
|
||||
inspector = new Inspector(el);
|
||||
});
|
||||
|
||||
test('initialization', async (done) => {
|
||||
test('initialization', async () => {
|
||||
await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending);
|
||||
resolveFns.init.success();
|
||||
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
|
||||
done();
|
||||
});
|
||||
|
||||
test('course cards', async (done) => {
|
||||
test('course cards', async () => {
|
||||
resolveFns.init.success();
|
||||
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
|
||||
await inspector.findByText(fakeData.courseRunData[0].course.title);
|
||||
@@ -152,11 +151,9 @@ describe('ESG app integration tests', () => {
|
||||
[
|
||||
courseData.provider.name,
|
||||
courseNumber,
|
||||
appMessages.withValues.CourseCard.accessExpires({
|
||||
appMessages.withValues.CourseCardDetails.accessExpires({
|
||||
accessExpirationDate: courseData.enrollment.accessExpirationDate,
|
||||
}),
|
||||
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import CourseCard from 'containers/CourseCard/messages';
|
||||
import CourseCardDetails from 'containers/CourseCard/components/CourseCardDetails/messages';
|
||||
|
||||
const mapMessages = (messages) => Object.keys(messages).reduce(
|
||||
(acc, key) => ({ ...acc, [key]: messages[key].defaultMessage }),
|
||||
@@ -22,8 +22,8 @@ const mapMessagesWithValues = (messages) => Object.keys(messages).reduce(
|
||||
);
|
||||
|
||||
export default {
|
||||
CourseCard: mapMessages(CourseCard),
|
||||
CourseCardDetails: mapMessages(CourseCardDetails),
|
||||
withValues: {
|
||||
CourseCard: mapMessagesWithValues(CourseCard),
|
||||
CourseCardDetails: mapMessagesWithValues(CourseCardDetails),
|
||||
},
|
||||
};
|
||||
|
||||
5
src/utils/dateFormatter.js
Normal file
5
src/utils/dateFormatter.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import moment from 'moment';
|
||||
|
||||
const dateFormatter = (formatDate, date) => formatDate(moment(date).toDate(), { year: 'numeric', month: 'long', day: '2-digit' });
|
||||
|
||||
export default dateFormatter;
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as StrictDict } from './StrictDict';
|
||||
export { default as keyStore } from './keyStore';
|
||||
export { default as dateFormatter } from './dateFormatter';
|
||||
|
||||
Reference in New Issue
Block a user