diff --git a/src/containers/CourseList/hooks.js b/src/containers/CourseList/hooks.js
index 6804cc5..f8bc737 100644
--- a/src/containers/CourseList/hooks.js
+++ b/src/containers/CourseList/hooks.js
@@ -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,
});
diff --git a/src/containers/CourseList/hooks.test.js b/src/containers/CourseList/hooks.test.js
index 9ff166c..7a90ab5 100644
--- a/src/containers/CourseList/hooks.test.js
+++ b/src/containers/CourseList/hooks.test.js
@@ -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,
});
diff --git a/src/containers/EnterpriseDashboardModal/index.jsx b/src/containers/EnterpriseDashboardModal/index.jsx
index aaf6375..3a7bf44 100644
--- a/src/containers/EnterpriseDashboardModal/index.jsx
+++ b/src/containers/EnterpriseDashboardModal/index.jsx
@@ -16,7 +16,7 @@ export const EnterpriseDashboardModal = () => {
handleClick,
dashboard,
} = useEnterpriseDashboardHook();
- if (!dashboard) {
+ if (!dashboard || !dashboard.label) {
return null;
}
return (
diff --git a/src/containers/SelectSessionModal/hooks.js b/src/containers/SelectSessionModal/hooks.js
index 02434be..1e1aaf1 100644
--- a/src/containers/SelectSessionModal/hooks.js
+++ b/src/containers/SelectSessionModal/hooks.js
@@ -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,
diff --git a/src/containers/SelectSessionModal/hooks.test.js b/src/containers/SelectSessionModal/hooks.test.js
index 1b03599..e211a15 100644
--- a/src/containers/SelectSessionModal/hooks.test.js
+++ b/src/containers/SelectSessionModal/hooks.test.js
@@ -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' },
diff --git a/src/containers/SelectSessionModal/index.jsx b/src/containers/SelectSessionModal/index.jsx
index 31299f4..8d95a61 100644
--- a/src/containers/SelectSessionModal/index.jsx
+++ b/src/containers/SelectSessionModal/index.jsx
@@ -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) => (
{dateFormatter(formatDate, session.startDate)} - {dateFormatter(formatDate, session.endDate)}
diff --git a/src/containers/SelectSessionModal/index.test.jsx b/src/containers/SelectSessionModal/index.test.jsx
index f90cc58..cf6f98c 100644
--- a/src/containers/SelectSessionModal/index.test.jsx
+++ b/src/containers/SelectSessionModal/index.test.jsx
@@ -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()).toMatchSnapshot();
});
@@ -44,7 +44,7 @@ describe('SelectSessionModal', () => {
test('modal without leave option ', () => {
hooks.mockReturnValueOnce({
...hookReturn,
- entitlementSessions: [...availableSessions],
+ availableSessions,
showLeaveOption: false,
});
expect(shallow()).toMatchSnapshot();
diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js
deleted file mode 100644
index 243b65a..0000000
--- a/src/data/redux/app/selectors.js
+++ /dev/null
@@ -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,
-});
diff --git a/src/data/redux/app/selectors/appSelectors.js b/src/data/redux/app/selectors/appSelectors.js
new file mode 100644
index 0000000..10a85fb
--- /dev/null
+++ b/src/data/redux/app/selectors/appSelectors.js
@@ -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,
+});
diff --git a/src/data/redux/app/selectors/appSelectors.test.js b/src/data/redux/app/selectors/appSelectors.test.js
new file mode 100644
index 0000000..ef56208
--- /dev/null
+++ b/src/data/redux/app/selectors/appSelectors.test.js
@@ -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);
+ });
+ });
+});
diff --git a/src/data/redux/app/selectors/courseCard.js b/src/data/redux/app/selectors/courseCard.js
new file mode 100644
index 0000000..0d7e23a
--- /dev/null
+++ b/src/data/redux/app/selectors/courseCard.js
@@ -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;
diff --git a/src/data/redux/app/selectors/courseCard.test.js b/src/data/redux/app/selectors/courseCard.test.js
new file mode 100644
index 0000000..4483cb5
--- /dev/null
+++ b/src/data/redux/app/selectors/courseCard.test.js
@@ -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);
+ });
+ });
+ });
+});
diff --git a/src/data/redux/app/selectors/currentList.js b/src/data/redux/app/selectors/currentList.js
new file mode 100644
index 0000000..b5fbbfd
--- /dev/null
+++ b/src/data/redux/app/selectors/currentList.js
@@ -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;
diff --git a/src/data/redux/app/selectors/currentList.test.js b/src/data/redux/app/selectors/currentList.test.js
new file mode 100644
index 0000000..9db1461
--- /dev/null
+++ b/src/data/redux/app/selectors/currentList.test.js
@@ -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);
+ });
+ });
+});
diff --git a/src/data/redux/app/selectors/index.js b/src/data/redux/app/selectors/index.js
new file mode 100644
index 0000000..a2cc581
--- /dev/null
+++ b/src/data/redux/app/selectors/index.js
@@ -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,
+});
diff --git a/src/data/redux/app/selectors/simpleSelectors.js b/src/data/redux/app/selectors/simpleSelectors.js
new file mode 100644
index 0000000..8947f9b
--- /dev/null
+++ b/src/data/redux/app/selectors/simpleSelectors.js
@@ -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;
diff --git a/src/data/redux/app/selectors/simpleSelectors.test.js b/src/data/redux/app/selectors/simpleSelectors.test.js
new file mode 100644
index 0000000..38db5f0
--- /dev/null
+++ b/src/data/redux/app/selectors/simpleSelectors.test.js
@@ -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;
+ });
+ });
+ });
+});
diff --git a/src/data/redux/requests/selectors.test.js b/src/data/redux/requests/selectors.test.js
index 0c9e3a4..d1a8bde 100644
--- a/src/data/redux/requests/selectors.test.js
+++ b/src/data/redux/requests/selectors.test.js
@@ -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' };
diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js
index 1cc065c..dedebf0 100644
--- a/src/data/services/lms/fakeData/courses.js
+++ b/src/data/services/lms/fakeData/courses.js
@@ -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,
diff --git a/src/data/services/lms/shapes.js b/src/data/services/lms/shapes.js
deleted file mode 100644
index 9ff6ad4..0000000
--- a/src/data/services/lms/shapes.js
+++ /dev/null
@@ -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;
diff --git a/src/setupTest.jsx b/src/setupTest.jsx
index 1acd9a5..b1c6088 100755
--- a/src/setupTest.jsx
+++ b/src/setupTest.jsx
@@ -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),
- }),
-}));
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index 5c65b93..7746de7 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -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);
+ },
+ ]);
+ });
});
});