Bw/selector tests (#35)
This commit is contained in:
@@ -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,
|
||||
});
|
||||
27
src/data/redux/app/selectors/appSelectors.js
Normal file
27
src/data/redux/app/selectors/appSelectors.js
Normal 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,
|
||||
});
|
||||
36
src/data/redux/app/selectors/appSelectors.test.js
Normal file
36
src/data/redux/app/selectors/appSelectors.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
136
src/data/redux/app/selectors/courseCard.js
Normal file
136
src/data/redux/app/selectors/courseCard.js
Normal 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;
|
||||
346
src/data/redux/app/selectors/courseCard.test.js
Normal file
346
src/data/redux/app/selectors/courseCard.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
51
src/data/redux/app/selectors/currentList.js
Normal file
51
src/data/redux/app/selectors/currentList.js
Normal 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;
|
||||
167
src/data/redux/app/selectors/currentList.test.js
Normal file
167
src/data/redux/app/selectors/currentList.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
13
src/data/redux/app/selectors/index.js
Normal file
13
src/data/redux/app/selectors/index.js
Normal 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,
|
||||
});
|
||||
35
src/data/redux/app/selectors/simpleSelectors.js
Normal file
35
src/data/redux/app/selectors/simpleSelectors.js
Normal 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;
|
||||
72
src/data/redux/app/selectors/simpleSelectors.test.js
Normal file
72
src/data/redux/app/selectors/simpleSelectors.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user