feat: CourseCard and CourseCardActions i18n and tests

This commit is contained in:
Ben Warzeski
2022-06-09 22:28:10 -04:00
parent 459c0058de
commit aa3fc1321d
18 changed files with 670 additions and 84 deletions

View File

@@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard component snapshot 1`] = `
<div
className="mb-3"
>
<Card
orientation="horizontal"
>
<Card.ImageCap
src="hooks.bannerUrl"
srcAlt={
Object {
"formatted": Object {
"defaultMessage": "Course thumbnail",
"description": "Course card banner alt-text",
"id": "learner-dash.courseCard.bannerAlt",
},
}
}
/>
<Card.Body>
<Card.Header
actions={
<CourseCardMenu
courseNumber="test-course-number"
/>
}
title="hooks.title"
/>
<Card.Section>
hooks.providerName
test-course-number
</Card.Section>
<Card.Footer
orientation="vertical"
textElement={
<RelatedProgramsBadge
courseNumber="test-course-number"
/>
}
>
<CourseCardActions
courseNumber="test-course-number"
/>
</Card.Footer>
</Card.Body>
</Card>
<div
className="course-card-banners"
>
<CourseBanner
courseNumber="test-course-number"
/>
<CertificateBanner
courseNumber="test-course-number"
/>
<EntitlementBanner
courseNumber="test-course-number"
/>
</div>
</div>
`;

View File

@@ -1,58 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { Locked } from '@edx/paragon/icons';
import { selectors } from 'data/redux';
import { getCardValues } from 'hooks';
const { cardData } = selectors;
export const CourseCardActions = ({ courseNumber }) => {
const {
canUpgrade,
isAudit,
isAuditAccessExpired,
isVerified,
isPending,
isFinished,
} = getCardValues(courseNumber, {
canUpgrade: cardData.canUpgrade,
isAudit: cardData.isAudit,
isAuditAccessExpired: cardData.isAuditAccessExpired,
isVerified: cardData.isVerified,
isPending: cardData.isCourseRunPending,
isFinished: cardData.isCourseRunFinished,
});
let primary;
let secondary = null;
if (!isVerified) {
secondary = (
<Button
iconBefore={Locked}
variant="outline-primary"
disabled={!canUpgrade}
>
Upgrade
</Button>
);
}
if (isPending) {
primary = (<Button>Begin Course</Button>);
} else if (!isFinished) {
primary = (isAudit && isAuditAccessExpired)
? (<Button disabled>Resume</Button>)
: (<Button>Resume</Button>);
} else {
primary = (<Button>View Course</Button>);
}
return (<>{secondary}{primary}</>);
};
CourseCardActions.propTypes = {
courseNumber: PropTypes.string.isRequired,
};
export default CourseCardActions;

View File

@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard Actions component does not render secondary button if null is returned for secondary props 1`] = `
<Fragment>
<Button
courseNumber="test-course-number"
prop1="primary-prop1"
prop2="primary-prop2"
>
primary-children
</Button>
</Fragment>
`;
exports[`CourseCard Actions component loads primary and secondary button props from hook 1`] = `
<Fragment>
<Button
courseNumber="test-course-number"
prop1="primary-prop1"
prop2="primary-prop2"
>
primary-children
</Button>
<Button
courseNumber="test-course-number"
prop1="primary-prop1"
prop2="primary-prop2"
>
primary-children
</Button>
</Fragment>
`;

View File

@@ -0,0 +1,42 @@
import { Locked } from '@edx/paragon/icons';
import { selectors } from 'data/redux';
import { useIntl, getCardValues } from 'hooks';
import messages from './messages';
const { cardData } = selectors;
export const actionHooks = ({ courseNumber }) => {
const { formatMessage } = useIntl();
const data = getCardValues(courseNumber, {
canUpgrade: cardData.canUpgrade,
isAudit: cardData.isAudit,
isAuditAccessExpired: cardData.isAuditAccessExpired,
isVerified: cardData.isVerified,
isPending: cardData.isCourseRunPending,
isFinished: cardData.isCourseRunFinished,
});
let primary;
let secondary = null;
if (!data.isVerified) {
secondary = {
iconBefore: Locked,
variant: 'outline-primary',
disabled: !data.canUpgrade,
children: formatMessage(messages.upgrade),
};
}
if (data.isPending) {
primary = { children: formatMessage(messages.beginCourse) };
} else if (!data.isFinished) {
primary = {
children: formatMessage(messages.resume),
disabled: data.isAudit && data.isAuditAccessExpired,
};
} else {
primary = { children: formatMessage(messages.viewCourse) };
}
return { primary, secondary };
};
export default actionHooks;

