chore: update course entitlement

chore: update requested change

chore: move show select session dialog to redux app level
This commit is contained in:
Leangseu Kim
2022-07-26 16:08:53 -04:00
committed by leangseu-edx
parent c8c4f8019c
commit 7f7625333d
41 changed files with 1147 additions and 257 deletions

25
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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"

View File

@@ -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 = {

View File

@@ -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' };

View File

@@ -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"
>

View File

@@ -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',

View File

@@ -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 };
};

View File

@@ -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),
});
});
});
});

View File

@@ -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;

View File

@@ -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>
`;

View File

@@ -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;

View File

@@ -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) },
));
});
});
});
});

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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;

View File

@@ -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,
};
};

View File

@@ -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) },
));
});
});
});
});

View File

@@ -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"

View File

@@ -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 })),
};

View File

@@ -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;

View File

@@ -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>
);

View 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;

View 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();
});
});
});

View File

@@ -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>
`;

View File

@@ -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`] = `""`;

View 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;

View 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,
});
});
});
});

View 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;

View 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();
});
});
});

View 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;

View File

@@ -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,
},
}),
},
});

View File

@@ -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 = [

View File

@@ -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,

View File

@@ -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(

View File

@@ -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: {

View File

@@ -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),
}),
}));

View File

@@ -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();
});
});

View File

@@ -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),
},
};

View 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;

View File

@@ -1,2 +1,3 @@
export { default as StrictDict } from './StrictDict';
export { default as keyStore } from './keyStore';
export { default as dateFormatter } from './dateFormatter';