({
jest.mock('data/redux', () => ({
hooks: {
useCardCourseData: jest.fn(),
+ useCardEnrollmentData: jest.fn(),
useCardSocialSettingsData: jest.fn(),
useMasqueradeData: jest.fn(),
},
@@ -47,6 +48,7 @@ const defaultSocialShare = {
};
const courseName = 'test-course-name';
let wrapper;
+let el;
describe('CourseCardMenu', () => {
beforeEach(() => {
@@ -54,49 +56,74 @@ describe('CourseCardMenu', () => {
useUnenrollData.mockReturnValue(defaultUnenrollModal);
appHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare);
appHooks.useCardCourseData.mockReturnValue({ courseName });
+ appHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true });
appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
});
- test('snapshot', () => {
- wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- // expect(wrapper.find('FacebookShareButton').length).toEqual(1);
- expect(wrapper.find('TwitterShareButton').length).toEqual(1);
- expect(wrapper.find({
- 'data-testid': 'unenrollModalToggle',
- }).props().disabled).toEqual(false);
- expect(wrapper.find({
- 'data-testid': 'emailSettingsModalToggle',
- }).props().disabled).toEqual(false);
- });
- test('snapshot: masquerading', () => {
- appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
- wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.find({
- 'data-testid': 'unenrollModalToggle',
- }).props().disabled).toEqual(true);
- expect(wrapper.find({
- 'data-testid': 'emailSettingsModalToggle',
- }).props().disabled).toEqual(true);
- });
- /*
- test('facebook share disabled', () => {
- appHooks.useCardSocialSettingsData.mockReturnValueOnce({
- ...defaultSocialShare,
- facebook: { ...defaultSocialShare.facebook, isEnabled: false },
+ describe('enrolled, share enabled', () => {
+ beforeEach(() => {
+ wrapper = shallow();
});
- wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('FacebookShareButton').length).toEqual(0);
- });
- */
- test('twitter share disabled', () => {
- appHooks.useCardSocialSettingsData.mockReturnValueOnce({
- ...defaultSocialShare,
- twitter: { ...defaultSocialShare.twitter, isEnabled: false },
+ test('snapshot', () => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ it('renders share buttons', () => {
+ // expect(wrapper.find('FacebookShareButton').length).toEqual(1);
+ expect(wrapper.find('TwitterShareButton').length).toEqual(1);
+ });
+ it('renders enabled unenroll modal toggle', () => {
+ el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
+ expect(el.props().disabled).toEqual(false);
+ });
+ it('renders enabled email settings modal toggle', () => {
+ el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
+ expect(el.props().disabled).toEqual(false);
+ });
+ });
+ describe('not enrolled, share disabled', () => {
+ beforeEach(() => {
+ appHooks.useCardSocialSettingsData.mockReturnValueOnce({
+ ...defaultSocialShare,
+ twitter: { ...defaultSocialShare.twitter, isEnabled: false },
+ // facebook: { ...defaultSocialShare.facebook, isEnabled: false },
+ });
+ appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
+ wrapper = shallow();
+ });
+ test('snapshot', () => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ it('renders share buttons', () => {
+ // expect(wrapper.find('FacebookShareButton').length).toEqual(0);
+ expect(wrapper.find('TwitterShareButton').length).toEqual(0);
+ });
+ it('does not render unenroll modal toggle', () => {
+ el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
+ expect(el.length).toEqual(0);
+ });
+ it('renders enabled email settings modal toggle', () => {
+ el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
+ expect(el.props().disabled).toEqual(false);
+ });
+ });
+ describe('masquerading', () => {
+ beforeEach(() => {
+ appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
+ wrapper = shallow();
+ });
+ test('snapshot', () => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ it('renders share buttons', () => {
+ // expect(wrapper.find('FacebookShareButton').length).toEqual(1);
+ expect(wrapper.find('TwitterShareButton').length).toEqual(1);
+ });
+ it('renders disabled unenroll modal toggle', () => {
+ el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
+ expect(el.props().disabled).toEqual(true);
+ });
+ it('renders disabled email settings modal toggle', () => {
+ el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
+ expect(el.props().disabled).toEqual(true);
});
- wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('TwitterShareButton').length).toEqual(0);
});
});
diff --git a/src/containers/CourseList/hooks.test.js b/src/containers/CourseList/hooks.test.js
index c22c13b..b7ea33f 100644
--- a/src/containers/CourseList/hooks.test.js
+++ b/src/containers/CourseList/hooks.test.js
@@ -15,7 +15,6 @@ jest.mock('data/redux', () => ({
hooks: {
useCurrentCourseList: jest.fn(),
usePageNumber: jest.fn(() => 23),
- useIsPendingRequest: jest.fn(),
},
}));
@@ -37,7 +36,6 @@ describe('CourseList hooks', () => {
let out;
appHooks.useCurrentCourseList.mockReturnValue(testListData);
- appHooks.useIsPendingRequest.mockReturnValue(false);
paragon.useCheckboxSetValues.mockImplementation(() => testCheckboxSetValues);
describe('state values', () => {
diff --git a/src/containers/Dashboard/index.jsx b/src/containers/Dashboard/index.jsx
index e75e3b2..dd8bc49 100644
--- a/src/containers/Dashboard/index.jsx
+++ b/src/containers/Dashboard/index.jsx
@@ -17,7 +17,7 @@ export const Dashboard = () => {
const { pageTitle } = hooks.useDashboardMessages();
const hasCourses = appHooks.useHasCourses();
const hasAvailableDashboards = appHooks.useHasAvailableDashboards();
- const initIsPending = appHooks.useIsPendingRequest(RequestKeys.initialize);
+ const initIsPending = appHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = appHooks.useShowSelectSessionModal();
return (
diff --git a/src/containers/Dashboard/index.test.jsx b/src/containers/Dashboard/index.test.jsx
index 96444da..dd73ef8 100644
--- a/src/containers/Dashboard/index.test.jsx
+++ b/src/containers/Dashboard/index.test.jsx
@@ -21,7 +21,7 @@ jest.mock('data/redux', () => ({
useHasCourses: jest.fn(),
useHasAvailableDashboards: jest.fn(),
useShowSelectSessionModal: jest.fn(),
- useIsPendingRequest: jest.fn(),
+ useRequestIsPending: jest.fn(),
},
}));
@@ -49,7 +49,7 @@ describe('Dashboard', () => {
}) => {
appHooks.useHasCourses.mockReturnValueOnce(hasCourses);
appHooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
- appHooks.useIsPendingRequest.mockReturnValueOnce(initIsPending);
+ appHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
appHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
return shallow();
};
diff --git a/src/containers/UnenrollConfirmModal/hooks.js b/src/containers/UnenrollConfirmModal/hooks.js
index 2839edd..bfaea80 100644
--- a/src/containers/UnenrollConfirmModal/hooks.js
+++ b/src/containers/UnenrollConfirmModal/hooks.js
@@ -47,8 +47,10 @@ export const useUnenrollReasons = ({
selectOption: useValueCallback(setSelectedReason),
isSkipped,
- skip: React.useCallback(() => setIsSkipped(true), [setIsSkipped]),
-
+ skip: React.useCallback(() => {
+ setIsSkipped(true);
+ dispatch(thunkActions.app.unenrollFromCourse(cardId));
+ }, [cardId, dispatch, setIsSkipped]),
isSubmitted: isSkipped,
submit: React.useCallback(() => {
const submittedReason = selectedReason === 'custom' ? customOption : selectedReason;
diff --git a/src/containers/UnenrollConfirmModal/hooks.test.js b/src/containers/UnenrollConfirmModal/hooks.test.js
index b5f51a8..bf041cb 100644
--- a/src/containers/UnenrollConfirmModal/hooks.test.js
+++ b/src/containers/UnenrollConfirmModal/hooks.test.js
@@ -10,6 +10,7 @@ jest.mock('hooks', () => ({
jest.mock('data/redux/thunkActions/app', () => ({
refreshList: jest.fn((args) => ({ refreshList: args })),
+ unenrollFromCourse: jest.fn((...args) => ({ unenrollFromCourse: args })),
}));
const state = new MockUseState(hooks);
@@ -70,11 +71,12 @@ describe('UnenrollConfirmModal hooks', () => {
state.mockVal(state.keys.isSkipped, testValue);
expect(createUseUnenrollReasons().isSkipped).toEqual(testValue);
});
- test('skip returns callback that sets isSkipped to true', () => {
+ test('skip returns callback that sets isSkipped to true and unenrolls with no reason', () => {
const { cb, prereqs } = out.skip.useCallback;
- expect(prereqs).toEqual([state.setState.isSkipped]);
+ expect(prereqs).toEqual([cardId, dispatch, state.setState.isSkipped]);
cb();
expect(state.setState.isSkipped).toHaveBeenCalledWith(true);
+ expect(dispatch).toHaveBeenCalledWith(thunkActions.app.unenrollFromCourse(cardId));
});
describe('isSubmitted', () => {
it('returns false if submittedReason is null and not isSkipped', () => {
@@ -86,14 +88,28 @@ describe('UnenrollConfirmModal hooks', () => {
});
});
describe('submit', () => {
+ const customValue = 'custom-value';
+ const loadHook = ({ selectedReason, customOption }) => {
+ state.mockVal(state.keys.selectedReason, selectedReason);
+ state.mockVal(state.keys.customOption, customOption);
+ return createUseUnenrollReasons().submit.useCallback;
+ };
it('depends on customOption and selectedReason', () => {
- const customValue = 'custom-value';
- state.mockVal(state.keys.selectedReason, testValue);
- state.mockVal(state.keys.customOption, customValue);
- const { prereqs } = createUseUnenrollReasons().submit.useCallback;
+ const { prereqs } = loadHook({ selectedReason: testValue, customOption: customValue });
expect(prereqs).toContain(testValue);
expect(prereqs).toContain(customValue);
});
+ it('dispatches unenroll action with submitted reason', () => {
+ loadHook({ selectedReason: testValue, customOption: customValue }).cb();
+ expect(dispatch).toHaveBeenCalledWith(
+ thunkActions.app.unenrollFromCourse(cardId, testValue),
+ );
+ dispatch.mockClear();
+ loadHook({ selectedReason: 'custom', customOption: customValue }).cb();
+ expect(dispatch).toHaveBeenCalledWith(
+ thunkActions.app.unenrollFromCourse(cardId, customValue),
+ );
+ });
});
});
describe('modalHooks', () => {
diff --git a/src/data/redux/app/selectors/courseCard.js b/src/data/redux/app/selectors/courseCard.js
index afafd38..bc01cad 100644
--- a/src/data/redux/app/selectors/courseCard.js
+++ b/src/data/redux/app/selectors/courseCard.js
@@ -1,10 +1,9 @@
import { StrictDict } from 'utils';
-import urls from 'data/services/lms/urls';
+import { baseAppUrl, learningMfeUrl } 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();
@@ -21,7 +20,7 @@ export const courseCard = StrictDict({
const isAvailable = availableDate <= new Date();
return {
availableDate,
- certPreviewUrl: certificate.certPreviewUrl,
+ certPreviewUrl: baseAppUrl(certificate.certPreviewUrl),
isDownloadable: certificate.isDownloadable,
isEarnedButUnavailable: certificate.isEarned && !isAvailable,
isRestricted: certificate.isRestricted,
@@ -96,7 +95,11 @@ export const courseCard = StrictDict({
}
const deadline = new Date(entitlement.changeDeadline);
const deadlinePassed = deadline < today;
- const showExpirationWarning = !deadlinePassed && deadline <= dateSixMonthsFromNow;
+ const showExpirationWarning = (
+ !entitlement.isFulfilled
+ && !deadlinePassed
+ && deadline <= dateSixMonthsFromNow
+ );
return {
isEntitlement: true,
diff --git a/src/data/redux/app/selectors/courseCard.test.js b/src/data/redux/app/selectors/courseCard.test.js
index 0af5220..b6fcd45 100644
--- a/src/data/redux/app/selectors/courseCard.test.js
+++ b/src/data/redux/app/selectors/courseCard.test.js
@@ -84,11 +84,13 @@ describe('courseCard selectors module', () => {
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);
+ it('passes [isDownloadable, isRestricted]', () => {
expect(selected.isDownloadable).toEqual(testData.isDownloadable);
expect(selected.isRestricted).toEqual(testData.isRestricted);
});
+ it('passes certPreviewUrl as app url', () => {
+ expect(selected.certPreviewUrl).toEqual(baseAppUrl(testData.certPreviewUrl));
+ });
describe('isEarnedButUnavailable', () => {
it('passes true iff certificate is earned but availableDate is in the future', () => {
const testSelector = (data, expected) => {
@@ -278,16 +280,16 @@ describe('courseCard selectors module', () => {
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);
+ it('passes showExpirationWarning if the deadline is 0-6 months in the future and not fulfilled', () => {
+ const testSelector = ({ isFulfilled, changeDeadline }, expected) => {
+ expect(
+ selector({ ...testData, isFulfilled, changeDeadline }).showExpirationWarning,
+ ).toEqual(expected);
+ };
+ testSelector({ isFulfilled: false, changeDeadline: dates.yesterday }, false);
+ testSelector({ isFulfilled: false, changeDeadline: dates.tomorrow }, true);
+ testSelector({ isFulfilled: false, changeDeadline: dates.nextYear }, false);
+ testSelector({ isFulfilled: true, changeDeadline: dates.nextYear }, false);
});
});
describe('gradeData selector', () => {
diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js
index c3dda86..68d67e8 100644
--- a/src/data/redux/hooks.js
+++ b/src/data/redux/hooks.js
@@ -64,4 +64,5 @@ export const useUpdateSelectSessionModalCallback = (dispatch, cardId) => () => d
export const useMasqueradeData = () => useSelector(requestSelectors.masquerade);
-export const useIsPendingRequest = (requestName) => useSelector(requestSelectors.isPending(requestName));
+export const useRequestIsPending = (requestName) => useSelector(requestSelectors.isPending(requestName));
+export const useRequestIsFailed = (requestName) => useSelector(requestSelectors.isFailed(requestName));
diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js
index 303a78f..14242d6 100644
--- a/src/data/services/lms/urls.js
+++ b/src/data/services/lms/urls.js
@@ -12,8 +12,10 @@ const courseUnenroll = `${baseUrl}/change_enrollment`;
const updateEmailSettings = `${api}/change_email_settings`;
const entitlementEnrollment = (uuid) => `${api}/entitlements/v1/entitlements/${uuid}/enrollments`;
-const baseAppUrl = (url) => baseUrl + url;
-const learningMfeUrl = (url) => configuration.LEARNING_MICROFRONTEND_URL + url;
+const isAbsoluteUrl = (url) => url.startsWith('http://') || url.startsWith('https://');
+
+export const baseAppUrl = (url) => (isAbsoluteUrl(url) ? url : baseUrl + url);
+export const learningMfeUrl = (url) => (isAbsoluteUrl(url) ? url : configuration.LEARNING_MICROFRONTEND_URL + url);
// static view url
const programsUrl = baseAppUrl('/dashboard/programs');
diff --git a/src/messages.js b/src/messages.js
index a1c7a19..5934551 100644
--- a/src/messages.js
+++ b/src/messages.js
@@ -6,6 +6,11 @@ export const messages = StrictDict({
description: 'Page loading screen-reader text',
defaultMessage: 'Loading...',
},
+ errorMessage: {
+ id: 'learner-dash.error-page-message',
+ defaultMessage: 'If you experience repeated failures, please email support at {supportEmail}',
+ description: 'Error page message',
+ },
pageTitle: {
id: 'learner-dash.title',
description: 'Page title: Learner Home',
diff --git a/src/setupTest.jsx b/src/setupTest.jsx
index b1c6088..78e426a 100755
--- a/src/setupTest.jsx
+++ b/src/setupTest.jsx
@@ -42,6 +42,11 @@ jest.mock('moment', () => ({
}),
}));
+jest.mock('@edx/frontend-platform/react', () => ({
+ ...jest.requireActual('@edx/frontend-platform/react'),
+ ErrorPage: () => 'ErrorPage',
+}));
+
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
const PropTypes = jest.requireActual('prop-types');
@@ -214,3 +219,12 @@ jest.mock('hooks', () => ({
...jest.requireActual('hooks'),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
}));
+
+jest.mock('utils/hooks', () => {
+ const formatDate = jest.fn(date => new Date(date).toLocaleDateString())
+ .mockName('utils.formatDate');
+ return {
+ formatDate,
+ useFormatDate: () => formatDate,
+ };
+});
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index 2a09e5a..b6fd274 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -14,14 +14,17 @@ import userEvent from '@testing-library/user-event';
import thunk from 'redux-thunk';
import { useIntl, IntlProvider } from '@edx/frontend-platform/i18n';
+import { useFormatDate } from 'utils/hooks';
+
import api from 'data/services/lms/api';
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';
import { selectors, thunkActions } from 'data/redux';
import { cardId as genCardId } from 'data/redux/app/reducer';
+import messages from 'i18n';
+
import App from 'App';
import Inspector from './inspector';
import appMessages from './messages';
@@ -43,6 +46,14 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
}),
}));
+jest.mock('utils/hooks', () => {
+ const formatDate = jest.fn(date => `Date-${date}`);
+ return {
+ formatDate,
+ useFormatDate: () => formatDate,
+ };
+});
+
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
getLoginRedirectUrl: jest.fn(),
@@ -212,7 +223,7 @@ describe('ESG app integration tests', () => {
},
}, // audit, course run and learner started, access expired, cannot upgrade
];
- const { formatDate } = useIntl();
+ const formatDate = useFormatDate();
await loadApp([courses[0]]);
await testCourse([
({ cardId, cardDetails }) => {
diff --git a/src/utils/dateFormatter.js b/src/utils/dateFormatter.js
index c9f007b..9085acd 100644
--- a/src/utils/dateFormatter.js
+++ b/src/utils/dateFormatter.js
@@ -1,5 +1,9 @@
import moment from 'moment';
-const dateFormatter = (formatDate, date) => formatDate(moment(date).toDate(), { year: 'numeric', month: 'long', day: '2-digit' });
+export const dateFormatter = (formatDate, date) => formatDate(moment(date).toDate(), {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+});
export default dateFormatter;
diff --git a/src/utils/hooks.js b/src/utils/hooks.js
new file mode 100644
index 0000000..941f931
--- /dev/null
+++ b/src/utils/hooks.js
@@ -0,0 +1,12 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import dateFormatter from './dateFormatter';
+
+export const useFormatDate = () => {
+ const { formatDate } = useIntl();
+ return (date) => dateFormatter(formatDate, date);
+};
+
+export default {
+ useFormatDate,
+};