View File

@@ -0,0 +1,108 @@
import { Locked } from '@edx/paragon/icons';
import { selectors } from 'data/redux';
import * as appHooks from 'hooks';
import { testCardValues } from 'testUtils';
import * as hooks from './hooks';
import messages from './messages';
const courseNumber = 'my-test-course-number';
const { fieldKeys } = selectors.cardData;
const props = {
canUpgrade: false,
isAudit: true,
isAuditAccessExpired: false,
isVerified: false,
isPending: false,
isFinished: false,
};
describe('CourseCardActions hooks', () => {
let out;
const { formatMessage } = appHooks.useIntl();
describe('data connection', () => {
beforeEach(() => {
out = hooks.actionHooks({ courseNumber });
});
testCardValues(courseNumber, {
canUpgrade: fieldKeys.canUpgrade,
isAudit: fieldKeys.isAudit,
isAuditAccessExpired: fieldKeys.isAuditAccessExpired,
isVerified: fieldKeys.isVerified,
isPending: fieldKeys.isCourseRunPending,
isFinished: fieldKeys.isCourseRunFinished,
});
});
describe('secondary action', () => {
it('returns null if verified', () => {
appHooks.getCardValues.mockReturnValueOnce({
...props,
isAudit: false,
isVerified: true,
});
out = hooks.actionHooks({ courseNumber });
expect(out.secondary).toEqual(null);
});
it('returns disabled upgrade button if audit, but cannot upgrade', () => {
appHooks.getCardValues.mockReturnValueOnce(props);
out = hooks.actionHooks({ courseNumber });
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', () => {
appHooks.getCardValues.mockReturnValueOnce({ ...props, canUpgrade: true });
out = hooks.actionHooks({ courseNumber });
expect(out.secondary).toEqual({
iconBefore: Locked,
variant: 'outline-primary',
disabled: false,
children: formatMessage(messages.upgrade),
});
});
});
describe('primary action', () => {
it('returns Begin Course button if pending', () => {
appHooks.getCardValues.mockReturnValueOnce({ ...props, isPending: true });
out = hooks.actionHooks({ courseNumber });
expect(out.primary).toEqual({
children: formatMessage(messages.beginCourse),
});
});
it('returns enabled Resume button if active, and not audit with expired access', () => {
appHooks.getCardValues.mockReturnValueOnce({ ...props, isAuditAccessExpired: true });
out = hooks.actionHooks({ courseNumber });
expect(out.primary).toEqual({
children: formatMessage(messages.resume),
disabled: true,
});
});
it('returns disabled Resume button if active and audit without expired access', () => {
appHooks.getCardValues.mockReturnValueOnce({ ...props });
out = hooks.actionHooks({ courseNumber });
expect(out.primary).toEqual({
children: formatMessage(messages.resume),
disabled: false,
});
appHooks.getCardValues.mockReturnValueOnce({ ...props, isAudit: false, isVerified: true });
out = hooks.actionHooks({ courseNumber });
expect(out.primary).toEqual({
children: formatMessage(messages.resume),
disabled: false,
});
});
it('returns viewCourse button if finished', () => {
appHooks.getCardValues.mockReturnValueOnce({ ...props, isFinished: true });
out = hooks.actionHooks({ courseNumber });
expect(out.primary).toEqual({
children: formatMessage(messages.viewCourse),
});
});
});
});

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import hooks from './hooks';
export const CourseCardActions = ({ courseNumber }) => {
const { primary, secondary } = hooks({ courseNumber });
return (
<>
{(secondary !== null) && (
<Button {...secondary} />
)}
<Button {...primary} />
</>
);
};
CourseCardActions.propTypes = {
courseNumber: PropTypes.string.isRequired,
};
export default CourseCardActions;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { shallow } from 'enzyme';
import hooks from './hooks';
import CourseCardActions from '.';
jest.mock('./hooks', () => ({
__esModule: true,
default: jest.fn(),
}));
const courseNumber = 'test-course-number';
describe('CourseCard Actions component', () => {
it('loads primary and secondary button props from hook', () => {
const mockHook = (args) => ({
primary: {
prop1: 'primary-prop1',
prop2: 'primary-prop2',
children: 'primary-children',
courseNumber: args.courseNumber,
},
secondary: {
prop1: 'primary-prop1',
prop2: 'primary-prop2',
children: 'primary-children',
courseNumber: args.courseNumber,
},
});
hooks.mockImplementationOnce(mockHook);
expect(shallow(<CourseCardActions courseNumber={courseNumber} />)).toMatchSnapshot();
});
it('does not render secondary button if null is returned for secondary props', () => {
const mockHook = (args) => ({
primary: {
prop1: 'primary-prop1',
prop2: 'primary-prop2',
children: 'primary-children',
courseNumber: args.courseNumber,
},
secondary: null,
});
hooks.mockImplementationOnce(mockHook);
expect(shallow(<CourseCardActions courseNumber={courseNumber} />)).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,26 @@
import { StrictDict } from 'utils';
export const messages = StrictDict({
upgrade: {
id: 'learner-dash.courseCard.actions.upgrade',
description: 'Course card upgrade button text',
defaultMessage: 'Upgrade',
},
beginCourse: {
id: 'learner-dash.courseCard.actions.beginCourse',
description: 'Course card begin-course button text',
defaultMessage: 'Begin Course',
},
resume: {
id: 'learner-dash.courseCard.actions.resume',
description: 'Course card resume button text',
defaultMessage: 'Resume',
},
viewCourse: {
id: 'learner-dash.courseCard.actions.viewCourse',
description: 'Course card view-course button text',
defaultMessage: 'View Course',
},
});
export default messages;

View File

@@ -0,0 +1,49 @@
import { selectors } from 'data/redux';
import { useIntl, getCardValues } from 'hooks';
import * as module from './hooks';
import messages from './messages';
const { cardData } = selectors;
export const accessMessage = ({ courseNumber }) => {
const { formatMessage, formatDate } = useIntl();
const data = getCardValues(courseNumber, {
accessExpirationDate: cardData.courseRunAccessExpirationDate,
isAudit: cardData.isAudit,
isFinished: cardData.isCourseRunFinished,
isAuditAccessExpired: cardData.isAuditAccessExpired,
endDate: cardData.courseRunEndDate,
});
if (data.isAudit) {
return formatMessage(
data.isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
{ accessExpirationDate: formatDate(data.accessExpirationDate) },
);
}
return formatMessage(
data.isFinished ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(data.endDate) },
);
};
export const cardHooks = ({ courseNumber }) => {
const { formatMessage } = useIntl();
const data = getCardValues(courseNumber, {
title: cardData.courseTitle,
bannerUrl: cardData.courseBannerUrl,
providerName: cardData.providerName,
});
return {
title: data.title,
bannerUrl: data.bannerUrl,
providerName: data.providerName || formatMessage(messages.unknownProviderName),
accessMessage: module.accessMessage({ courseNumber }),
formatMessage,
};
};
export default cardHooks;

View File

@@ -0,0 +1,125 @@
import { keyStore } from 'utils';
import { selectors } from 'data/redux';
import * as appHooks from 'hooks';
import { testCardValues } from 'testUtils';
import * as hooks from './hooks';
import messages from './messages';
const { fieldKeys } = selectors.cardData;
const courseNumber = 'my-test-course-number';
describe('CourseCard hooks', () => {
let out;
const { formatMessage, formatDate } = appHooks.useIntl();
describe('cardHooks', () => {
const accessMessage = 'test-access-message';
const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, accessMessage });
const hookKeys = keyStore(hooks);
beforeEach(() => {
jest.spyOn(hooks, hookKeys.accessMessage).mockImplementationOnce(mockAccessMessage);
out = hooks.cardHooks({ courseNumber });
});
testCardValues(courseNumber, {
title: fieldKeys.courseTitle,
bannerUrl: fieldKeys.courseBannerUrl,
providerName: fieldKeys.providerName,
});
test('providerName returns Unknown message if not provided', () => {
appHooks.getCardValues.mockReturnValueOnce({
title: 'title',
bannerUrl: 'bannerUrl',
providerName: null,
});
jest.spyOn(hooks, hookKeys.accessMessage).mockImplementationOnce(mockAccessMessage);
out = hooks.cardHooks({ courseNumber });
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
});
describe('accessMessage', () => {
it('returns the output of accessMessage hook, passed courseNumber', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber }));
});
});
it('forwards formatMessage from useIntl', () => {
expect(out.formatMessage).toEqual(formatMessage);
});
});
describe('accessMessage', () => {
const accessExpirationDate = 'test-expiration-date';
const endDate = 'test-end-date';
describe('loaded data', () => {
beforeEach(() => {
appHooks.getCardValues.mockClear();
out = hooks.accessMessage({ courseNumber });
});
testCardValues(courseNumber, {
accessExpirationDate: fieldKeys.courseRunAccessExpirationDate,
isAudit: fieldKeys.isAudit,
isFinished: fieldKeys.isCourseRunFinished,
isAuditAccessExpired: fieldKeys.isAuditAccessExpired,
endDate: fieldKeys.courseRunEndDate,
});
});
describe('if audit, and expired', () => {
it('returns accessExpires message with accessExpirationDate from cardData', () => {
appHooks.getCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: true,
isFinished: false,
isAuditAccessExpired: true,
});
expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage(
messages.accessExpired,
{ accessExpirationDate: formatDate(accessExpirationDate) },
));
});
});
describe('if audit and not expired', () => {
it('returns accessExpires message with accessExpirationDate from cardData', () => {
appHooks.getCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: true,
isFinished: false,
isAuditAccessExpired: false,
});
expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage(
messages.accessExpires,
{ accessExpirationDate: formatDate(accessExpirationDate) },
));
});
});
describe('if verified and not ended', () => {
it('returns accessExpires message with accessExpirationDate from cardData', () => {
appHooks.getCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: false,
isFinished: false,
isAuditAccessExpired: true,
});
expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage(
messages.courseEnds,
{ endDate: formatDate(endDate) },
));
});
});
describe('if verified and ended', () => {
it('returns accessExpires message with accessExpirationDate from cardData', () => {
appHooks.getCardValues.mockReturnValueOnce({
accessExpirationDate,
endDate,
isAudit: false,
isFinished: true,
isAuditAccessExpired: true,
});
expect(hooks.accessMessage({ courseNumber })).toEqual(formatMessage(
messages.courseEnded,
{ endDate: formatDate(endDate) },
));
});
});
});
});

