28
src/App.jsx
28
src/App.jsx
@@ -4,14 +4,23 @@ import { useDispatch } from 'react-redux';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import store from 'data/store';
|
||||
import { selectors, actions, thunkActions } from 'data/redux';
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import {
|
||||
selectors,
|
||||
actions,
|
||||
thunkActions,
|
||||
hooks as appHooks,
|
||||
} from 'data/redux';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import './App.scss';
|
||||
@@ -21,6 +30,12 @@ export const App = () => {
|
||||
// TODO: made development-only
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const isFailed = {
|
||||
initialize: appHooks.useRequestIsFailed(RequestKeys.initialize),
|
||||
refreshList: appHooks.useRequestIsFailed(RequestKeys.refreshList),
|
||||
};
|
||||
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
@@ -49,7 +64,12 @@ export const App = () => {
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (<Dashboard />)}
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
|
||||
@@ -1,41 +1,104 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
actions: 'redux.actions',
|
||||
thunkActions: 'redux.thunkActions',
|
||||
hooks: {
|
||||
useRequestIsFailed: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('data/store', () => 'data/store');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
const supportEmail = 'test-support-url';
|
||||
appHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
const { formatMessage } = useIntl();
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.find(BrowserRouter);
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
const runBasicTests = () => {
|
||||
test('snapshot', () => { expect(el).toMatchSnapshot(); });
|
||||
it('displays title in helmet component', () => {
|
||||
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
|
||||
});
|
||||
it('displays learner dashboard header', () => {
|
||||
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
|
||||
});
|
||||
it('wraps the page in a browser router', () => {
|
||||
expect(el.find(Router)).toMatchObject(el);
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(el.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
};
|
||||
describe('no network failure', () => {
|
||||
beforeAll(() => {
|
||||
appHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
expect(el.find('main')).toMatchObject(shallow(
|
||||
<main><Dashboard /></main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
describe('initialize failure', () => {
|
||||
beforeAll(() => {
|
||||
appHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
describe('refresh failure', () => {
|
||||
beforeAll(() => {
|
||||
appHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
@@ -16,7 +44,35 @@ exports[`App router component snapshot: enabled 1`] = `
|
||||
<Dashboard />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -10,11 +10,10 @@ import messages from './messages';
|
||||
export const ViewCourseButton = ({ cardId }) => {
|
||||
const { homeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isEntitlement, isExpired } = hooks.useCardEntitlementData(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<Button
|
||||
disabled={!hasAccess || (isEntitlement && isExpired)}
|
||||
disabled={!hasAccess}
|
||||
as="a"
|
||||
href={homeUrl}
|
||||
>
|
||||
|
||||
@@ -12,47 +12,48 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const homeUrl = 'homeUrl';
|
||||
let wrapper;
|
||||
const props = { cardId: 'cardId' };
|
||||
const homeUrl = 'homeUrl';
|
||||
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
}) => {
|
||||
hooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
}) => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...props} />);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('default button', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true });
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...props} />);
|
||||
};
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
describe('learner has access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: true });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
});
|
||||
test('disabled button', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
test('link is enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('disabled button without access', () => {
|
||||
const wrapper = createWrapper({ hasAccess: false, isEntitlement: false, isExpired: false });
|
||||
expect(wrapper.prop('disabled')).toEqual(true);
|
||||
describe('learner does not have access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: false });
|
||||
});
|
||||
it('disabled button with access', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true, isEntitlement: true, isExpired: true });
|
||||
expect(wrapper.prop('disabled')).toEqual(true);
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('enabled button', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true, isEntitlement: false, isExpired: false });
|
||||
expect(wrapper.prop('disabled')).toEqual(false);
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
});
|
||||
test('link is enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewCourseButton snapshot default button 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="homeUrl"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton snapshot disabled button 1`] = `
|
||||
exports[`ViewCourseButton learner does not have access to course snapshot 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={true}
|
||||
@@ -19,3 +9,13 @@ exports[`ViewCourseButton snapshot disabled button 1`] = `
|
||||
View Course
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton learner has access to course snapshot 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="homeUrl"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MailtoLink, Hyperlink } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useFormatDate } from 'utils/hooks';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
@@ -21,7 +22,8 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
const { isArchived } = appHooks.useCardCourseRunData(cardId);
|
||||
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId);
|
||||
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Hyperlink } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { useFormatDate } from 'utils/hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -16,7 +17,8 @@ export const CourseBanner = ({ cardId }) => {
|
||||
coursewareAccess = {},
|
||||
} = appHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = appHooks.useCardCourseRunData(cardId);
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { dateFormatter } from 'utils';
|
||||
import { useFormatDate } from 'utils/hooks';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
@@ -23,7 +23,8 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
} = appHooks.useCardEntitlementData(cardId);
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const { formatDate, formatMessage } = useIntl();
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
if (!isEntitlement) {
|
||||
return null;
|
||||
@@ -42,7 +43,7 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.entitlementExpiringSoon, {
|
||||
changeDeadline: dateFormatter(formatDate, changeDeadline),
|
||||
changeDeadline: formatDate(changeDeadline),
|
||||
selectSessionButton: (
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
||||
{formatMessage(messages.selectSession)}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { useFormatDate } from 'utils/hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useAccessMessage = ({ cardId }) => {
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const { formatMessage } = useIntl();
|
||||
const enrollment = appHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = appHooks.useCardCourseRunData(cardId);
|
||||
const formatDate = useFormatDate();
|
||||
if (!courseRun.isStarted) {
|
||||
if (!courseRun.startDate) { return null; }
|
||||
const startDate = formatDate(courseRun.startDate);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardMenu snapshot 1`] = `
|
||||
exports[`CourseCardMenu enrolled, share enabled snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
@@ -49,7 +49,7 @@ exports[`CourseCardMenu snapshot 1`] = `
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu snapshot: masquerading 1`] = `
|
||||
exports[`CourseCardMenu masquerading snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
@@ -98,7 +98,7 @@ exports[`CourseCardMenu snapshot: masquerading 1`] = `
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu twitter share disabled 1`] = `
|
||||
exports[`CourseCardMenu not enrolled, share disabled snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
@@ -110,13 +110,6 @@ exports[`CourseCardMenu twitter share disabled 1`] = `
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
data-testid="unenrollModalToggle"
|
||||
disabled={false}
|
||||
onClick={[MockFunction unenrollShow]}
|
||||
>
|
||||
Unenroll
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid="emailSettingsModalToggle"
|
||||
disabled={false}
|
||||
|
||||
@@ -17,6 +17,7 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
const emailSettingsModal = useEmailSettings();
|
||||
const unenrollModal = useUnenrollData();
|
||||
const { courseName } = appHooks.useCardCourseData(cardId);
|
||||
const { isEnrolled } = appHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
// facebook,
|
||||
twitter,
|
||||
@@ -36,13 +37,15 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
alt={formatMessage(messages.dropdownAlt)}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={unenrollModal.show}
|
||||
data-testid="unenrollModalToggle"
|
||||
>
|
||||
{formatMessage(messages.unenroll)}
|
||||
</Dropdown.Item>
|
||||
{isEnrolled && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={unenrollModal.show}
|
||||
data-testid="unenrollModalToggle"
|
||||
>
|
||||
{formatMessage(messages.unenroll)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={emailSettingsModal.show}
|
||||
|
||||
@@ -11,6 +11,7 @@ jest.mock('react-share', () => ({
|
||||
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(<CourseCardMenu {...props} />);
|
||||
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(<CourseCardMenu {...props} />);
|
||||
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(<CourseCardMenu {...props} />);
|
||||
});
|
||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
||||
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(<CourseCardMenu {...props} />);
|
||||
});
|
||||
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(<CourseCardMenu {...props} />);
|
||||
});
|
||||
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(<CourseCardMenu {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('TwitterShareButton').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 (
|
||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-3">
|
||||
|
||||
@@ -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(<Dashboard />);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
12
src/utils/hooks.js
Normal file
12
src/utils/hooks.js
Normal file
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user