fix: Bug-doc fixes (#52)

Co-authored-by: Leangseu Kim <lkim@edx.org>
This commit is contained in:
Ben Warzeski
2022-10-25 15:36:33 -04:00
committed by GitHub
parent f8b181e8c9
commit 61e484af1f
27 changed files with 404 additions and 165 deletions

View File

@@ -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>

View File

@@ -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>,
));
});
});
});
});

View File

@@ -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>

View File

@@ -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}
>

View File

@@ -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);
});
});
});

View File

@@ -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>
`;

View File

@@ -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>;

View File

@@ -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;

View File

@@ -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)}

View File

@@ -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);

View File

@@ -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}

View File

@@ -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}

View File

@@ -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);
});
});

View File

@@ -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', () => {

View File

@@ -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">

View File

@@ -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 />);
};

View File

@@ -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;

View File

@@ -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', () => {

View File

@@ -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,

View File

@@ -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', () => {

View File

@@ -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));

View File

@@ -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');

View File

@@ -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',

View File

@@ -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,
};
});

View File

@@ -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 }) => {

View File

@@ -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
View 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,
};