View File

@@ -4,9 +4,7 @@ import PropTypes from 'prop-types';
// import PropTypes from 'prop-types';
import { Card } from '@edx/paragon';
import { selectors } from 'data/redux';
import { getCardValues } from 'hooks';
import hooks from './hooks';
import RelatedProgramsBadge from './components/RelatedProgramsBadge';
import CourseCardMenu from './components/CourseCardMenu';
@@ -16,27 +14,22 @@ import {
EntitlementBanner,
} from './components/Banners';
import CourseCardActions from './components/CourseCardActions';
const { cardData } = selectors;
import messages from './messages';
export const CourseCard = ({ courseNumber }) => {
const {
title,
bannerUrl,
accessExpirationDate,
providerName,
} = getCardValues(courseNumber, {
title: cardData.courseTitle,
bannerUrl: cardData.courseBannerUrl,
accessExpirationDate: cardData.courseRunAccessExpirationDate,
providerName: cardData.providerName,
});
accessMessage,
formatMessage,
} = hooks({ courseNumber });
return (
<div className="mb-3">
<Card orientation="horizontal">
<Card.ImageCap
src={bannerUrl}
srcAlt="course thumbnail"
srcAlt={formatMessage(messages.bannerAlt)}
/>
<Card.Body>
<Card.Header
@@ -44,7 +37,7 @@ export const CourseCard = ({ courseNumber }) => {
actions={<CourseCardMenu courseNumber={courseNumber} />}
/>
<Card.Section>
{providerName || 'Unkown'} {courseNumber} Access expires {accessExpirationDate}
{providerName} {courseNumber} {accessMessage}
</Card.Section>
<Card.Footer
orientation="vertical"

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { shallow } from 'enzyme';
import CourseCard from '.';
import hooks from './hooks';
jest.mock('./hooks', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('./components/RelatedProgramsBadge', () => 'RelatedProgramsBadge');
jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
jest.mock('./components/banners', () => ({
CourseBanner: () => 'CourseBanner',
CertificateBanner: () => 'CertificateBanner',
EntitlementBanner: () => 'EntitlementBanner',
}));
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
const dataProps = {
title: 'hooks.title',
bannerUrl: 'hooks.bannerUrl',
providerName: 'hooks.providerName',
accessMessagE: 'hooks.accessMessage',
formatMessage: jest.fn(msg => ({ formatted: msg })),
};
const courseNumber = 'test-course-number';
describe('CourseCard component', () => {
test('snapshot', () => {
hooks.mockReturnValueOnce(dataProps);
expect(shallow(<CourseCard courseNumber={courseNumber} />)).toMatchSnapshot();
expect(hooks).toHaveBeenCalledWith({ courseNumber });
});
});

View File

@@ -0,0 +1,36 @@
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

@@ -11,6 +11,7 @@ export const fieldSelectors = {
courseTitle: data => data.course.title,
courseBannerUrl: data => data.course.bannerUrl,
courseRunAccessExpirationDate: data => data.courseRun.accessExpirationDate,
courseRunEndDate: data => data.courseRun.endDate,
courseWebsite: data => data.course.website,
providerName: data => data.provider?.name,
isVerified: data => data.enrollment.isVerified,

View File

@@ -56,6 +56,7 @@ export const genCourseRunData = (data = {}) => ({
isFinished: false,
isArchived: false,
accessExpirationDate: futureDate,
endDate: futureDate,
minPassingGrade: 70,
...data,
});
@@ -140,7 +141,7 @@ export const courseRuns = [
},
// verified, pending, restricted
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isPending: true },
certificates: genCertificateData({ isRestricted: true }),
@@ -148,7 +149,7 @@ export const courseRuns = [
},
// verified, started
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
@@ -156,7 +157,7 @@ export const courseRuns = [
},
// verified, not passing
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: false },
courseRun: { isStarted: true },
certificates: genCertificateData(),
@@ -164,9 +165,9 @@ export const courseRuns = [
},
// verified, finished, not passing
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: false },
courseRun: { isFinished: true },
courseRun: { isFinished: true, endDate: pastDate },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
@@ -191,7 +192,7 @@ export const courseRuns = [
},
// verified, earned, downloadable (web + link)
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData({
@@ -208,7 +209,7 @@ export const courseRuns = [
},
// verified, earned, downloadable (link)
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData({
@@ -224,7 +225,7 @@ export const courseRuns = [
},
// Entitlement Course Run - Cannot view yet
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isPending: true },
certificates: genCertificateData(),
@@ -240,7 +241,7 @@ export const courseRuns = [
},
// Entitlement Course Run - Can View and Change
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
@@ -256,7 +257,7 @@ export const courseRuns = [
},
// Entitlement Course Run - Can View but not Change
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
@@ -272,9 +273,14 @@ export const courseRuns = [
},
// Entitlement Course Run - Expired
{
enrollment: genEnrollmentData({ isVerified: true }),
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true, isFinished: true, isArchived: true },
courseRun: {
isStarted: true,
isFinished: true,
isArchived: true,
endDate: pastDate,
},
certificates: genCertificateData(),
entitlements: {
isEntitlement: true,

View File

@@ -1,3 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { selectors } from 'data/redux';
@@ -18,6 +19,10 @@ export const getCardValues = (courseNumber, mapping) => {
export const nullMethod = () => ({});
export { useIntl };
export default {
getCardValues,
nullMethod,
useIntl,
};

View File

@@ -39,6 +39,9 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Card: {
Body: 'Card.Body',
Footer: 'Card.Footer',
Header: 'Card.Header',
ImageCap: 'Card.ImageCap',
Section: 'Card.Section',
},
Col: 'Col',
Collapsible: {
@@ -129,3 +132,24 @@ jest.mock('react-redux', () => {
useSelector: jest.fn((selector) => ({ useSelector: selector })),
};
});
jest.mock('hooks', () => {
const formatMessage = jest.fn((msg, values) => ({ formatted: { msg, values } }));
return {
...jest.requireActual('hooks'),
useIntl: () => ({
formatMessage,
formatDate: jest.fn((date) => ({ formatted: date })),
}),
getCardValues: jest.fn((courseNumber, mapping) => (
Object.keys(mapping).reduce(
(obj, key) => ({
...obj,
[key]: { selector: mapping[key], courseNumber },
}),
{},
)
)),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
};
});

View File

@@ -1,7 +1,13 @@
import react from 'react';
import { selectors } from 'data/redux';
import * as appHooks from 'hooks';
import { StrictDict } from 'utils';
const { cardData } = selectors;
/**
* Mocked formatMessage provided by react-intl
*/
@@ -185,3 +191,23 @@ export class MockUseState {
});
}
}
/**
* Test that getCardValues was called with the given courseNumber and selector mapping.
* @param {string} courseNumber - course run identifier
* @param {obj} mapping - value mapping { <requestedKey>: <selectorFieldKey> }
*/
export const testCardValues = (courseNumber, mapping) => {
describe('cardData values', () => {
let mapped;
test('passess correct courseNumber', () => {
expect(appHooks.getCardValues.mock.calls[0][0]).toEqual(courseNumber);
});
Object.keys(mapping).forEach(key => {
test(`loads ${key} from card data ${mapping[key]} selector`, () => {
[[, mapped]] = appHooks.getCardValues.mock.calls;
expect(mapped[key]).toEqual(cardData[mapping[key]]);
});
});
});
};