Bw/selector tests (#35)

This commit is contained in:
Ben Warzeski
2022-10-07 11:56:36 -04:00
committed by GitHub
parent ab2bf89e25
commit 6a275cc603
26 changed files with 1133 additions and 515 deletions

View File

@@ -4,6 +4,7 @@ exports[`CourseCard component snapshot: collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="vertical"
@@ -34,6 +35,7 @@ exports[`CourseCard component snapshot: not collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="horizontal"

View File

@@ -13,13 +13,13 @@ export const UpgradeButton = ({ cardId }) => {
const { canUpgrade } = hooks.useCardEnrollmentData(cardId);
const { isMasquerading } = hooks.useMasqueradeData();
const { formatMessage } = useIntl();
const isEnabled = (!isMasquerading && canUpgrade);
return (
<Button
iconBefore={Locked}
variant="outline-primary"
disabled={isMasquerading || !canUpgrade}
as="a"
href={upgradeUrl}
disabled={!isEnabled}
{...isEnabled && { as: 'a', href: upgradeUrl }}
>
{formatMessage(messages.upgrade)}
</Button>

View File

@@ -14,9 +14,7 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
<Button
as="a"
disabled={true}
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>
@@ -26,9 +24,7 @@ exports[`UpgradeButton snapshot cannot upgrade 1`] = `
exports[`UpgradeButton snapshot masquerading 1`] = `
<Button
as="a"
disabled={true}
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>

View File

@@ -16,7 +16,7 @@ export const CourseCard = ({
const isCollapsed = useIsCollapsed();
const orientation = isCollapsed ? 'vertical' : 'horizontal';
return (
<div className="mb-4.5 course-card" data-testid="CourseCard">
<div className="mb-4.5 course-card" id={cardId} data-testid="CourseCard">
<Card orientation={orientation}>
<div className="d-flex flex-column w-100">
<div {...(!isCollapsed && { className: 'd-flex' })}>

View File

@@ -18,11 +18,10 @@ export const useCourseListData = () => {
const dispatch = useDispatch();
const pageNumber = appHooks.usePageNumber();
const [filters, setFilters] = useCheckboxSetValues([]);
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.title);
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
const { numPages, visible } = appHooks.useCurrentCourseList({
sortBy,
isAscending: true,
filters,
pageSize: ListPageSize,
});

View File

@@ -53,13 +53,12 @@ describe('CourseList hooks', () => {
out = hooks.useCourseListData();
});
describe('behavior', () => {
it('initializes sort with title', () => {
state.expectInitializedWith(state.keys.sortBy, SortKeys.title);
it('initializes sort with enrollment date', () => {
state.expectInitializedWith(state.keys.sortBy, SortKeys.enrolled);
});
it('loads current course list with sortBy, isAscending, filters, and page size', () => {
it('loads current course list with sortBy, filters, and page size', () => {
expect(appHooks.useCurrentCourseList).toHaveBeenCalledWith({
sortBy: testSortBy,
isAscending: true,
filters: testFilters,
pageSize: ListPageSize,
});

View File

@@ -16,7 +16,7 @@ export const EnterpriseDashboardModal = () => {
handleClick,
dashboard,
} = useEnterpriseDashboardHook();
if (!dashboard) {
if (!dashboard || !dashboard.label) {
return null;
}
return (

View File

@@ -18,7 +18,7 @@ export const useSelectSessionModalData = () => {
const dispatch = useDispatch();
const selectedCardId = appHooks.useSelectSessionModalData().cardId;
const {
entitlementSessions,
availableSessions,
isFulfilled,
uuid,
} = appHooks.useCardEntitlementData(selectedCardId);
@@ -52,7 +52,7 @@ export const useSelectSessionModalData = () => {
closeSessionModal: updateCallback(dispatch, null),
openSessionModal: (cardId) => updateCallback(dispatch, cardId),
showLeaveOption: isFulfilled,
entitlementSessions,
availableSessions,
hint,
header,
selectedSession,

View File

@@ -40,7 +40,7 @@ const selectSessionData = {
};
const entitlementData = {
ntitlementSessions: [
availableSessions: [
{ startDate: '1/2/2000', endDate: '1/2/2020', cardId: 'session-id-1' },
{ startDate: '2/3/2000', endDate: '2/3/2020', cardId: 'session-id-2' },
{ startDate: '3/4/2000', endDate: '3/4/2020', cardId: 'session-id-3' },

View File

@@ -17,7 +17,7 @@ import messages from './messages';
export const SelectSessionModal = () => {
const {
entitlementSessions,
availableSessions,
showModal,
closeSessionModal,
showLeaveOption,
@@ -49,7 +49,7 @@ export const SelectSessionModal = () => {
onChange={handleSelection}
value={selectedSession}
>
{entitlementSessions?.map((session) => (
{availableSessions?.map((session) => (
<Form.Radio key={session.courseId} value={session.courseId}>
{dateFormatter(formatDate, session.startDate)} - {dateFormatter(formatDate, session.endDate)}
</Form.Radio>

View File

@@ -10,7 +10,7 @@ jest.mock('./hooks', () => ({
}));
const hookReturn = {
entitlementSessions: [],
availableSessions: [],
showModal: true,
closeSessionModal: jest.fn().mockName('useSelectSessionModalData.closeSessionModal'),
showLeaveOption: true,
@@ -36,7 +36,7 @@ describe('SelectSessionModal', () => {
test('modal with leave option ', () => {
hooks.mockReturnValueOnce({
...hookReturn,
entitlementSessions: [...availableSessions],
availableSessions: [...availableSessions],
});
expect(shallow(<SelectSessionModal />)).toMatchSnapshot();
});
@@ -44,7 +44,7 @@ describe('SelectSessionModal', () => {
test('modal without leave option ', () => {
hooks.mockReturnValueOnce({
...hookReturn,
entitlementSessions: [...availableSessions],
availableSessions,
showLeaveOption: false,
});
expect(shallow(<SelectSessionModal />)).toMatchSnapshot();

View File

@@ -1,212 +0,0 @@
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import { FilterKeys } from 'data/constants/app';
import urls from 'data/services/lms/urls';
import * as module from './selectors';
const { baseAppUrl, learningMfeUrl } = urls;
export const appSelector = (state) => state.app;
const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb);
// top-level app data selectors
export const simpleSelectors = {
courseData: mkSimpleSelector(app => app.courseData),
platformSettings: mkSimpleSelector(app => app.platformSettings),
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard),
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
pageNumber: mkSimpleSelector(app => app.pageNumber),
};
export const numCourses = createSelector(
[module.simpleSelectors.courseData],
(courseData) => Object.keys(courseData).length,
);
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
export const hasAvailableDashboards = createSelector(
[module.simpleSelectors.enterpriseDashboard],
(data) => data !== null,
);
export const showSelectSessionModal = createSelector(
[module.simpleSelectors.selectSessionModal],
(data) => data.cardId != null,
);
export const courseCardData = (state, cardId) => (
module.simpleSelectors.courseData(state)[cardId]
);
const mkCardSelector = (sel) => (state, cardId) => {
const cardData = module.courseCardData(state, cardId);
return sel(cardData);
};
const dateSixMonthsFromNow = new Date();
dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180);
const today = new Date();
export const courseCard = StrictDict({
certificate: mkCardSelector(({ certificate }) => {
const availableDate = certificate.availableDate ? new Date(certificate.availableDate) : null;
return {
availableDate,
certPreviewUrl: certificate.certPreviewUrl,
isDownloadable: certificate.isDownloadable,
isEarnedButUnavailable: certificate.isEarned && availableDate > today,
isRestricted: certificate.isRestricted,
};
}),
course: mkCardSelector(({ course }) => ({
bannerImgSrc: baseAppUrl(course.bannerImgSrc),
courseNumber: course.courseNumber,
courseName: course.courseName,
website: course.website,
})),
courseRun: mkCardSelector(({ courseRun }) => (courseRun === null ? {} : {
endDate: courseRun.endDate ? new Date(courseRun?.endDate) : null,
courseId: courseRun.courseId,
isArchived: courseRun.isArchived,
isStarted: courseRun.isStarted,
isFinished: courseRun.isFinished,
minPassingGrade: Math.floor(courseRun.minPassingGrade * 100),
startDate: courseRun.startDate ? new Date(courseRun.startDate) : null,
homeUrl: courseRun.homeUrl,
marketingUrl: courseRun.marketingUrl,
progressUrl: learningMfeUrl(courseRun.progressUrl),
unenrollUrl: learningMfeUrl(courseRun.unenrollUrl),
upgradeUrl: courseRun.upgradeUrl,
resumeUrl: learningMfeUrl(courseRun.resumeUrl),
})),
enrollment: mkCardSelector(({ enrollment }) => {
if (enrollment == null) {
return {
isEnrolled: false,
};
}
const { isStaff, hasUnmetPrereqs, isTooEarly } = enrollment.coursewareAccess;
return {
accessExpirationDate: enrollment.accessExpirationDate ? new Date(enrollment.accessExpirationDate) : null,
canUpgrade: enrollment.canUpgrade,
hasStarted: enrollment.hasStarted,
coursewareAccess: enrollment.coursewareAccess,
hasAccess: isStaff || !(hasUnmetPrereqs || isTooEarly),
hasFinished: enrollment.hasFinished,
isAudit: enrollment.isAudit,
isAuditAccessExpired: enrollment.isAuditAccessExpired,
isEmailEnabled: enrollment.isEmailEnabled,
isVerified: enrollment.isVerified,
lastEnrolled: enrollment.lastEnrollment,
isEnrolled: enrollment.isEnrolled,
};
}),
entitlement: mkCardSelector(({ entitlement }) => {
if (!entitlement || Object.keys(entitlement).length === 0) {
return { isEntitlement: false };
}
const deadline = new Date(entitlement.changeDeadline);
const deadlinePassed = deadline < new Date();
const showExpirationWarning = !deadlinePassed && deadline <= dateSixMonthsFromNow;
return {
canChange: !deadlinePassed,
entitlementSessions: entitlement.availableSessions,
isEntitlement: true,
isExpired: entitlement.isExpired,
isFulfilled: entitlement.isFulfilled,
hasSessions: entitlement.availableSessions?.length > 0,
changeDeadline: deadline,
uuid: entitlement.uuid,
showExpirationWarning,
};
}),
gradeData: mkCardSelector(({ gradeData }) => ({ isPassing: gradeData.isPassing })),
courseProvider: mkCardSelector(({ courseProvider }) => ({ name: courseProvider?.name })),
relatedPrograms: mkCardSelector(({ programs: { relatedPrograms } }) => ({
list: relatedPrograms.map(program => ({
bannerImgSrc: program.bannerImgSrc,
logoImgSrc: program.logoImgSrc,
numberOfCourses: program.numberOfCourses,
programType: program.programType,
programUrl: program.programUrl,
provider: program.provider,
title: program.title,
})),
length: relatedPrograms.length,
})),
});
export const currentList = (state, {
sortBy,
isAscending,
filters,
pageSize,
}) => {
const pageNumber = module.simpleSelectors.pageNumber(state);
let list = Object.values(module.simpleSelectors.courseData(state));
const hasFilter = filters.reduce((obj, filter) => ({ ...obj, [filter]: true }), {});
if (filters.length) {
list = list.filter(course => {
if (hasFilter[FilterKeys.notEnrolled]) {
if (course.enrollment.isEnrolled) {
return false;
}
}
if (hasFilter[FilterKeys.done]) {
if (!course.enrollment.hasFinished) {
return false;
}
}
if (hasFilter[FilterKeys.upgraded]) {
if (!course.enrollment.isVerified) {
return false;
}
}
if (hasFilter[FilterKeys.inProgress]) {
if (!course.enrollment.hasStarted) {
return false;
}
}
if (hasFilter[FilterKeys.notStarted]) {
if (course.enrollment.hasStarted) {
return false;
}
}
return true;
});
}
if (sortBy === 'enrolled') {
list = list.sort((a, b) => {
const dateA = new Date(a.enrollment.lastEnrolled);
const dateB = new Date(b.enrollment.lastEnrolled);
if (dateA < dateB) { return isAscending ? -1 : 1; }
if (dateA > dateB) { return isAscending ? 1 : 1; }
return 0;
});
} else {
list = list.sort((a, b) => {
const nameA = a.course.courseName.toLowerCase();
const nameB = b.course.courseName.toLowerCase();
if (nameA < nameB) { return isAscending ? -1 : 1; }
if (nameA > nameB) { return isAscending ? 1 : 1; }
return 0;
});
}
return {
visible: list.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
numPages: Math.ceil(list.length / pageSize),
};
};
export default StrictDict({
...simpleSelectors,
courseCard,
currentList,
hasCourses,
hasAvailableDashboards,
showSelectSessionModal,
});

View File

@@ -0,0 +1,27 @@
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import simpleSelectors from './simpleSelectors';
import * as module from './appSelectors';
export const numCourses = createSelector(
[simpleSelectors.courseData],
(courseData) => Object.keys(courseData).length,
);
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
export const hasAvailableDashboards = createSelector(
[simpleSelectors.enterpriseDashboard],
(data) => data !== null,
);
export const showSelectSessionModal = createSelector(
[simpleSelectors.selectSessionModal],
(data) => data.cardId != null,
);
export default StrictDict({
numCourses,
hasCourses,
hasAvailableDashboards,
showSelectSessionModal,
});

View File

@@ -0,0 +1,36 @@
import simpleSelectors from './simpleSelectors';
import * as appSelectors from './appSelectors';
describe('basic app selectors', () => {
describe('numCourses', () => {
it('returns the number of courses in the courseData object', () => {
const { preSelectors, cb } = appSelectors.numCourses;
expect(preSelectors).toEqual([simpleSelectors.courseData]);
expect(cb({ course1: 'data', course2: 'data', course3: 'data' })).toEqual(3);
});
});
describe('hasCourses', () => {
it('returns true iff numCourses is greater than 0', () => {
const { preSelectors, cb } = appSelectors.hasCourses;
expect(preSelectors).toEqual([appSelectors.numCourses]);
expect(cb(3)).toEqual(true);
expect(cb(0)).toEqual(false);
});
});
describe('hasAvailableDashboards', () => {
it('returns true iff the enterpriseDashboard field is populated', () => {
const { preSelectors, cb } = appSelectors.hasAvailableDashboards;
expect(preSelectors).toEqual([simpleSelectors.enterpriseDashboard]);
expect(cb({ data: 'test' })).toEqual(true);
expect(cb(null)).toEqual(false);
});
});
describe('showSelectSessionModal', () => {
it('returns true if the selectSessionModal cardId is not null', () => {
const { preSelectors, cb } = appSelectors.showSelectSessionModal;
expect(preSelectors).toEqual([simpleSelectors.selectSessionModal]);
expect(cb({ cardId: 'test' })).toEqual(true);
expect(cb({ data: 'test' })).toEqual(false);
});
});
});

View File

@@ -0,0 +1,136 @@
import { StrictDict } from 'utils';
import urls from 'data/services/lms/urls';
import * as module from './courseCard';
import * as simpleSelectors from './simpleSelectors';
const { baseAppUrl, learningMfeUrl } = urls;
const { cardSimpleSelectors, mkCardSelector } = simpleSelectors;
const today = new Date();
const dateSixMonthsFromNow = new Date();
dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180);
export const loadDateVal = (date) => (date ? new Date(date) : null);
export const courseCard = StrictDict({
certificate: mkCardSelector(
cardSimpleSelectors.certificate,
(certificate) => {
const availableDate = new Date(certificate.availableDate);
const isAvailable = availableDate <= new Date();
return {
availableDate,
certPreviewUrl: certificate.certPreviewUrl,
isDownloadable: certificate.isDownloadable,
isEarnedButUnavailable: certificate.isEarned && !isAvailable,
isRestricted: certificate.isRestricted,
};
},
),
course: mkCardSelector(
cardSimpleSelectors.course,
(course) => ({
bannerImgSrc: baseAppUrl(course.bannerImgSrc),
courseNumber: course.courseNumber,
courseName: course.courseName,
website: course.website,
}),
),
courseProvider: mkCardSelector(
cardSimpleSelectors.courseProvider,
(courseProvider) => ({ name: courseProvider?.name }),
),
courseRun: mkCardSelector(
cardSimpleSelectors.courseRun,
(courseRun) => (courseRun === null ? {} : {
endDate: module.loadDateVal(courseRun.endDate),
startDate: module.loadDateVal(courseRun.startDate),
courseId: courseRun.courseId,
isArchived: courseRun.isArchived,
isStarted: courseRun.isStarted,
isFinished: courseRun.isFinished,
minPassingGrade: Math.floor(courseRun.minPassingGrade * 100),
homeUrl: courseRun.homeUrl,
marketingUrl: courseRun.marketingUrl,
upgradeUrl: courseRun.upgradeUrl,
progressUrl: learningMfeUrl(courseRun.progressUrl),
resumeUrl: learningMfeUrl(courseRun.resumeUrl),
unenrollUrl: learningMfeUrl(courseRun.unenrollUrl),
}),
),
enrollment: mkCardSelector(
cardSimpleSelectors.enrollment,
(enrollment) => {
if (enrollment == null) {
return { isEnrolled: false };
}
const { isStaff, hasUnmetPrereqs, isTooEarly } = enrollment.coursewareAccess;
return {
coursewareAccess: enrollment.coursewareAccess,
hasAccess: isStaff || !(hasUnmetPrereqs || isTooEarly),
isEnrolled: enrollment.isEnrolled,
lastEnrolled: enrollment.lastEnrolled,
hasStarted: enrollment.hasStarted,
hasFinished: enrollment.hasFinished,
accessExpirationDate: module.loadDateVal(enrollment.accessExpirationDate),
canUpgrade: enrollment.canUpgrade,
isAudit: enrollment.isAudit,
isAuditAccessExpired: enrollment.isAuditAccessExpired,
isVerified: enrollment.isVerified,
isEmailEnabled: enrollment.isEmailEnabled,
};
},
),
entitlement: mkCardSelector(
cardSimpleSelectors.entitlement,
(entitlement) => {
if (!entitlement || Object.keys(entitlement).length === 0) {
return { isEntitlement: false };
}
const deadline = new Date(entitlement.changeDeadline);
const deadlinePassed = deadline < today;
const showExpirationWarning = !deadlinePassed && deadline <= dateSixMonthsFromNow;
return {
isEntitlement: true,
availableSessions: entitlement.availableSessions,
changeDeadline: deadline,
isExpired: entitlement.isExpired,
isFulfilled: entitlement.isFulfilled,
uuid: entitlement.uuid,
hasSessions: entitlement.availableSessions?.length > 0,
canChange: !deadlinePassed,
showExpirationWarning,
};
},
),
gradeData: mkCardSelector(
cardSimpleSelectors.gradeData,
(gradeData) => ({ isPassing: gradeData.isPassing }),
),
relatedPrograms: mkCardSelector(
cardSimpleSelectors.relatedPrograms,
(relatedPrograms) => ({
list: relatedPrograms.map(program => ({
bannerImgSrc: program.bannerImgSrc,
logoImgSrc: program.logoImgSrc,
numberOfCourses: program.numberOfCourses,
programType: program.programType,
programUrl: program.programUrl,
provider: program.provider,
title: program.title,
})),
length: relatedPrograms.length,
}),
),
});
export default courseCard;

View File

@@ -0,0 +1,346 @@
import { keyStore } from 'utils';
import urls from 'data/services/lms/urls';
import simpleSelectors from './simpleSelectors';
import * as module from './courseCard';
jest.mock('data/services/lms/urls', () => ({
baseAppUrl: url => ({ baseAppUrl: url }),
learningMfeUrl: url => ({ learningMfeUrl: url }),
}));
jest.mock('./simpleSelectors', () => ({
mkCardSelector: (simpleSelector, selector) => ({
mkCardSelector: { selector, simpleSelector },
}),
cardSimpleSelectors: jest.requireActual('./simpleSelectors').cardSimpleSelectors,
}));
const { courseCard } = module;
const { cardSimpleSelectors } = simpleSelectors;
const { baseAppUrl, learningMfeUrl } = urls;
const moduleKeys = keyStore(module);
let testData;
let selector;
let simpleSelector;
let selected;
const dates = {
today: new Date(),
tomorrow: new Date(),
nextYear: new Date(),
yesterday: new Date(),
};
dates.tomorrow.setDate(dates.tomorrow.getDate() + 1);
dates.nextYear.setDate(dates.nextYear.getDate() + 365);
dates.yesterday.setDate(dates.yesterday.getDate() - 1);
/*
* Takes a selector as input and fetches the referenced selector and simpleSelector for
* the selector to be tested, to be stored in global variables.
* Also sets `selected` global variable is loaded with the passed data, which is stored
* as `testData`.
*/
const loadSelector = (sel, data) => {
({ simpleSelector, selector } = sel.mkCardSelector);
testData = data;
selected = selector(data);
};
describe('courseCard selectors module', () => {
describe('loadDateVal helper function', () => {
it('returns passed date value converted to Date', () => {
const testDate = '2000-10-10';
expect(module.loadDateVal(testDate)).toEqual(new Date(testDate));
});
it('returns null if no value is passed', () => {
expect(module.loadDateVal()).toEqual(null);
});
});
describe('courseCard selectors', () => {
let dateSpy;
beforeEach(() => {
dateSpy = jest.spyOn(module, moduleKeys.loadDateVal);
dateSpy.mockImplementation(v => new Date(v));
});
afterEach(() => {
dateSpy.mockRestore();
});
describe('certificate selector', () => {
beforeEach(() => {
loadSelector(courseCard.certificate, {
certPreviewUrl: 'test-cert-preview-url',
isDownloadable: 'test-is-downloadable',
isRestricted: 'test-is-restricted',
isEarned: false,
availableDate: '2020-10-10',
});
});
it('returns a card selector based on certificate cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.certificate);
});
it('passes availableDate, converted to a date', () => {
expect(selected.availableDate).toMatchObject(new Date(testData.availableDate));
});
it('passes [certPreviewUrl, isDownloadable, isRestricted]', () => {
expect(selected.certPreviewUrl).toEqual(testData.certPreviewUrl);
expect(selected.isDownloadable).toEqual(testData.isDownloadable);
expect(selected.isRestricted).toEqual(testData.isRestricted);
});
describe('isEarnedButUnavailable', () => {
it('passes true iff certificate is earned but availableDate is in the future', () => {
const testSelector = (data, expected) => {
expect(selector({ ...testData, ...data }).isEarnedButUnavailable).toEqual(expected);
};
testSelector({ isEarned: true, availableDate: dates.today }, false);
testSelector({ isEarned: true, availableDate: dates.yesterday }, false);
testSelector({ isEarned: true, availableDate: dates.tomorrow }, true);
testSelector({ isEarned: false, availableDate: dates.tomorrow }, false);
});
});
});
describe('course selector', () => {
beforeEach(() => {
loadSelector(courseCard.course, {
bannerImgSrc: 'test-banner-img-src',
courseNumber: 'test-course-number',
courseName: 'test-course-name',
website: 'test-website',
});
});
it('returns a card selector based on course cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.course);
});
it('passes bannerImgSrc, converted to a baseAppUrl', () => {
expect(selected.bannerImgSrc).toEqual(baseAppUrl(testData.bannerImgSrc));
});
it('passes [courseNumber, courseName, website]', () => {
expect(selected.courseNumber).toEqual(testData.courseNumber);
});
});
describe('courseProvider selector', () => {
beforeEach(() => {
loadSelector(courseCard.courseProvider, { name: 'test-provider-name' });
});
it('returns a card selector based on courseProvider cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.courseProvider);
});
describe('name', () => {
it('passes the provider name if provider is known', () => {
expect(selected.name).toEqual(testData.name);
});
it('passes undefined if provider is not known', () => {
expect(selector().name).toEqual(undefined);
});
});
});
describe('courseRun selector', () => {
beforeEach(() => {
loadSelector(courseCard.courseRun, {
endDate: '3000-10-20',
startDate: '2000-10-20',
courseId: 'test-course-id',
isArchived: 'test-is-archived',
isStarted: 'test-is-started',
isFinished: 'test-is-finished',
minPassingGrade: 0.9354,
homeUrl: 'test-home-url',
marketingUrl: 'test-marketing-url',
upgradeUrl: 'test-upgrade-url',
progressUrl: 'test-progress-url',
resumeUrl: 'test-resume-url',
unenrollUrl: 'test-unenroll-url',
});
});
it('returns a card selector based on courseRun cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.courseRun);
});
it('passes [endDate, startDate], converted to dates', () => {
expect(selected.endDate).toEqual(new Date(testData.endDate));
expect(selected.startDate).toEqual(new Date(testData.startDate));
});
it('passes [courseId, isArchived, isStarted, isFinished]', () => {
expect(selected.courseId).toEqual(testData.courseId);
expect(selected.isArchived).toEqual(testData.isArchived);
expect(selected.isStarted).toEqual(testData.isStarted);
expect(selected.isFinished).toEqual(testData.isFinished);
});
it('passes minPassingGrade floored from float to a percentage value', () => {
expect(selected.minPassingGrade).toEqual(93);
});
it('passes [homeUrl, marketingUrl, upgradeUrl]', () => {
expect(selected.homeUrl).toEqual(testData.homeUrl);
expect(selected.marketingUrl).toEqual(testData.marketingUrl);
expect(selected.upgradeUrl).toEqual(testData.upgradeUrl);
});
it('passes [progressUrl, unenrollUrl, resumeUrl], converted to learningMfeUrls', () => {
expect(selected.progressUrl).toEqual(learningMfeUrl(testData.progressUrl));
expect(selected.resumeUrl).toEqual(learningMfeUrl(testData.resumeUrl));
expect(selected.unenrollUrl).toEqual(learningMfeUrl(testData.unenrollUrl));
});
});
describe('enrollment selector', () => {
beforeEach(() => {
loadSelector(courseCard.enrollment, {
coursewareAccess: {
isStaff: false,
hasUnmetPrereqs: false,
isTooEarly: false,
},
isEnrolled: 'test-is-enrolled',
lastEnrolled: 'test-last-enrolled',
hasStarted: 'test-has-started',
hasFinished: 'test-has-finished',
accessExpirationDate: '3000-10-20',
canUpgrade: 'test-can-upgrade',
isAudit: 'test-is-audit',
isAuditAccessExpired: 'test-is-audit-access-expired',
isVerified: 'test-is-verified',
isEmailEnabled: 'test-is-email-enabled',
});
});
it('returns a card selector based on enrollment cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.enrollment);
});
it('returns { isEnrolled: false } object if null enrollment received', () => {
expect(selector(null)).toEqual({ isEnrolled: false });
});
it('passes [coursewareAccess, hasStarted, hasFinished, isEnrolled, lastEnrolled]', () => {
expect(selected.coursewareAccess).toEqual(testData.coursewareAccess);
expect(selected.hasStarted).toEqual(testData.hasStarted);
expect(selected.hasFinished).toEqual(testData.hasFinished);
expect(selected.isEnrolled).toEqual(testData.isEnrolled);
expect(selected.lastEnrolled).toEqual(testData.lastEnrolled);
});
it('passes hasAccess if staff or neither has umet prereqs nor is too early', () => {
const testAccess = (access, expected) => {
expect(selector({ ...testData, coursewareAccess: access }).hasAccess).toEqual(expected);
};
testAccess({ isStaff: false, hasUnmetPrereqs: false, isTooEarly: false }, true);
testAccess({ isStaff: false, hasUnmetPrereqs: false, isTooEarly: true }, false);
testAccess({ isStaff: false, hasUnmetPrereqs: true, isTooEarly: false }, false);
testAccess({ isStaff: false, hasUnmetPrereqs: true, isTooEarly: true }, false);
testAccess({ isStaff: true, hasUnmetPrereqs: true, isTooEarly: true }, true);
});
it('passes accessExpirationDate, converted to date', () => {
expect(selected.accessExpirationDate).toEqual(new Date(testData.accessExpirationDate));
});
it('passes [canUpgrade, isAudit, isAuditAccessExpired, isVerified]', () => {
expect(selected.canUpgrade).toEqual(testData.canUpgrade);
expect(selected.isAudit).toEqual(testData.isAudit);
expect(selected.isAuditAccessExpired).toEqual(testData.isAuditAccessExpired);
expect(selected.isVerified).toEqual(testData.isVerified);
});
it('passes isEmailEnabled', () => {
expect(selected.isEmailEnabled).toEqual(testData.isEmailEnabled);
});
});
describe('entitlement selector', () => {
beforeEach(() => {
loadSelector(courseCard.entitlement, {
availableSessions: ['test', 'sessions'],
changeDeadline: '2000-10-20',
isExpired: 'test-is-expired',
isFulfilled: 'test-is-fulfilled',
uuid: 'test-uuid',
});
});
it('returns a card selector based on entilement cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.entitlement);
});
it('returns { isEntilement: false } if entitlement object is missing or empty', () => {
expect(selector({})).toEqual({ isEntitlement: false });
expect(selector()).toEqual({ isEntitlement: false });
});
it('passes isEntitlement: true for entilement data', () => {
expect(selected.isEntitlement).toEqual(true);
});
it('passes [availableSessions, isExpired, isFulfilled, uuid]', () => {
expect(selected.availableSessions).toEqual(testData.availableSessions);
expect(selected.isExpired).toEqual(testData.isExpired);
expect(selected.isFulfilled).toEqual(testData.isFulfilled);
expect(selected.uuid).toEqual(testData.uuid);
});
it('passess changeDeadline as changeDeadline, converted to a date', () => {
expect(selected.changeDeadline).toEqual(new Date(testData.changeDeadline));
});
it('passes hasSessions if availableSessions is provided and has a length > 0', () => {
expect(selected.hasSessions).toEqual(true);
expect(selector({ ...testData, availableSessions: [] }).hasSessions).toEqual(false);
});
it('passes canChange if the deadline is not before current date', () => {
expect(selector({ ...testData, changeDeadline: dates.yesterday }).canChange).toEqual(false);
expect(selector({ ...testData, changeDeadline: dates.tomorrow }).canChange).toEqual(true);
});
it('passes showExpirationWarning if the deadline is 0-6 months in the future', () => {
expect(
selector({ ...testData, changeDeadline: dates.yesterday }).showExpirationWarning,
).toEqual(false);
expect(
selector({ ...testData, changeDeadline: dates.tomorrow }).showExpirationWarning,
).toEqual(true);
expect(
selector({ ...testData, changeDeadline: dates.nextYear }).showExpirationWarning,
).toEqual(false);
});
});
describe('gradeData selector', () => {
beforeEach(() => {
loadSelector(courseCard.gradeData, { isPassing: 'test-is-passing' });
});
it('returns a card selector based on gradeData cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.gradeData);
});
it('passes isPassing', () => {
expect(selected.isPassing).toEqual(testData.isPassing);
});
});
describe('relatedPrograms selector', () => {
beforeEach(() => {
const programData = (index) => ({
bannerImgSrc: `test-banner-img-src-${index}`,
logoImgSrc: `test-logo-img-src-${index}`,
numberOfCourses: `test-number-of-courses-${index}`,
programType: `test-program-type-${index}`,
programUrl: `test-program-url-${index}`,
provider: `test-provider-${index}`,
title: `test-title-${index}`,
});
loadSelector(courseCard.relatedPrograms, [
programData(0),
programData(1),
programData(2),
programData(3),
programData(4),
]);
});
it('returns a card selector based on relatedPrograms cardSimpleSelector', () => {
expect(simpleSelector).toEqual(cardSimpleSelectors.relatedPrograms);
});
it('passes [bannerImgSrc, logoImgSrc] for each program', () => {
selected.list.forEach((row, i) => {
expect(row.bannerImgSrc).toEqual(testData[i].bannerImgSrc);
expect(row.logoImgSrc).toEqual(testData[i].logoImgSrc);
});
});
it('passes [numberOfCourses, programType, programUrl, provider, title] for each', () => {
selected.list.forEach((row, i) => {
expect(row.numberOfCourses).toEqual(testData[i].numberOfCourses);
expect(row.programType).toEqual(testData[i].programType);
expect(row.programUrl).toEqual(testData[i].programUrl);
expect(row.provider).toEqual(testData[i].provider);
expect(row.title).toEqual(testData[i].title);
});
});
it('passes number of programs a length', () => {
expect(selected.length).toEqual(testData.length);
});
});
});
});

View File

@@ -0,0 +1,51 @@
import { StrictDict } from 'utils';
import { FilterKeys, SortKeys } from 'data/constants/app';
import simpleSelectors from './simpleSelectors';
import * as module from './currentList';
export const sortFn = (transform) => (v1, v2) => {
const [a, b] = [v1, v2].map(transform);
if (a === b) { return 0; }
return (a > b) ? 1 : -1;
};
export const courseFilters = StrictDict({
[FilterKeys.notEnrolled]: (course) => !course.enrollment.isEnrolled,
[FilterKeys.done]: (course) => course.enrollment.hasFinished,
[FilterKeys.upgraded]: (course) => course.enrollment.isVerified,
[FilterKeys.inProgress]: (course) => course.enrollment.hasStarted,
[FilterKeys.notStarted]: (course) => !course.enrollment.hasStarted,
});
export const transforms = StrictDict({
[SortKeys.enrolled]: ({ enrollment }) => new Date(enrollment.lastEnrolled),
[SortKeys.title]: ({ course }) => course.courseName.toLowerCase(),
});
export const courseFilterFn = filters => (filters.length
? course => filters.reduce((match, filter) => match && courseFilters[filter](course), true)
: () => true);
export const currentList = (allCourses, {
sortBy,
filters,
}) => allCourses
.filter(module.courseFilterFn(filters))
.sort(module.sortFn(transforms[sortBy]));
export const visibleList = (state, {
sortBy,
filters,
pageSize,
}) => {
const courses = Object.values(simpleSelectors.courseData(state));
const list = module.currentList(courses, { sortBy, filters });
const pageNumber = simpleSelectors.pageNumber(state);
return {
visible: list.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
numPages: Math.ceil(list.length / pageSize),
};
};
export default visibleList;

View File

@@ -0,0 +1,167 @@
import { keyStore } from 'utils';
import { FilterKeys, SortKeys } from 'data/constants/app';
import simpleSelectors from './simpleSelectors';
import * as module from './currentList';
jest.mock('./simpleSelectors', () => ({
__esModule: true,
default: {
courseData: jest.fn(),
pageNumber: jest.fn(),
},
}));
const {
sortFn,
courseFilters,
transforms,
courseFilterFn,
currentList,
visibleList,
} = module;
const moduleKeys = keyStore(module);
const testDate = '2000-10-10';
const testString = 'test-STRING';
describe('courseList selector module', () => {
describe('utilities', () => {
describe('sortFn', () => {
it('performs comparison sort after running both values through transform', () => {
const transform = ({ val }) => val;
expect(sortFn(transform)({ val: 2 }, { val: 1 })).toEqual(1);
expect(sortFn(transform)({ val: 1 }, { val: 1 })).toEqual(0);
expect(sortFn(transform)({ val: 1 }, { val: 2 })).toEqual(-1);
});
});
describe('courseFilters', () => {
let filterFn;
test('notEnrolled returns true iff course is not enrolled', () => {
filterFn = courseFilters[FilterKeys.notEnrolled];
expect(filterFn({ enrollment: { isEnrolled: true } })).toEqual(false);
expect(filterFn({ enrollment: { isEnrolled: false } })).toEqual(true);
});
test('done returns true iff learner has finished course', () => {
filterFn = courseFilters[FilterKeys.done];
expect(filterFn({ enrollment: { hasFinished: true } })).toEqual(true);
expect(filterFn({ enrollment: { hasFinished: false } })).toEqual(false);
});
test('upgraded returns true if learner is verified', () => {
filterFn = courseFilters[FilterKeys.upgraded];
expect(filterFn({ enrollment: { isVerified: true } })).toEqual(true);
expect(filterFn({ enrollment: { isVerified: false } })).toEqual(false);
});
test('inProgress returns true iff learner has started course', () => {
filterFn = courseFilters[FilterKeys.inProgress];
expect(filterFn({ enrollment: { hasStarted: true } })).toEqual(true);
expect(filterFn({ enrollment: { hasStarted: false } })).toEqual(false);
});
test('notStarted returns true iff learner has not started course', () => {
filterFn = courseFilters[FilterKeys.notStarted];
expect(filterFn({ enrollment: { hasStarted: true } })).toEqual(false);
expect(filterFn({ enrollment: { hasStarted: false } })).toEqual(true);
});
});
describe('transforms', () => {
test('enrolled transform returns date transform of enrollment.lastEnrolled', () => {
const data = { enrollment: { lastEnrolled: testDate } };
expect(transforms[SortKeys.enrolled](data)).toEqual(new Date(testDate));
});
test('title transform returns lowercase transform of course.courseName', () => {
const data = { course: { courseName: testString } };
expect(transforms[SortKeys.title](data)).toEqual(testString.toLowerCase());
});
});
describe('courseFilterFn', () => {
it('returns always-true filter if filter list is empty', () => {
expect(courseFilterFn([])()).toEqual(true);
});
it('takes filters and then course, and returns true if course matches all filters', () => {
const filters = [FilterKeys.inProgress, FilterKeys.upgraded];
const inProgressSpy = jest.spyOn(courseFilters, FilterKeys.inProgress);
const upgradedSpy = jest.spyOn(courseFilters, FilterKeys.upgraded);
inProgressSpy.mockImplementation(v => v > 3);
upgradedSpy.mockImplementation(v => v > 5);
expect(courseFilterFn(filters)(6)).toEqual(true);
expect(courseFilterFn(filters)(5)).toEqual(false);
upgradedSpy.mockImplementation(v => v > 2);
expect(courseFilterFn(filters)(3)).toEqual(false);
inProgressSpy.mockRestore();
upgradedSpy.mockRestore();
});
});
describe('currentList selector', () => {
it('returns passed courses filtered and sorted', () => {
const sortSpy = jest.spyOn(module, moduleKeys.sortFn);
const filterSpy = jest.spyOn(module, moduleKeys.courseFilterFn);
filterSpy.mockReturnValue(({ val }) => val > 0);
sortSpy.mockReturnValue((v1, v2) => {
const [a, b] = [v1, v2].map(({ val }) => val);
if (a === b) { return 0; }
return (a > b) ? 1 : -1;
});
const testCourses = {
empty: { val: 0 },
v1: { val: 1 },
v2: { val: 2 },
v3: { val: 3 },
};
const {
empty,
v1,
v2,
v3,
} = testCourses;
const sortBy = SortKeys.enrolled;
const testFilters = [1, 2, 3];
expect(currentList(
[empty, v2, v1, empty, empty, v3, empty],
{ sortBy, filters: testFilters },
)).toEqual([v1, v2, v3]);
expect(sortSpy).toHaveBeenCalledWith(transforms[sortBy]);
expect(filterSpy).toHaveBeenCalledWith(testFilters);
sortSpy.mockRestore();
filterSpy.mockRestore();
});
});
});
describe('visibleList selector', () => {
let listSpy;
let out;
const pageSize = 2;
const pageNumber = 3;
const testState = { some: 'state' };
const sortBy = SortKeys.enrolled;
const testFilters = [1, 2, 3];
const testCourseData = { test: 'course-data' };
const testList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
beforeEach(() => {
listSpy = jest.spyOn(module, moduleKeys.currentList);
listSpy.mockReturnValue(testList);
simpleSelectors.courseData.mockReturnValue(testCourseData);
simpleSelectors.pageNumber.mockReturnValue(pageNumber);
out = visibleList(testState, { sortBy, filters: testFilters, pageSize });
});
afterEach(() => {
listSpy.mockRestore();
});
it('loads currentList from courseData values and passed sortBy and filters values', () => {
expect(simpleSelectors.courseData).toHaveBeenCalledWith(testState);
expect(simpleSelectors.pageNumber).toHaveBeenCalledWith(testState);
expect(listSpy).toHaveBeenCalledWith(
Object.values(testCourseData),
{ sortBy, filters: testFilters },
);
});
it('returns visible page based on passed page size and stored pageNumber', () => {
// page 3, 2 per page. [0 1] [2 3] [4 5] ...
expect(out.visible).toEqual([testList[4], testList[5]]);
});
it('returns number of pages based on page size and list length', () => {
expect(out.numPages).toEqual(6);
});
});
});

View File

@@ -0,0 +1,13 @@
import { StrictDict } from 'utils';
import simpleSelectors from './simpleSelectors';
import appSelectors from './appSelectors';
import courseCard from './courseCard';
import currentList from './currentList';
export default StrictDict({
...simpleSelectors,
...appSelectors,
courseCard,
currentList,
});

View File

@@ -0,0 +1,35 @@
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import * as module from './simpleSelectors';
export const appSelector = (state) => state.app;
const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb);
// top-level app data selectors
export const simpleSelectors = StrictDict({
courseData: mkSimpleSelector(app => app.courseData),
platformSettings: mkSimpleSelector(app => app.platformSettings),
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard),
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
pageNumber: mkSimpleSelector(app => app.pageNumber),
});
export const cardSimpleSelectors = StrictDict({
certificate: ({ certificate }) => certificate,
course: ({ course }) => course,
courseProvider: ({ courseProvider }) => courseProvider,
courseRun: ({ courseRun }) => courseRun,
enrollment: ({ enrollment }) => enrollment,
entitlement: ({ entitlement }) => entitlement,
gradeData: ({ gradeData }) => gradeData,
relatedPrograms: ({ programs: { relatedPrograms } }) => relatedPrograms,
});
export const mkCardSelector = (simpleSelector, selector) => (state, cardId) => (
selector(simpleSelector(module.simpleSelectors.courseData(state)[cardId]))
);
export default simpleSelectors;

View File

@@ -0,0 +1,72 @@
import { keyStore } from 'utils';
import * as module from './simpleSelectors';
const {
appSelector,
simpleSelectors,
cardSimpleSelectors,
mkCardSelector,
} = module;
let keys;
let testData;
let testState;
const testString = 'test-STRING';
const testCardId = 'testCARD-id';
describe('app simple selectors', () => {
describe('base app selector', () => {
});
describe('simple selectors', () => {
keys = keyStore(simpleSelectors);
test.each([
keys.courseData,
keys.platformSettings,
keys.suggestedCourses,
keys.emailConfirmation,
keys.enterpriseDashboard,
keys.selectSessionModal,
keys.pageNumber,
])('%s app simple selector forwards corresponding data from app store', (key) => {
testState = { app: { [key]: testString, otherField: 'fake string' } };
const { preSelectors, cb } = simpleSelectors[key];
expect(preSelectors).toEqual([appSelector]);
expect(cb(testState.app)).toEqual(testString);
});
describe('cardSimpleSelectors', () => {
keys = keyStore(cardSimpleSelectors);
test.each([
keys.certificate,
keys.course,
keys.courseProvider,
keys.courseRun,
keys.enrollment,
keys.entitlement,
keys.gradeData,
])('%s card simple selector forwards corresponding data from passed object', (key) => {
testState = { [key]: testString };
expect(cardSimpleSelectors[key](testState)).toEqual(testString);
});
test('relatedPrograms simple selector forwards relatedPrograms from programs obj', () => {
expect(
cardSimpleSelectors.relatedPrograms({ programs: { relatedPrograms: testString } }),
).toEqual(testString);
});
});
describe('mkCardSelector util', () => {
it('takes [card simpleSelector, selector] and creates card selector from cardData', () => {
const selector = (data) => ({ selector: data });
const simpleSelector = (data) => ({ simpleSelector: data });
testData = { some: 'test data' };
const oldCourseData = simpleSelectors.courseData;
simpleSelectors.courseData = jest.fn().mockReturnValueOnce({ [testCardId]: testData });
expect(mkCardSelector(simpleSelector, selector)(testState, testCardId)).toEqual(
selector(simpleSelector(testData)),
);
expect(simpleSelectors.courseData).toHaveBeenCalledWith(testState);
simpleSelectors.courseData = oldCourseData;
});
});
});
});

View File

@@ -2,10 +2,6 @@ import { RequestStates, RequestKeys } from 'data/constants/requests';
import selectors from './selectors';
jest.mock('reselect', () => ({
createSelector: jest.fn((preSelectors, cb) => ({ preSelectors, cb })),
}));
const requestKey = 'my-test-request-key';
const requestData = { some: 'request-data' };
const inactiveRequest = { status: RequestStates.inactive, some: 'request-data' };

View File

@@ -54,15 +54,15 @@ export const genEntitlementUUID = (index) => `entitlement-course-uuid-${index}`;
const bannerImgSrc = '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg';
const farPastDate = '1900-11-11T00:00:00Z';
const pastDate = '2000-11-11T00:00:00Z';
const futureDate = '3030-11-11T00:00:00Z';
const farFutureDate = '4040-11-11T00:00:00Z';
const soonDate = new Date();
export const farPastDate = '1900-11-11T00:00:00Z';
export const pastDate = '2000-11-11T00:00:00Z';
export const futureDate = '3030-11-11T00:00:00Z';
export const farFutureDate = '4040-11-11T00:00:00Z';
export const soonDate = new Date();
soonDate.setDate(soonDate.getDate() + 60);
const soonDateStr = soonDate.toDateString();
export const soonDateStr = soonDate.toDateString();
const globalData = {
export const globalData = {
emailConfirmation: {
isNeeded: true,
sendEmailUrl: 'sendConfirmation@edx.org',
@@ -252,9 +252,7 @@ export const courseRuns = [
},
// audit, course run and learner started, access expired
{
courseRun: {
courseRun: { isStarted: true },
},
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
isAuditAccessExpired: true,
@@ -263,9 +261,7 @@ export const courseRuns = [
},
// audit, course run and learner started, access expired, cannot upgrade
{
courseRun: {
courseRun: { isStarted: true },
},
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
canUpgrade: false,
@@ -276,10 +272,8 @@ export const courseRuns = [
// audit, course run ended, learner started, expired, cannot upgraded, not passing
{
courseRun: {
courseRun: {
isStarted: true,
endDate: pastDate,
},
isStarted: true,
endDate: pastDate,
},
enrollment: {
accessExpirationDate: pastDate,
@@ -292,11 +286,9 @@ export const courseRuns = [
// audit, course run archived, learner started, expired, cannot upgrade, not passing
{
courseRun: {
courseRun: {
isStarted: true,
isArchived: true,
endDate: pastDate,
},
isStarted: true,
isArchived: true,
endDate: pastDate,
},
enrollment: {
accessExpirationDate: pastDate,
@@ -624,102 +616,107 @@ export const entitlementCourses = [
},
];
export const compileCourseRunData = (data, index) => {
const courseName = genCourseTitle(index);
const courseId = genCourseId(index);
const courseNumber = genCourseNumber(index);
const providerIndex = index % 3;
const lastEnrolledDate = new Date();
lastEnrolledDate.setDate(lastEnrolledDate.getDate() + index);
const lastEnrolled = lastEnrolledDate.toISOString();
const iteratedData = [
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: false, hasOptedOutOfEmail: false },
programs: { relatedPrograms },
courseProvider: providers.edx,
},
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: true, hasOptedOutOfEmail: false },
courseProvider: providers.mit,
programs: { relatedPrograms: [relatedPrograms[0]] },
},
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: true, hasOptedOutOfEmail: true },
courseProvider: null,
programs: { relatedPrograms: [] },
},
];
const out = {
gradeData: { isPassing: true },
entitlement: null,
...data,
certificate: genCertificateData(data.certificate),
enrollment: genEnrollmentData({ lastEnrolled, ...data.enrollment }),
courseRun: genCourseRunData({
...data.courseRun,
...iteratedData.emailSettings,
courseId,
}),
courseProvider: iteratedData[providerIndex].courseProvider,
course: iteratedData[providerIndex].course,
programs: iteratedData[providerIndex].programs,
};
if (out.enrollment.canUpgrade) {
out.courseRun.upgradeUrl = 'test-upgrade-url';
}
return out;
};
export const compileEntitlementData = (data, index) => {
const courseName = genCourseTitle(100 + index);
const courseNumber = genCourseNumber(100 + index);
const providerIndex = index % 3;
const iteratedData = [
{
courseProvider: providers.edx,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms },
},
{
courseProvider: providers.mit,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms: [relatedPrograms[0]] },
},
{
courseProvider: null,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms: [] },
},
];
return {
enrollment: genEnrollmentData({
isEnrolled: false,
lastEnrolled: null,
accessExpirationDate: null,
canUpgrade: false,
hasFinished: false,
hasStarted: false,
isAudit: false,
isAuditAccessExpired: false,
isEmailEnabled: false,
isVerified: false,
}),
gradeData: null,
certificate: null,
courseRun: null,
...data,
...iteratedData[providerIndex],
};
};
// Entitlement Course - refundable
// Entitlement Course - cannot view yet
// Entitlement Course - can view and change
// Entitlement Course - expired
export const courseRunData = courseRuns.map(
(data, index) => {
const courseName = genCourseTitle(index);
const courseId = genCourseId(index);
const courseNumber = genCourseNumber(index);
const providerIndex = index % 3;
const lastEnrolledDate = new Date();
lastEnrolledDate.setDate(lastEnrolledDate.getDate() - index);
const lastEnrolled = lastEnrolledDate.toISOString();
const iteratedData = [
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: false, hasOptedOutOfEmail: false },
programs: { relatedPrograms },
courseProvider: providers.edx,
},
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: true, hasOptedOutOfEmail: false },
courseProvider: providers.mit,
programs: { relatedPrograms: [relatedPrograms[0]] },
},
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: true, hasOptedOutOfEmail: true },
courseProvider: null,
programs: { relatedPrograms: [] },
},
];
return {
gradeData: { isPassing: true },
entitlement: null,
...data,
certificate: genCertificateData(data.certificate),
enrollment: genEnrollmentData({ lastEnrolled, ...data.enrollment }),
courseRun: genCourseRunData({
...data.courseRun,
...iteratedData.emailSettings,
courseId,
}),
courseProvider: iteratedData[providerIndex].courseProvider,
course: iteratedData[providerIndex].course,
programs: iteratedData[providerIndex].programs,
};
},
);
export const entitlementData = entitlementCourses.map(
(data, index) => {
const courseName = genCourseTitle(100 + index);
const courseNumber = genCourseNumber(100 + index);
const providerIndex = index % 3;
const iteratedData = [
{
courseProvider: providers.edx,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms },
},
{
courseProvider: providers.mit,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms: [relatedPrograms[0]] },
},
{
courseProvider: null,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms: [] },
},
];
return {
enrollment: genEnrollmentData({
isEnrolled: false,
lastEnrolled: null,
accessExpirationDate: null,
canUpgrade: false,
hasFinished: false,
hasStarted: false,
isAudit: false,
isAuditAccessExpired: false,
isEmailEnabled: false,
isVerified: false,
}),
gradeData: null,
certificate: null,
courseRun: null,
...data,
...iteratedData[providerIndex],
};
},
);
export const courseRunData = courseRuns.map(compileCourseRunData);
export const entitlementData = entitlementCourses.map(compileEntitlementData);
export default {
compileEntitlementData,
compileCourseRunData,
courseRunData,
entitlementData,
globalData,

View File

@@ -1,93 +0,0 @@
import PropTypes from 'prop-types';
import { StrictDict, keyStore } from 'utils';
export const shapes = StrictDict({
course: PropTypes.shape({
bannerImgSrc: PropTypes.string,
title: PropTypes.string,
website: PropTypes.string,
}),
provider: PropTypes.shape({
email: PropTypes.string,
name: PropTypes.string,
website: PropTypes.string,
}),
courseRun: PropTypes.shape({
accessExpirationDate: PropTypes.string,
cardId: PropTypes.string,
isArchived: PropTypes.bool,
isFinished: PropTypes.bool,
isPending: PropTypes.bool,
isStarted: PropTypes.bool,
minPassingGrade: PropTypes.number,
}),
certificate: PropTypes.shape({
availableDate: PropTypes.string,
downloadUrls: PropTypes.shape({
preview: PropTypes.string,
download: PropTypes.string,
}),
isDownloadable: PropTypes.bool,
isEarned: PropTypes.bool,
isRestricted: PropTypes.bool,
}),
enrollment: PropTypes.shape({
canUpgrade: PropTypes.bool,
isAudit: PropTypes.bool,
isAuditAccessExpired: PropTypes.bol,
isVerified: PropTypes.bool,
}),
entitlement: PropTypes.shape({
isEntitlement: PropTypes.bool.isRequired,
availableSessions: PropTypes.shape({
startDate: PropTypes.string,
endDate: PropTypes.string,
cardId: PropTypes.string,
}),
canChange: PropTypes.bool,
isFulfilled: PropTypes.bool,
isRefundable: PropTypes.bool,
changeDeadline: PropTypes.string,
isExpired: PropTypes.bool,
canViewCourse: PropTypes.bool,
}),
gradeData: PropTypes.shape({
isPassing: PropTypes.bool,
}),
programCard: PropTypes.shape({
provider: PropTypes.string,
bannerImgSrc: PropTypes.string,
logoImgSrc: PropTypes.string,
title: PropTypes.string,
programUrl: PropTypes.string,
programType: PropTypes.string,
programTypeUrl: PropTypes.string,
numberOfCourses: PropTypes.number,
estimatedNumberOfWeeks: PropTypes.number,
}),
});
shapes.courseRunCardData = PropTypes.shape({
course: shapes.course,
provider: shapes.provider,
courseRun: shapes.courseRun,
certificate: shapes.certificate,
enrollment: shapes.enrollment,
entitlement: shapes.entitlement,
gradeData: shapes.gradeData,
relatedPrograms: PropTypes.arrayOf(shapes.programCard),
});
export const keys = StrictDict({
cardData: keyStore(shapes.courseRunCardData),
course: keyStore(shapes.course),
provider: keyStore(shapes.provider),
courseRun: keyStore(shapes.courseRun),
certificate: keyStore(shapes.certificate),
enrollment: keyStore(shapes.enrollment),
entitlement: keyStore(shapes.entitlement),
gradeData: keyStore(shapes.gradeData),
program: keyStore(shapes.programCard),
});
export default shapes;

View File

@@ -16,6 +16,32 @@ jest.mock('react', () => ({
useContext: jest.fn(context => context),
}));
jest.mock('reselect', () => ({
createSelector: jest.fn((preSelectors, cb) => ({ preSelectors, cb })),
}));
// Mock react-redux hooks
// unmock for integration tests
jest.mock('react-redux', () => {
const dispatch = jest.fn((...args) => ({ dispatch: args })).mockName('react-redux.dispatch');
return {
connect: (mapStateToProps, mapDispatchToProps) => (component) => ({
mapStateToProps,
mapDispatchToProps,
component,
}),
useDispatch: jest.fn(() => dispatch),
useSelector: jest.fn((selector) => ({ useSelector: selector })),
};
});
jest.mock('moment', () => ({
__esModule: true,
default: (date) => ({
toDate: jest.fn().mockReturnValue(date),
}),
}));
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
const PropTypes = jest.requireActual('prop-types');
@@ -188,30 +214,3 @@ jest.mock('hooks', () => ({
...jest.requireActual('hooks'),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
}));
// Mock react-redux hooks
// unmock for integration tests
jest.mock('react-redux', () => {
const dispatch = jest.fn((...args) => ({ dispatch: args })).mockName('react-redux.dispatch');
return {
connect: (mapStateToProps, mapDispatchToProps) => (component) => ({
mapStateToProps,
mapDispatchToProps,
component,
}),
useDispatch: jest.fn(() => dispatch),
useSelector: jest.fn((selector) => ({ useSelector: selector })),
};
});
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

@@ -15,7 +15,7 @@ import thunk from 'redux-thunk';
import { useIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import api from 'data/services/lms/api';
import fakeData from 'data/services/lms/fakeData/courses';
import * as fakeData from 'data/services/lms/fakeData/courses';
import { RequestKeys, RequestStates } from 'data/constants/requests';
import reducers from 'data/redux';
import messages from 'i18n';
@@ -32,6 +32,7 @@ jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@edx/frontend-component-footer');
jest.unmock('react');
jest.unmock('react-redux');
jest.unmock('reselect');
jest.unmock('hooks');
jest.mock('@edx/frontend-platform/i18n', () => ({
@@ -83,19 +84,27 @@ const mockForbiddenError = (reject) => () => reject(new Error({
response: { status: ErrorStatuses.forbidden },
}));
const allCourses = [
...fakeData.courseRunData,
...fakeData.entitlementData,
];
const { compileCourseRunData, compileEntitlementData } = fakeData;
const initCourses = jest.fn(() => []);
const mockApi = () => {
api.initializeList = jest.fn(() => new Promise(
(resolve, reject) => {
resolveFns.init = {
success: () => resolve({
data: {
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
success: () => {
const data = {
courses: initCourses(),
...fakeData.globalData,
}
}),
};
resolve({ data });
},
};
}));
};
@@ -124,51 +133,94 @@ const waitForRequestStatus = (key, status) => waitForEqual(
key,
);
const loadApp = async (courses) => {
initCourses.mockReturnValue(courses);
await renderEl();
inspector = new Inspector(el);
}
describe('ESG app integration tests', () => {
beforeEach(async () => {
beforeEach(() => {
mockApi();
await renderEl();
inspector = new Inspector(el);
});
test('initialization', async () => {
await loadApp([compileCourseRunData({}, 0)]);
await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
});
test('course cards', async () => {
const { formatDate } = useIntl();
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
await inspector.findByText(fakeData.courseRunData[0].course.courseName);
const cards = inspector.get.courseCards;
describe('course cards', () => {
const loadCourses = async (courses, tests) => {
await loadApp(courses);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
await getState();
const cards = inspector.get.courseCards;
courses.forEach((course, index) => {
const card = cards.at(index);
const cardId = genCardId(index);
const cardDetails = inspector.get.card.details(card);
const { courseName } = selectors.app.courseCard.course(state, cardId);
inspector.verifyText(inspector.get.card.header(card), courseName);
if (tests.length > index) {
tests[index]({ cardId, cardDetails });
}
});
}
let cardId;
let courseData;
let cardDetails;
await getState();
// Card 1 is Audit, pending, and can upgrade
cardId = genCardId(0);
courseData = state.app.courseData[cardId];
expect(courseData.enrollment.isAudit).toEqual(true);
expect(courseData.courseRun.isStarted).toEqual(false);
expect(courseData.enrollment.canUpgrade).toEqual(true);
let card = cards.at(0);
inspector.verifyText(
inspector.get.card.header(card),
courseData.course.courseName,
);
cardDetails = inspector.get.card.details(card);
[
courseData.courseProvider.name,
courseData.course.courseNumber,
appMessages.withValues.CourseCardDetails.courseStarts({
startDate: formatDate(new Date(courseData.courseRun.startDate)),
}),
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));
test('audit', async () => {
const courses = [
{}, // audit, course run not started
{
enrollment: {
coursewareAccess: {
isTooEarly: true,
hasUnmetPrerequisites: false,
isStaff: false,
},
},
}, // audit, course run not started, is too early
{
courseRun: {
courseRun: { isStarted: true },
},
enrollment: {
accessExpirationDate: fakeData.pastDate,
canUpgrade: false,
isAuditAccessExpired: true,
hasStarted: true,
},
}, // audit, course run and learner started, access expired, cannot upgrade
];
const { formatDate } = useIntl();
await loadCourses(courses.map(compileCourseRunData), [
({ cardId, cardDetails }) => {
const enrollment = selectors.app.courseCard.enrollment(state, cardId);
const courseRun = selectors.app.courseCard.courseRun(state, cardId);
const courseProvider = selectors.app.courseCard.courseProvider(state, cardId);
const course = selectors.app.courseCard.course(state, cardId);
expect(enrollment.isAudit).toEqual(true);
expect(courseRun.isStarted).toEqual(false);
expect(enrollment.canUpgrade).toEqual(true);
[
courseProvider.name,
course.courseNumber,
appMessages.withValues.CourseCardDetails.courseStarts({
startDate: formatDate(new Date(courseRun.startDate)),
}),
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));
},
({ cardId, cardDetails }) => {
const enrollment = selectors.app.courseCard.enrollment(state, cardId);
const courseRun = selectors.app.courseCard.courseRun(state, cardId);
expect(enrollment.isAudit).toEqual(true);
expect(courseRun.isStarted).toEqual(false);
expect(enrollment.coursewareAccess.isTooEarly).toEqual(true);
expect(enrollment.hasAccess).toEqual(false);
},
]);
});
});
});