Thunk Removal Proof-of-concept (#98)

This commit is contained in:
Ben Warzeski
2023-01-19 10:13:48 -05:00
committed by GitHub
parent 82268b4f37
commit b2e8621e5c
115 changed files with 1409 additions and 1601 deletions

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -16,9 +15,8 @@ import store from 'data/store';
import {
selectors,
actions,
thunkActions,
hooks as appHooks,
} from 'data/redux';
import { reduxHooks } from 'hooks';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
@@ -32,34 +30,33 @@ import messages from './messages';
import './App.scss';
export const App = () => {
const dispatch = useDispatch();
// TODO: made development-only
const { authenticatedUser } = React.useContext(AppContext);
const { formatMessage } = useIntl();
const isFailed = {
initialize: appHooks.useRequestIsFailed(RequestKeys.initialize),
refreshList: appHooks.useRequestIsFailed(RequestKeys.refreshList),
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
};
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
const { supportEmail } = appHooks.usePlatformSettingsData();
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
React.useEffect(() => {
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
window.loadEmptyData = () => {
dispatch(thunkActions.app.loadData({ ...fakeData.globalData, courses: [] }));
loadData({ ...fakeData.globalData, courses: [] });
};
window.loadMockData = () => {
dispatch(thunkActions.app.loadData({
loadData({
...fakeData.globalData,
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
}));
});
};
window.store = store;
window.selectors = selectors;
window.actions = actions;
window.thunkActions = thunkActions;
window.track = track;
}
if (process.env.HOTJAR_APP_ID) {
@@ -73,7 +70,7 @@ export const App = () => {
logError(error);
}
}
}, [authenticatedUser, dispatch]);
}, [authenticatedUser, loadData]);
return (
<Router>
<Helmet>

View File

@@ -10,7 +10,7 @@ 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 { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { App } from './App';
@@ -25,18 +25,24 @@ jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
thunkActions: 'redux.thunkActions',
hooks: {
}));
jest.mock('hooks', () => ({
reduxHooks: {
useRequestIsFailed: jest.fn(),
usePlatformSettingsData: jest.fn(),
useLoadData: jest.fn(),
},
}));
jest.mock('data/store', () => 'data/store');
const loadData = jest.fn();
reduxHooks.useLoadData.mockReturnValue(loadData);
const logo = 'fakeLogo.png';
let el;
const supportEmail = 'test-support-url';
appHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
describe('App router component', () => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
@@ -59,7 +65,7 @@ describe('App router component', () => {
};
describe('no network failure', () => {
beforeAll(() => {
appHooks.useRequestIsFailed.mockReturnValue(false);
reduxHooks.useRequestIsFailed.mockReturnValue(false);
el = shallow(<App />);
});
runBasicTests();
@@ -71,7 +77,7 @@ describe('App router component', () => {
});
describe('initialize failure', () => {
beforeAll(() => {
appHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
el = shallow(<App />);
});
runBasicTests();
@@ -87,7 +93,7 @@ describe('App router component', () => {
});
describe('refresh failure', () => {
beforeAll(() => {
appHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
el = shallow(<App />);
});
runBasicTests();

View File

@@ -4,16 +4,16 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const BeginCourseButton = ({ cardId }) => {
const { homeUrl } = hooks.useCardCourseRunData(cardId);
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
const { isMasquerading } = hooks.useMasqueradeData();
const { formatMessage } = useIntl();
const handleClick = hooks.useTrackCourseEvent(
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const handleClick = reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
cardId,
homeUrl,

View File

@@ -1,7 +1,7 @@
import { shallow } from 'enzyme';
import { htmlProps } from 'data/constants/htmlKeys';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import BeginCourseButton from './BeginCourseButton';
@@ -11,8 +11,8 @@ jest.mock('tracking', () => ({
},
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
@@ -25,7 +25,7 @@ jest.mock('data/redux', () => ({
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
const { homeUrl } = hooks.useCardCourseRunData();
const { homeUrl } = reduxHooks.useCardCourseRunData();
describe('BeginCourseButton', () => {
const props = {
@@ -39,7 +39,7 @@ describe('BeginCourseButton', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
props.cardId,
homeUrl,
@@ -49,20 +49,20 @@ describe('BeginCourseButton', () => {
describe('behavior', () => {
it('initializes course run data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('initializes enrollment data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
});
describe('disabled states', () => {
test('learner does not have access', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
wrapper = shallow(<BeginCourseButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('masquerading', () => {
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
wrapper = shallow(<BeginCourseButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});

View File

@@ -3,17 +3,17 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const ResumeButton = ({ cardId }) => {
const { resumeUrl } = hooks.useCardCourseRunData(cardId);
const { hasAccess, isAudit, isAuditAccessExpired } = hooks.useCardEnrollmentData(cardId);
const { isMasquerading } = hooks.useMasqueradeData();
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl();
const handleClick = hooks.useTrackCourseEvent(
const handleClick = reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
cardId,
resumeUrl,

View File

@@ -1,12 +1,12 @@
import { shallow } from 'enzyme';
import { htmlProps } from 'data/constants/htmlKeys';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import ResumeButton from './ResumeButton';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
useCardEnrollmentData: jest.fn(() => ({
hasAccess: true,
@@ -26,7 +26,7 @@ jest.mock('tracking', () => ({
}));
jest.mock('./ActionButton', () => 'ActionButton');
const { resumeUrl } = hooks.useCardCourseRunData();
const { resumeUrl } = reduxHooks.useCardCourseRunData();
describe('ResumeButton', () => {
const props = {
@@ -48,20 +48,20 @@ describe('ResumeButton', () => {
describe('behavior', () => {
it('initializes course run data based on cardId', () => {
shallow(<ResumeButton {...props} />);
expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('initializes course enrollment data based on cardId', () => {
shallow(<ResumeButton {...props} />);
expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
});
describe('disabled states', () => {
test('masquerading', () => {
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
const wrapper = shallow(<ResumeButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('learner does not have access', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
hasAccess: false,
isAudit: true,
isAuditAccessExpired: false,
@@ -70,7 +70,7 @@ describe('ResumeButton', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('audit access expired', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
hasAccess: true,
isAudit: true,
isAuditAccessExpired: true,

View File

@@ -3,16 +3,16 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const SelectSessionButton = ({ cardId }) => {
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
const { canChange, hasSessions } = hooks.useCardEntitlementData(cardId);
const { isMasquerading } = hooks.useMasqueradeData();
const { formatMessage } = useIntl();
const openSessionModal = hooks.useUpdateSelectSessionModalCallback(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
const { canChange, hasSessions } = reduxHooks.useCardEntitlementData(cardId);
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
return (
<ActionButton
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}

View File

@@ -1,11 +1,11 @@
import { shallow } from 'enzyme';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import SelectSessionButton from './SelectSessionButton';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
@@ -24,12 +24,12 @@ describe('SelectSessionButton', () => {
expect(wrapper).toMatchSnapshot();
});
it('renders disabled button when user does not have access to the course', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
it('renders disabled button if masquerading', () => {
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -39,26 +39,26 @@ describe('SelectSessionButton', () => {
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick).getMockName())
.toEqual(hooks.useUpdateSelectSessionModalCallback().getMockName());
.toEqual(reduxHooks.useUpdateSelectSessionModalCallback().getMockName());
});
describe('disabled states', () => {
test('learner does not have access', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('learner cannot change sessions', () => {
hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('entitlement does not have available sessions', () => {
hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('user is masquerading', () => {
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});

View File

@@ -5,7 +5,7 @@ import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ActionButton from './ActionButton';
import messages from './messages';
@@ -13,10 +13,10 @@ import messages from './messages';
export const UpgradeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { upgradeUrl } = hooks.useCardCourseRunData(cardId);
const { canUpgrade } = hooks.useCardEnrollmentData(cardId);
const { isMasquerading } = hooks.useMasqueradeData();
const trackUpgradeClick = hooks.useTrackCourseEvent(
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { canUpgrade } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
cardId,
upgradeUrl,

View File

@@ -1,7 +1,7 @@
import { shallow } from 'enzyme';
import track from 'tracking';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import UpgradeButton from './UpgradeButton';
@@ -11,8 +11,8 @@ jest.mock('tracking', () => ({
},
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
@@ -29,26 +29,26 @@ describe('UpgradeButton', () => {
cardId: 'cardId',
};
const upgradeUrl = 'upgradeUrl';
hooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
describe('snapshot', () => {
test('can upgrade', () => {
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
props.cardId,
upgradeUrl,
));
});
test('cannot upgrade', () => {
hooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('masquerading', () => {
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);

View File

@@ -4,20 +4,19 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const ViewCourseButton = ({ cardId }) => {
const { homeUrl } = hooks.useCardCourseRunData(cardId);
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
const handleClick = hooks.useTrackCourseEvent(
const { formatMessage } = useIntl();
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
const handleClick = reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
cardId,
homeUrl,
);
const { formatMessage } = useIntl();
return (
<ActionButton
disabled={!hasAccess}

View File

@@ -2,7 +2,7 @@ import { shallow } from 'enzyme';
import track from 'tracking';
import { htmlProps } from 'data/constants/htmlKeys';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ViewCourseButton from './ViewCourseButton';
jest.mock('tracking', () => ({
@@ -11,8 +11,8 @@ jest.mock('tracking', () => ({
},
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
@@ -33,9 +33,9 @@ const createWrapper = ({
isExpired = false,
propsOveride = {},
}) => {
hooks.useCardCourseRunData.mockReturnValue({ homeUrl });
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
return shallow(<ViewCourseButton {...defaultProps} {...propsOveride} />);
};
@@ -48,7 +48,7 @@ describe('ViewCourseButton', () => {
expect(wrapper).toMatchSnapshot();
});
test('links to home URL', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
@@ -66,7 +66,7 @@ describe('ViewCourseButton', () => {
expect(wrapper).toMatchSnapshot();
});
test('links to home URL', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(hooks.useTrackCourseEvent(
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { ActionRow } from '@edx/paragon';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
@@ -12,9 +12,9 @@ import ResumeButton from './ResumeButton';
import ViewCourseButton from './ViewCourseButton';
export const CourseCardActions = ({ cardId }) => {
const { isEntitlement, isFulfilled } = hooks.useCardEntitlementData(cardId);
const { isVerified, hasStarted } = hooks.useCardEnrollmentData(cardId);
const { isArchived } = hooks.useCardCourseRunData(cardId);
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
const { isVerified, hasStarted } = reduxHooks.useCardEnrollmentData(cardId);
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
let PrimaryButton;
if (isEntitlement) {
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;

View File

@@ -1,11 +1,11 @@
import { shallow } from 'enzyme';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CourseCardActions from '.';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
@@ -25,9 +25,9 @@ describe('CourseCardActions', () => {
const createWrapper = ({
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
}) => {
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
hooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
hooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
return shallow(<CourseCardActions {...props} />);
};
describe('snapshot', () => {

View File

@@ -6,22 +6,23 @@ 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 { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages';
const { useFormatDate } = utilHooks;
export const CertificateBanner = ({ cardId }) => {
const certificate = appHooks.useCardCertificateData(cardId);
const certificate = reduxHooks.useCardCertificateData(cardId);
const {
isAudit,
isVerified,
} = appHooks.useCardEnrollmentData(cardId);
const { isPassing } = appHooks.useCardGradeData(cardId);
const { isArchived } = appHooks.useCardCourseRunData(cardId);
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId);
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
} = reduxHooks.useCardEnrollmentData(cardId);
const { isPassing } = reduxHooks.useCardGradeData(cardId);
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
const { minPassingGrade, progressUrl } = reduxHooks.useCardCourseRunData(cardId);
const { supportEmail, billingEmail } = reduxHooks.usePlatformSettingsData();
const { formatMessage } = useIntl();
const formatDate = useFormatDate();

View File

@@ -1,11 +1,14 @@
import { shallow } from 'enzyme';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner';
import messages from './messages';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: jest.fn(() => date => date),
},
reduxHooks: {
useCardCertificateData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -18,16 +21,17 @@ jest.mock('Components/Banner', () => 'Banner');
describe('CertificateBanner', () => {
const props = { cardId: 'cardId' };
hooks.usePlatformSettingsData.mockReturnValue({
reduxHooks.usePlatformSettingsData.mockReturnValue({
supportEmail: 'suport@email',
billingEmail: 'billing@email',
});
hooks.useCardCourseRunData.mockReturnValue({
reduxHooks.useCardCourseRunData.mockReturnValue({
minPassingGrade: 0.8,
progressUrl: 'progressUrl',
});
const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
@@ -44,10 +48,10 @@ describe('CertificateBanner', () => {
grade = {},
courseRun = {},
}) => {
hooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
hooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
hooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
hooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
return shallow(<CertificateBanner {...props} />);
};
describe('snapshot', () => {

View File

@@ -4,8 +4,7 @@ import PropTypes from 'prop-types';
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 { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages';
@@ -15,10 +14,10 @@ export const CourseBanner = ({ cardId }) => {
isAuditAccessExpired,
canUpgrade,
coursewareAccess = {},
} = appHooks.useCardEnrollmentData(cardId);
const courseRun = appHooks.useCardCourseRunData(cardId);
} = reduxHooks.useCardEnrollmentData(cardId);
const courseRun = reduxHooks.useCardCourseRunData(cardId);
const { formatMessage } = useIntl();
const formatDate = useFormatDate();
const formatDate = utilHooks.useFormatDate();
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;

View File

@@ -2,15 +2,18 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Hyperlink } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import { CourseBanner } from './CourseBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
},
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
@@ -41,11 +44,11 @@ const render = (overrides = {}) => {
courseRun = {},
enrollment = {},
} = overrides;
appHooks.useCardCourseRunData.mockReturnValueOnce({
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
...courseRunData,
...courseRun,
});
appHooks.useCardEnrollmentData.mockReturnValueOnce({
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
...enrollmentData,
...enrollment,
});
@@ -55,8 +58,8 @@ const render = (overrides = {}) => {
describe('CourseBanner', () => {
test('initializes data with course number from enrollment, course and course run data', () => {
render();
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } });

View File

@@ -1,5 +1,6 @@
import { StrictDict } from 'utils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent';
@@ -14,8 +15,8 @@ export const statusComponents = StrictDict({
});
export const useCreditBannerData = (cardId) => {
const credit = appHooks.useCardCreditData(cardId);
const { supportEmail } = appHooks.usePlatformSettingsData();
const credit = reduxHooks.useCardCreditData(cardId);
const { supportEmail } = reduxHooks.usePlatformSettingsData();
if (!credit.isEligible) { return null; }
const { error, purchased, requestStatus } = credit;

View File

@@ -1,5 +1,5 @@
import { keyStore } from 'utils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent';
@@ -9,8 +9,8 @@ import RejectedContent from './views/RejectedContent';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
usePlatformSettingsData: jest.fn(),
},
@@ -34,18 +34,18 @@ const defaultProps = {
};
const loadHook = (creditData = {}) => {
appHooks.useCardCreditData.mockReturnValue({ ...defaultProps, ...creditData });
reduxHooks.useCardCreditData.mockReturnValue({ ...defaultProps, ...creditData });
out = hooks.useCreditBannerData(cardId);
};
describe('useCreditBannerData hook', () => {
beforeEach(() => {
appHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
});
it('loads card credit data with cardID and loads platform settings data', () => {
loadHook({ isEligible: false });
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(appHooks.usePlatformSettingsData).toHaveBeenCalledWith();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.usePlatformSettingsData).toHaveBeenCalledWith();
});
describe('non-credit-eligible learner', () => {
it('returns null if the learner is not credit eligible', () => {

View File

@@ -3,14 +3,14 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink';
import messages from './messages';
export const ApprovedContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = appHooks.useCardCreditData(cardId);
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { formatMessage } = useIntl();
return (
<CreditContent

View File

@@ -2,13 +2,13 @@ import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import ApprovedContent from './ApprovedContent';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},
}));
@@ -21,7 +21,7 @@ const credit = {
providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name',
};
appHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCreditData.mockReturnValue(credit);
describe('ApprovedContent component', () => {
beforeEach(() => {
@@ -29,7 +29,7 @@ describe('ApprovedContent component', () => {
});
describe('behavior', () => {
it('initializes credit data with cardId', () => {
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import CreditContent from './components/CreditContent';
@@ -11,8 +11,8 @@ import messages from './messages';
export const EligibleContent = ({ cardId }) => {
const { formatMessage } = useIntl();
const { providerName, creditPurchaseUrl: href } = appHooks.useCardCreditData(cardId);
const { courseId } = appHooks.useCardCourseRunData(cardId);
const { providerName, creditPurchaseUrl: href } = reduxHooks.useCardCreditData(cardId);
const { courseId } = reduxHooks.useCardCourseRunData(cardId);
const onClick = track.credit.purchase(courseId, href);
const getCredit = formatMessage(messages.getCredit);

View File

@@ -1,15 +1,15 @@
import React from 'react';
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import track from 'tracking';
import messages from './messages';
import EligibleContent from './EligibleContent';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useCardCourseRunData: jest.fn(),
},
@@ -30,8 +30,8 @@ const credit = {
creditPurchaseUrl: 'test-credit-purchase-url',
providerName: 'test-credit-provider-name',
};
appHooks.useCardCreditData.mockReturnValue(credit);
appHooks.useCardCourseRunData.mockReturnValue({ courseId });
reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
const render = () => {
el = shallow(<EligibleContent cardId={cardId} />);
@@ -45,10 +45,10 @@ describe('EligibleContent component', () => {
});
describe('behavior', () => {
it('initializes credit data with cardId', () => {
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
it('initializes course run data with cardId', () => {
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
@@ -65,7 +65,7 @@ describe('EligibleContent component', () => {
expect(component.props().action.message).toEqual(formatMessage(messages.getCredit));
});
test('message is formatted eligible message if no provider', () => {
appHooks.useCardCreditData.mockReturnValueOnce({
reduxHooks.useCardCreditData.mockReturnValueOnce({
creditPurchaseUrl: credit.creditPurchaseUrl,
});
render();

View File

@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import messages from './messages';
import hooks from './hooks';
export const PendingContent = ({ cardId }) => {
const { providerName } = appHooks.useCardCreditData(cardId);
const { providerName } = reduxHooks.useCardCreditData(cardId);
const { formatMessage } = useIntl();
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
return (

View File

@@ -2,13 +2,13 @@ import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import messages from './messages';
import hooks from './hooks';
import PendingContent from './PendingContent';
jest.mock('data/redux', () => ({ hooks: { useCardCreditData: jest.fn() } }));
jest.mock('hooks', () => ({ reduxHooks: { useCardCreditData: jest.fn() } }));
jest.mock('./hooks', () => ({ useCreditRequestData: jest.fn() }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
@@ -20,7 +20,7 @@ const cardId = 'test-card-id';
const requestData = { test: 'requestData' };
const providerName = 'test-credit-provider-name';
const createCreditRequest = jest.fn().mockName('createCreditRequest');
appHooks.useCardCreditData.mockReturnValue({ providerName });
reduxHooks.useCardCreditData.mockReturnValue({ providerName });
hooks.useCreditRequestData.mockReturnValue({ requestData, createCreditRequest });
const render = () => {
@@ -32,7 +32,7 @@ describe('PendingContent component', () => {
});
describe('behavior', () => {
it('initializes card credit data with cardId', () => {
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
it('initializes credit request data with cardId', () => {
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);

View File

@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink';
import messages from './messages';
export const RejectedContent = ({ cardId }) => {
const credit = appHooks.useCardCreditData(cardId);
const credit = reduxHooks.useCardCreditData(cardId);
const { formatMessage } = useIntl();
return (
<CreditContent

View File

@@ -2,13 +2,13 @@ import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import RejectedContent from './RejectedContent';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},
}));
@@ -20,7 +20,7 @@ const credit = {
providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name',
};
appHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCreditData.mockReturnValue(credit);
let el;
let component;
@@ -31,7 +31,7 @@ describe('RejectedContent component', () => {
beforeEach(render);
describe('behavior', () => {
it('initializes credit data with cardId', () => {
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {

View File

@@ -2,11 +2,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { Hyperlink } from '@edx/paragon';
export const ProviderLink = ({ cardId }) => {
const credit = appHooks.useCardCreditData(cardId);
const credit = reduxHooks.useCardCreditData(cardId);
return (
<Hyperlink
href={credit.providerStatusUrl}

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ProviderLink from './ProviderLink';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},
}));
@@ -20,12 +20,12 @@ let el;
describe('ProviderLink component', () => {
beforeEach(() => {
appHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCreditData.mockReturnValue(credit);
el = shallow(<ProviderLink cardId={cardId} />);
});
describe('behavior', () => {
it('initializes credit hook with cardId', () => {
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { StrictDict } from 'utils';
import { AppContext } from '@edx/frontend-platform/react';
import { hooks as appHooks } from 'data/redux';
import api from 'data/services/lms/api';
import { apiHooks } from 'hooks';
import * as module from './hooks';
@@ -12,17 +11,11 @@ export const state = StrictDict({
export const useCreditRequestData = (cardId) => {
const [requestData, setRequestData] = module.state.creditRequestData(null);
const { courseId } = appHooks.useCardCourseRunData(cardId);
const { providerId } = appHooks.useCardCreditData(cardId);
const { authenticatedUser } = React.useContext(AppContext);
const { username } = authenticatedUser;
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
const createCreditRequest = (e) => {
e.preventDefault();
api.createCreditRequest({ providerId, courseId, username })
.then(setRequestData);
createCreditApiRequest().then(setRequestData);
};
return { requestData, createCreditRequest };
};

View File

@@ -1,37 +1,21 @@
import { AppContext } from '@edx/frontend-platform/react';
import { MockUseState } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import api from 'data/services/lms/api';
import { apiHooks } from 'hooks';
import * as hooks from './hooks';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: { username: 'test-username' },
jest.mock('hooks', () => ({
apiHooks: {
useCreateCreditRequest: jest.fn(),
},
}));
jest.mock('data/redux', () => ({
hooks: {
useCardCourseRunData: jest.fn(),
useCardCreditData: jest.fn(),
},
}));
jest.mock('data/services/lms/api', () => ({
createCreditRequest: jest.fn(),
}));
const state = new MockUseState(hooks);
const cardId = 'test-card-id';
const testValue = 'test-value';
const courseId = 'test-course-id';
const providerId = 'test-credit-provider-id';
const requestData = { test: 'request data' };
const creditRequest = jest.fn().mockReturnValue(Promise.resolve(requestData));
apiHooks.useCreateCreditRequest.mockReturnValue(creditRequest);
const event = { preventDefault: jest.fn() };
appHooks.useCardCourseRunData.mockReturnValue({ courseId });
appHooks.useCardCreditData.mockReturnValue({ providerId });
api.createCreditRequest.mockReturnValue(Promise.resolve(testValue));
const { username } = AppContext.authenticatedUser;
let out;
describe('Credit Banner view hooks', () => {
describe('state', () => {
@@ -40,35 +24,31 @@ describe('Credit Banner view hooks', () => {
describe('useCreditRequestData', () => {
beforeEach(() => {
state.mock();
state.mockVal(state.keys.creditRequestData, testValue);
out = hooks.useCreditRequestData(cardId);
});
describe('behavior', () => {
it('initializes creditRequestData state field with null value', () => {
state.expectInitializedWith(state.keys.creditRequestData, null);
});
it('calls useCardCourseRunData with passed cardID', () => {
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
});
it('calls useCardCreditData with passed cardID', () => {
expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
it('calls useCreateCreditRequest with passed cardID', () => {
expect(apiHooks.useCreateCreditRequest).toHaveBeenCalledWith(cardId);
});
});
describe('output', () => {
it('returns requestData state value', () => {
expect(out.requestData).toEqual(testValue);
state.mockVal(state.keys.creditRequestData, requestData);
out = hooks.useCreditRequestData(cardId);
expect(out.requestData).toEqual(requestData);
});
describe('createCreditRequest', () => {
const preventDefault = jest.fn();
const event = { preventDefault };
it('returns an event handler that prevents default click behavior', () => {
out.createCreditRequest(event);
expect(preventDefault).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
});
it('calls api.createCreditRequest and sets requestData with the response', async () => {
await out.createCreditRequest(event);
expect(api.createCreditRequest).toHaveBeenCalledWith({ providerId, courseId, username });
expect(state.setState.creditRequestData).toHaveBeenCalledWith(testValue);
expect(creditRequest).toHaveBeenCalledWith();
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData);
});
});
});

View File

@@ -4,13 +4,13 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, MailtoLink } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import { useFormatDate } from 'utils/hooks';
import { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages';
export const EntitlementBanner = ({ cardId }) => {
const { formatMessage } = useIntl();
const {
isEntitlement,
hasSessions,
@@ -18,11 +18,10 @@ export const EntitlementBanner = ({ cardId }) => {
changeDeadline,
showExpirationWarning,
isExpired,
} = appHooks.useCardEntitlementData(cardId);
const { supportEmail } = appHooks.usePlatformSettingsData();
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(cardId);
const { formatMessage } = useIntl();
const formatDate = useFormatDate();
} = reduxHooks.useCardEntitlementData(cardId);
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
const formatDate = utilHooks.useFormatDate();
if (!isEntitlement) {
return null;

View File

@@ -1,12 +1,15 @@
import React from 'react';
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import EntitlementBanner from './EntitlementBanner';
jest.mock('components/Banner', () => 'Banner');
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
},
reduxHooks: {
usePlatformSettingsData: jest.fn(),
useCardEntitlementData: jest.fn(),
useUpdateSelectSessionModalCallback: jest.fn(
@@ -30,16 +33,16 @@ const platformData = { supportEmail: 'test-support-email' };
const render = (overrides = {}) => {
const { entitlement = {} } = overrides;
appHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
appHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
el = shallow(<EntitlementBanner cardId={cardId} />);
};
describe('EntitlementBanner', () => {
test('initializes data with course number from entitlement', () => {
render();
expect(appHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(appHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
});
test('no display if not an entitlement', () => {
render({ entitlement: { isEntitlement: false } });

View File

@@ -15,7 +15,7 @@ exports[`CertificateBanner snapshot is passing and is downloadable 1`] = `
exports[`CertificateBanner snapshot is passing and is earned but unavailable 1`] = `
<Banner>
Your grade and certificate will be ready after Invalid Date.
Your grade and certificate will be ready after 10/20/3030.
</Banner>
`;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CourseBanner from './CourseBanner';
import CertificateBanner from './CertificateBanner';
@@ -9,7 +9,7 @@ import CreditBanner from './CreditBanner';
import EntitlementBanner from './EntitlementBanner';
export const CourseCardBanners = ({ cardId }) => {
const { isEnrolled } = appHooks.useCardEnrollmentData(cardId);
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
return (
<div className="course-card-banners" data-testid="CourseCardBanners">
<CourseBanner cardId={cardId} />

View File

@@ -1,15 +1,14 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import { useFormatDate } from 'utils/hooks';
import { utilHooks, reduxHooks } from 'hooks';
import * as hooks from './hooks';
import messages from './messages';
export const useAccessMessage = ({ cardId }) => {
const { formatMessage } = useIntl();
const enrollment = appHooks.useCardEnrollmentData(cardId);
const courseRun = appHooks.useCardCourseRunData(cardId);
const formatDate = useFormatDate();
const enrollment = reduxHooks.useCardEnrollmentData(cardId);
const courseRun = reduxHooks.useCardCourseRunData(cardId);
const formatDate = utilHooks.useFormatDate();
if (!courseRun.isStarted) {
if (!courseRun.startDate) { return null; }
const startDate = formatDate(courseRun.startDate);
@@ -39,15 +38,15 @@ export const useAccessMessage = ({ cardId }) => {
export const useCardDetailsData = ({ cardId }) => {
const { formatMessage } = useIntl();
const providerName = appHooks.useCardProviderData(cardId).name;
const { courseNumber } = appHooks.useCardCourseData(cardId);
const providerName = reduxHooks.useCardProviderData(cardId).name;
const { courseNumber } = reduxHooks.useCardCourseData(cardId);
const {
isEntitlement,
isFulfilled,
canChange,
} = appHooks.useCardEntitlementData(cardId);
} = reduxHooks.useCardEntitlementData(cardId);
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(cardId);
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
return {
providerName: providerName || formatMessage(messages.unknownProviderName),

View File

@@ -1,13 +1,16 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { keyStore, dateFormatter } from 'utils';
import { hooks as appHooks } from 'data/redux';
import { keyStore } from 'utils';
import { utilHooks, reduxHooks } from 'hooks';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: jest.fn(),
},
reduxHooks: {
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -21,11 +24,13 @@ const cardId = 'my-test-card-id';
const courseNumber = 'test-course-number';
const useAccessMessage = 'test-access-message';
const mockAccessMessage = (args) => ({ cardId: args.cardId, useAccessMessage });
const formatDate = jest.fn(date => `formatted-${date}`);
utilHooks.useFormatDate.mockReturnValue(formatDate);
const hookKeys = keyStore(hooks);
describe('CourseCardDetails hooks', () => {
let out;
const { formatMessage, formatDate } = useIntl();
const { formatMessage } = useIntl();
beforeEach(() => {
jest.clearAllMocks();
});
@@ -45,15 +50,15 @@ describe('CourseCardDetails hooks', () => {
const runHook = ({ provider = {}, entitlement = {} }) => {
jest.spyOn(hooks, hookKeys.useAccessMessage)
.mockImplementationOnce(mockAccessMessage);
appHooks.useCardProviderData.mockReturnValueOnce({
reduxHooks.useCardProviderData.mockReturnValueOnce({
...providerData,
...provider,
});
appHooks.useCardEntitlementData.mockReturnValueOnce({
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
...entitlementData,
...entitlement,
});
appHooks.useCardCourseData.mockReturnValueOnce({ courseNumber });
reduxHooks.useCardCourseData.mockReturnValueOnce({ courseNumber });
out = hooks.useCardDetailsData({ cardId });
};
beforeEach(() => {
@@ -86,11 +91,11 @@ describe('CourseCardDetails hooks', () => {
endDate: '10/20/2000',
};
const runHook = ({ enrollment = {}, courseRun = {} }) => {
appHooks.useCardCourseRunData.mockReturnValueOnce({
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
...courseRunData,
...courseRun,
});
appHooks.useCardEnrollmentData.mockReturnValueOnce({
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
...enrollmentData,
...enrollment,
});
@@ -99,8 +104,8 @@ describe('CourseCardDetails hooks', () => {
it('loads data from enrollment and course run data based on course number', () => {
runHook({});
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
describe('if not started yet', () => {
@@ -111,7 +116,7 @@ describe('CourseCardDetails hooks', () => {
});
expect(out).toEqual(formatMessage(
messages.courseStarts,
{ startDate: dateFormatter(formatDate, courseRunData.startDate) },
{ startDate: formatDate(courseRunData.startDate) },
));
});
});
@@ -123,7 +128,7 @@ describe('CourseCardDetails hooks', () => {
runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } });
expect(out).toEqual(formatMessage(
messages.accessExpired,
{ accessExpirationDate: dateFormatter(formatDate, enrollmentData.accessExpirationDate) },
{ accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) },
));
});
});
@@ -133,7 +138,7 @@ describe('CourseCardDetails hooks', () => {
runHook({ enrollment: { isAudit: true } });
expect(out).toEqual(formatMessage(
messages.accessExpires,
{ accessExpirationDate: dateFormatter(formatDate, enrollmentData.accessExpirationDate) },
{ accessExpirationDate: formatDate(enrollmentData.accessExpirationDate) },
));
});
it('no endDate and no accessExpirationDate, returns null', () => {
@@ -144,14 +149,14 @@ describe('CourseCardDetails hooks', () => {
runHook({ enrollment: { isAudit: true, accessExpirationDate: '' } });
expect(out).toEqual(formatMessage(
messages.courseEnds,
{ endDate: dateFormatter(formatDate, courseRunData.endDate) },
{ endDate: formatDate(courseRunData.endDate) },
));
});
it('no accessExpirationDate and is archived, return courseEnded with endDate', () => {
runHook({ enrollment: { isAudit: true, accessExpirationDate: '' }, courseRun: { isArchived: true } });
expect(out).toEqual(formatMessage(
messages.courseEnded,
{ endDate: dateFormatter(formatDate, courseRunData.endDate) },
{ endDate: formatDate(courseRunData.endDate) },
));
});
});
@@ -162,7 +167,7 @@ describe('CourseCardDetails hooks', () => {
runHook({});
expect(out).toEqual(formatMessage(
messages.courseEnds,
{ endDate: dateFormatter(formatDate, courseRunData.endDate) },
{ endDate: formatDate(courseRunData.endDate) },
));
});
});
@@ -172,7 +177,7 @@ describe('CourseCardDetails hooks', () => {
runHook({ courseRun: { isArchived: true } });
expect(out).toEqual(formatMessage(
messages.courseEnded,
{ endDate: dateFormatter(formatDate, courseRunData.endDate) },
{ endDate: formatDate(courseRunData.endDate) },
));
});
});

View File

@@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Badge } from '@edx/paragon';
import track from 'tracking';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import verifiedRibbon from 'assets/verified-ribbon.png';
@@ -15,11 +15,11 @@ const { courseImageClicked } = track.course;
export const CourseCardImage = ({ cardId, orientation }) => {
const { formatMessage } = useIntl();
const { bannerImgSrc } = appHooks.useCardCourseData(cardId);
const { homeUrl } = appHooks.useCardCourseRunData(cardId);
const { isVerified } = appHooks.useCardEnrollmentData(cardId);
const { isEntitlement } = appHooks.useCardEntitlementData(cardId);
const handleImageClicked = appHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
const { bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
const { isEntitlement } = reduxHooks.useCardEntitlementData(cardId);
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
const image = (
<>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { StrictDict } from 'utils';
import track from 'tracking';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import * as module from './hooks';
@@ -31,7 +31,7 @@ export const useEmailSettings = () => {
export const useHandleToggleDropdown = (cardId) => {
const eventName = track.course.courseOptionsDropdownClicked;
const trackCourseEvent = appHooks.useTrackCourseEvent(eventName, cardId);
const trackCourseEvent = reduxHooks.useTrackCourseEvent(eventName, cardId);
return (isOpen) => {
if (isOpen) { trackCourseEvent(); }
};

View File

@@ -1,9 +1,22 @@
import { MockUseState } from 'testUtils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import * as hooks from './hooks';
jest.mock('hooks', () => ({
reduxHooks: {
useTrackCourseEvent: jest.fn(),
},
}));
const trackCourseEvent = jest.fn();
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
const state = new MockUseState(hooks);
const cardId = 'test-card-id';
let out;
describe('CourseCardMenu hooks', () => {
describe('state values', () => {
state.testGetter(state.keys.isUnenrollConfirmVisible);
@@ -11,7 +24,6 @@ describe('CourseCardMenu hooks', () => {
});
describe('useUnenrollData', () => {
let out;
beforeEach(() => {
state.mock();
out = hooks.useUnenrollData();
@@ -34,7 +46,6 @@ describe('CourseCardMenu hooks', () => {
});
describe('useEmailSettings', () => {
let out;
beforeEach(() => {
state.mock();
out = hooks.useEmailSettings();
@@ -55,4 +66,26 @@ describe('CourseCardMenu hooks', () => {
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, false);
});
});
describe('useHandleToggleDropdown', () => {
beforeEach(() => {
out = hooks.useHandleToggleDropdown(cardId);
});
describe('behavior', () => {
it('initializes course event tracker with event name and card ID', () => {
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.courseOptionsDropdownClicked,
cardId,
);
});
});
describe('returned method', () => {
it('calls trackCourseEvent iff true is passed', () => {
out(false);
expect(trackCourseEvent).not.toHaveBeenCalled();
out(true);
expect(trackCourseEvent).toHaveBeenCalled();
});
});
});
});

View File

@@ -7,7 +7,7 @@ import { Dropdown, Icon, IconButton } from '@edx/paragon';
import { MoreVert } from '@edx/paragon/icons';
import track from 'tracking';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import {
@@ -21,16 +21,16 @@ import messages from './messages';
export const CourseCardMenu = ({ cardId }) => {
const { formatMessage } = useIntl();
const { courseName } = appHooks.useCardCourseData(cardId);
const { isEnrolled, isEmailEnabled } = appHooks.useCardEnrollmentData(cardId);
const { twitter, facebook } = appHooks.useCardSocialSettingsData(cardId);
const { isMasquerading } = appHooks.useMasqueradeData();
const handleTwitterShare = appHooks.useTrackCourseEvent(
const { courseName } = reduxHooks.useCardCourseData(cardId);
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const handleTwitterShare = reduxHooks.useTrackCourseEvent(
track.socialShare,
cardId,
'twitter',
);
const handleFacebookShare = appHooks.useTrackCourseEvent(
const handleFacebookShare = reduxHooks.useTrackCourseEvent(
track.socialShare,
cardId,
'facebook',

View File

@@ -1,6 +1,6 @@
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { useEmailSettings, useUnenrollData } from './hooks';
import CourseCardMenu from '.';
@@ -8,8 +8,8 @@ jest.mock('react-share', () => ({
FacebookShareButton: () => 'FacebookShareButton',
TwitterShareButton: () => 'TwitterShareButton',
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardSocialSettingsData: jest.fn(),
@@ -56,10 +56,10 @@ describe('CourseCardMenu', () => {
beforeEach(() => {
useEmailSettings.mockReturnValue(defaultEmailSettingsModal);
useUnenrollData.mockReturnValue(defaultUnenrollModal);
appHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare);
appHooks.useCardCourseData.mockReturnValue({ courseName });
appHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true, isEmailEnabled: true });
appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
reduxHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare);
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true, isEmailEnabled: true });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
});
describe('enrolled, share enabled, email setting enable', () => {
beforeEach(() => {
@@ -91,12 +91,12 @@ describe('CourseCardMenu', () => {
});
describe('not enrolled, share disabled, email setting disabled', () => {
beforeEach(() => {
appHooks.useCardSocialSettingsData.mockReturnValueOnce({
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
...defaultSocialShare,
twitter: { ...defaultSocialShare.twitter, isEnabled: false },
facebook: { ...defaultSocialShare.facebook, isEnabled: false },
});
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, isEmailEnabled: false });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, isEmailEnabled: false });
wrapper = shallow(<CourseCardMenu {...props} />);
});
test('snapshot', () => {
@@ -117,7 +117,7 @@ describe('CourseCardMenu', () => {
});
describe('masquerading', () => {
beforeEach(() => {
appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
wrapper = shallow(<CourseCardMenu {...props} />);
});
test('snapshot', () => {

View File

@@ -2,15 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import track from 'tracking';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
const { courseTitleClicked } = track.course;
export const CourseCardTitle = ({ cardId }) => {
const { courseName } = appHooks.useCardCourseData(cardId);
const { isEntitlement, isFulfilled } = appHooks.useCardEntitlementData(cardId);
const { homeUrl } = appHooks.useCardCourseRunData(cardId);
const handleTitleClicked = appHooks.useTrackCourseEvent(courseTitleClicked, cardId, homeUrl);
const { courseName } = reduxHooks.useCardCourseData(cardId);
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const handleTitleClicked = reduxHooks.useTrackCourseEvent(courseTitleClicked, cardId, homeUrl);
return (
<h3>
<a

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import messages from './messages';
import * as module from './hooks';
@@ -14,7 +14,7 @@ export const state = StrictDict({
export const useRelatedProgramsBadgeData = ({ cardId }) => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const { formatMessage } = useIntl();
const numPrograms = appHooks.useCardRelatedProgramsData(cardId).length;
const numPrograms = reduxHooks.useCardRelatedProgramsData(cardId).length;
let programsMessage = '';
if (numPrograms) {
programsMessage = formatMessage(
@@ -27,8 +27,8 @@ export const useRelatedProgramsBadgeData = ({ cardId }) => {
numPrograms,
programsMessage,
isOpen,
openModal: React.useCallback(() => setIsOpen(true), [setIsOpen]),
closeModal: React.useCallback(() => setIsOpen(false), [setIsOpen]),
openModal: () => setIsOpen(true),
closeModal: () => setIsOpen(false),
};
};

View File

@@ -1,13 +1,13 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { MockUseState } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardRelatedProgramsData: jest.fn(),
},
}));
@@ -30,7 +30,7 @@ describe('RelatedProgramsBadge hooks', () => {
describe('useRelatedProgramsBadgeData', () => {
beforeEach(() => {
state.mock();
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({
length: numPrograms,
});
out = hooks.useRelatedProgramsBadgeData({ cardId });
@@ -38,16 +38,12 @@ describe('RelatedProgramsBadge hooks', () => {
afterEach(state.restore);
test('openModal sets isOpen to true as useCallback', () => {
const { cb, prereqs } = out.openModal.useCallback;
expect(prereqs).toEqual([state.setState.isOpen]);
cb();
out.openModal();
expect(state.setState.isOpen).toHaveBeenCalledWith(true);
});
test('closeModal sets isOpen to false as useCallback', () => {
const { cb, prereqs } = out.closeModal.useCallback;
expect(prereqs).toEqual([state.setState.isOpen]);
cb();
out.closeModal();
expect(state.setState.isOpen).toHaveBeenCalledWith(false);
});
@@ -59,12 +55,12 @@ describe('RelatedProgramsBadge hooks', () => {
expect(out.numPrograms).toEqual(numPrograms);
});
test('returns empty programsMessage if no programs', () => {
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
out = hooks.useRelatedProgramsBadgeData({ cardId });
expect(out.programsMessage).toEqual('');
});
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
out = hooks.useRelatedProgramsBadgeData({ cardId });
expect(out.programsMessage).toEqual(formatMessage(
messages.badgeLabelSingular,

View File

@@ -1,6 +1,6 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
export const useIsCollapsed = () => {
const { width } = useWindowSize();
@@ -9,8 +9,8 @@ export const useIsCollapsed = () => {
export const useCardData = ({ cardId }) => {
const { formatMessage } = useIntl();
const { title, bannerImgSrc } = appHooks.useCardCourseData(cardId);
const { isEnrolled } = appHooks.useCardEnrollmentData(cardId);
const { title, bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
return {
isEnrolled,

View File

@@ -1,11 +1,11 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
@@ -26,11 +26,11 @@ describe('CourseCard hooks', () => {
bannerImgSrc: 'my-banner-url',
};
const runHook = ({ course = {} }) => {
appHooks.useCardCourseData.mockReturnValueOnce({
reduxHooks.useCardCourseData.mockReturnValueOnce({
...courseData,
...course,
});
appHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: 'test-is-enrolled' });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: 'test-is-enrolled' });
out = hooks.useCardData({ cardId });
};
beforeEach(() => {
@@ -40,7 +40,7 @@ describe('CourseCard hooks', () => {
expect(out.formatMessage).toEqual(formatMessage);
});
it('passes course title and banner URL form course data', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(out.title).toEqual(courseData.title);
expect(out.bannerImgSrc).toEqual(courseData.bannerImgSrc);
});

View File

@@ -14,7 +14,7 @@ import {
} from '@edx/paragon';
import { Close, Tune } from '@edx/paragon/icons';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import FilterForm from './components/FilterForm';
import SortForm from './components/SortForm';
@@ -30,7 +30,7 @@ export const CourseFilterControls = ({
setFilters,
}) => {
const { formatMessage } = useIntl();
const hasCourses = appHooks.useHasCourses();
const hasCourses = reduxHooks.useHasCourses();
const {
isOpen,
open,

View File

@@ -2,13 +2,13 @@ import { shallow } from 'enzyme';
import { breakpoints, useWindowSize } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import CourseFilterControls from './CourseFilterControls';
import useCourseFilterControlsData from './hooks';
jest.mock('data/redux', () => ({
hooks: { useHasCourses: jest.fn() },
jest.mock('hooks', () => ({
reduxHooks: { useHasCourses: jest.fn() },
}));
jest.mock('./hooks', () => jest.fn().mockName('useCourseFilterControlsData'));
@@ -16,7 +16,7 @@ jest.mock('./hooks', () => jest.fn().mockName('useCourseFilterControlsData'));
jest.mock('./components/FilterForm', () => 'FilterForm');
jest.mock('./components/SortForm', () => 'SortForm');
appHooks.useHasCourses.mockReturnValue(true);
reduxHooks.useHasCourses.mockReturnValue(true);
describe('CourseFilterControls', () => {
const props = {
@@ -41,7 +41,7 @@ describe('CourseFilterControls', () => {
describe('no courses', () => {
test('snapshot', () => {
appHooks.useHasCourses.mockReturnValueOnce(false);
reduxHooks.useHasCourses.mockReturnValueOnce(false);
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper).toMatchSnapshot();

View File

@@ -4,15 +4,14 @@ import { Button, Image } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import emptyCourseSVG from 'assets/empty-course.svg';
import { reduxHooks } from 'hooks';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
import './index.scss';
export const NoCoursesView = () => {
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
return (
<div
id="no-courses-content-view"

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { useCheckboxSetValues, useWindowSize, breakpoints } from '@edx/paragon';
import { StrictDict } from 'utils';
import { actions, hooks as appHooks } from 'data/redux';
import { ListPageSize, SortKeys } from 'data/constants/app';
import { reduxHooks } from 'hooks';
import { StrictDict } from 'utils';
import * as module from './hooks';
@@ -19,18 +18,16 @@ export const state = StrictDict({
});
export const useCourseListData = () => {
const dispatch = useDispatch();
const pageNumber = appHooks.usePageNumber();
const [filters, setFilters] = useCheckboxSetValues([]);
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
const { numPages, visible } = appHooks.useCurrentCourseList({
const pageNumber = reduxHooks.usePageNumber();
const { numPages, visible } = reduxHooks.useCurrentCourseList({
sortBy,
filters,
pageSize: ListPageSize,
});
const handleRemoveFilter = (filter) => () => setFilters.remove(filter);
const setPageNumber = (value) => dispatch(actions.app.setPageNumber(value));
const setPageNumber = reduxHooks.useSetPageNumber();
return {
pageNumber,

View File

@@ -1,27 +1,20 @@
import { useDispatch } from 'react-redux';
import * as paragon from '@edx/paragon';
import { MockUseState } from 'testUtils';
import { actions, hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { ListPageSize, SortKeys } from 'data/constants/app';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
actions: {
app: {
setPageNumber: (value) => ({ setPageNumber: value }),
},
},
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCurrentCourseList: jest.fn(),
usePageNumber: jest.fn(() => 23),
useSetPageNumber: jest.fn(),
},
}));
const state = new MockUseState(hooks);
const dispatch = useDispatch();
const testList = ['a', 'b'];
const testListData = {
numPages: 52,
@@ -31,11 +24,13 @@ const testSortBy = 'fake sort option';
const testFilters = ['some', 'fake', 'filters'];
const testSetFilters = { add: jest.fn(), remove: jest.fn() };
const testCheckboxSetValues = [testFilters, testSetFilters];
const setPageNumber = jest.fn(val => ({ setPageNumber: val }));
reduxHooks.useSetPageNumber.mockReturnValue(setPageNumber);
describe('CourseList hooks', () => {
let out;
appHooks.useCurrentCourseList.mockReturnValue(testListData);
reduxHooks.useCurrentCourseList.mockReturnValue(testListData);
paragon.useCheckboxSetValues.mockImplementation(() => testCheckboxSetValues);
describe('state values', () => {
@@ -55,7 +50,7 @@ describe('CourseList hooks', () => {
state.expectInitializedWith(state.keys.sortBy, SortKeys.enrolled);
});
it('loads current course list with sortBy, filters, and page size', () => {
expect(appHooks.useCurrentCourseList).toHaveBeenCalledWith({
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
sortBy: testSortBy,
filters: testFilters,
pageSize: ListPageSize,
@@ -64,7 +59,7 @@ describe('CourseList hooks', () => {
});
describe('output', () => {
test('pageNumber loads from usePageNumber hook', () => {
expect(out.pageNumber).toEqual(appHooks.usePageNumber());
expect(out.pageNumber).toEqual(reduxHooks.usePageNumber());
});
test('numPages and visible list load from useCurrentCourseList hook', () => {
expect(out.numPages).toEqual(testListData.numPages);
@@ -94,7 +89,7 @@ describe('CourseList hooks', () => {
expect(testSetFilters.remove).toHaveBeenCalledWith(testFilters[0]);
});
test('setPageNumber dispatches setPageNumber action with passed value', () => {
expect(out.setPageNumber(2)).toEqual(dispatch(actions.app.setPageNumber(2)));
expect(out.setPageNumber(2)).toEqual(setPageNumber(2));
});
});
});

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Pagination } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import {
ActiveCourseFilters,
CourseFilterControls,
@@ -19,7 +19,7 @@ import './index.scss';
export const CourseList = () => {
const { formatMessage } = useIntl();
const hasCourses = appHooks.useHasCourses();
const hasCourses = reduxHooks.useHasCourses();
const {
filterOptions,
setPageNumber,

View File

@@ -1,11 +1,11 @@
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { useCourseListData, useIsCollapsed } from './hooks';
import CourseList from '.';
jest.mock('data/redux', () => ({
hooks: { useHasCourses: jest.fn() },
jest.mock('hooks', () => ({
reduxHooks: { useHasCourses: jest.fn() },
}));
jest.mock('./hooks', () => ({
@@ -19,7 +19,7 @@ jest.mock('containers/CourseFilterControls', () => ({
CourseFilterControls: 'CourseFilterControls',
}));
appHooks.useHasCourses.mockReturnValue(true);
reduxHooks.useHasCourses.mockReturnValue(true);
describe('CourseList', () => {
const defaultCourseListData = {
@@ -40,7 +40,7 @@ describe('CourseList', () => {
describe('no courses', () => {
test('snapshot', () => {
appHooks.useHasCourses.mockReturnValue(true);
reduxHooks.useHasCourses.mockReturnValue(true);
const wrapper = createWrapper();
expect(wrapper).toMatchSnapshot();
});

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useDispatch } from 'react-redux';
import { thunkActions } from 'data/redux';
import { apiHooks } from 'hooks';
import appMessages from 'messages';
@@ -12,8 +11,8 @@ export const useIsDashboardCollapsed = () => {
};
export const useInitializeDashboard = () => {
const dispatch = useDispatch();
React.useEffect(() => { dispatch(thunkActions.app.initialize()); }, [dispatch]);
const initialize = apiHooks.useInitializeApp();
React.useEffect(() => { initialize(); }, []); // eslint-disable-line
};
export const useDashboardMessages = () => {

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { thunkActions } from 'data/redux';
import { apiHooks } from 'hooks';
import appMessages from 'messages';
import * as hooks from './hooks';
@@ -14,14 +13,14 @@ jest.mock('@edx/paragon', () => ({
breakpoints: {},
}));
jest.mock('data/redux', () => ({
thunkActions: {
app: {
initialize: jest.fn(() => 'thunkActions.app.initialize'),
},
jest.mock('hooks', () => ({
apiHooks: {
useInitializeApp: jest.fn(),
},
}));
const initializeApp = jest.fn();
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
describe('CourseCard hooks', () => {
const { formatMessage } = useIntl();
@@ -42,13 +41,12 @@ describe('CourseCard hooks', () => {
});
describe('useInitializeDashboard', () => {
it('dispatches initialize thunk action on component load', () => {
const dispatch = useDispatch();
hooks.useInitializeDashboard();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([dispatch]);
expect(dispatch).not.toHaveBeenCalled();
expect(prereqs).toEqual([]);
expect(initializeApp).not.toHaveBeenCalled();
cb();
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.initialize());
expect(initializeApp).toHaveBeenCalledWith();
});
});
describe('useDashboardMessages', () => {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { RequestKeys } from 'data/constants/requests';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
@@ -17,10 +17,10 @@ import './index.scss';
export const Dashboard = () => {
hooks.useInitializeDashboard();
const { pageTitle } = hooks.useDashboardMessages();
const hasCourses = appHooks.useHasCourses();
const hasAvailableDashboards = appHooks.useHasAvailableDashboards();
const initIsPending = appHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = appHooks.useShowSelectSessionModal();
const hasCourses = reduxHooks.useHasCourses();
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
return (
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
<h1 className="sr-only">{pageTitle}</h1>

View File

@@ -1,6 +1,6 @@
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
@@ -14,13 +14,8 @@ import LoadingView from './LoadingView';
import hooks from './hooks';
import Dashboard from '.';
jest.mock('data/redux', () => ({
thunkActions: {
app: {
initialize: jest.fn(),
},
},
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useHasCourses: jest.fn(),
useHasAvailableDashboards: jest.fn(),
useShowSelectSessionModal: jest.fn(),
@@ -52,10 +47,10 @@ describe('Dashboard', () => {
initIsPending,
showSelectSessionModal,
}) => {
appHooks.useHasCourses.mockReturnValueOnce(hasCourses);
appHooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
appHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
appHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
reduxHooks.useHasCourses.mockReturnValueOnce(hasCourses);
reduxHooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
reduxHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
reduxHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
return shallow(<Dashboard />);
};

View File

@@ -4,7 +4,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
<ModalDialog
hasCloseButton={false}
isOpen={false}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div
@@ -48,7 +48,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: true 1`] = `
<ModalDialog
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div
@@ -92,7 +92,7 @@ exports[`EmailSettingsModal render snapshot: emails enabled, show: true 1`] = `
<ModalDialog
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { StrictDict } from 'utils';
import { hooks as appHooks, thunkActions } from 'data/redux';
import { reduxHooks, apiHooks } from 'hooks';
import * as module from './hooks';
@@ -12,22 +12,15 @@ export const state = StrictDict({
export const useEmailData = ({
closeModal,
cardId,
dispatch,
}) => {
const { hasOptedOutOfEmail } = appHooks.useCardEnrollmentData(cardId);
const { hasOptedOutOfEmail } = reduxHooks.useCardEnrollmentData(cardId);
const [isOptedOut, setIsOptedOut] = module.state.toggle(hasOptedOutOfEmail);
const onToggle = React.useCallback(
() => setIsOptedOut(!isOptedOut),
[setIsOptedOut, isOptedOut],
);
const save = React.useCallback(
() => {
// update email settings 2nd arg is true if opting in, false if opting out
dispatch(thunkActions.app.updateEmailSettings(cardId, !isOptedOut));
closeModal();
},
[cardId, closeModal, dispatch, isOptedOut],
);
const updateEmailSettings = apiHooks.useUpdateEmailSettings(cardId);
const onToggle = () => setIsOptedOut(!isOptedOut);
const save = () => {
updateEmailSettings(!isOptedOut);
closeModal();
};
return {
onToggle,

View File

@@ -1,22 +1,21 @@
import { MockUseState } from 'testUtils';
import { hooks as appHooks, thunkActions } from 'data/redux';
import { reduxHooks, apiHooks } from 'hooks';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(),
},
thunkActions: {
app: {
updateEmailSettings: jest.fn(),
},
apiHooks: {
useUpdateEmailSettings: jest.fn(),
},
}));
const cardId = 'my-test-course-number';
const closeModal = jest.fn();
const dispatch = jest.fn();
const updateEmailSettings = jest.fn();
apiHooks.useUpdateEmailSettings.mockReturnValue(updateEmailSettings);
const state = new MockUseState(hooks);
@@ -31,41 +30,40 @@ describe('EmailSettingsModal hooks', () => {
describe('useEmailData', () => {
beforeEach(() => {
state.mock();
appHooks.useCardEnrollmentData.mockReturnValueOnce({ hasOptedOutOfEmail: true });
out = hooks.useEmailData({ closeModal, cardId, dispatch });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasOptedOutOfEmail: true });
out = hooks.useEmailData({ closeModal, cardId });
});
afterEach(state.restore);
test('loads enrollment data based on course number', () => {
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
it('loads enrollment data based on course number', () => {
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('initializes toggle value to cardData.hasOptedOutOfEmail', () => {
it('initializes toggle value to cardData.hasOptedOutOfEmail', () => {
state.expectInitializedWith(state.keys.toggle, true);
expect(out.isOptedOut).toEqual(true);
appHooks.useCardEnrollmentData.mockReturnValueOnce({ hasOptedOutOfEmail: false });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasOptedOutOfEmail: false });
out = hooks.useEmailData({ closeModal, cardId });
state.expectInitializedWith(state.keys.toggle, false);
expect(out.isOptedOut).toEqual(false);
});
it('initializes email settings hok with cardId', () => {
expect(apiHooks.useUpdateEmailSettings).toHaveBeenCalledWith(cardId);
});
describe('onToggle - returned callback', () => {
it('is based on toggle state value', () => {
expect(out.onToggle.useCallback.prereqs).toEqual([state.setState.toggle, out.isOptedOut]);
});
it('sets toggle state value to opposite current value', () => {
out.onToggle.useCallback.cb();
out.onToggle();
expect(state.setState.toggle).toHaveBeenCalledWith(!out.isOptedOut);
});
});
describe('save', () => {
it('calls dispatch with thunkActions.app.updateEmailSettings', () => {
out.save.useCallback.cb();
expect(thunkActions.app.updateEmailSettings).toHaveBeenCalledWith(cardId, !out.isOptedOut);
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.updateEmailSettings(cardId, !out.isOptedOut));
it('calls updateEmailSettings', () => {
out.save();
expect(updateEmailSettings).toHaveBeenCalledWith(!out.isOptedOut);
});
it('calls closeModal', () => {
out.save.useCallback.cb();
out.save();
expect(closeModal).toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -10,7 +9,7 @@ import {
ModalDialog,
} from '@edx/paragon';
import { nullMethod } from 'hooks';
import { nullMethod } from 'utils';
import useEmailData from './hooks';
import messages from './messages';
@@ -20,12 +19,11 @@ export const EmailSettingsModal = ({
show,
cardId,
}) => {
const dispatch = useDispatch();
const {
isOptedOut,
onToggle,
save,
} = useEmailData({ dispatch, closeModal, cardId });
} = useEmailData({ closeModal, cardId });
const { formatMessage } = useIntl();
return (

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { shallow } from 'enzyme';
import hooks from './hooks';
@@ -22,8 +21,6 @@ const props = {
cardId: 'test-course-number',
};
const dispatch = useDispatch();
describe('EmailSettingsModal', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -33,10 +30,9 @@ describe('EmailSettingsModal', () => {
hooks.mockReturnValueOnce(hookProps);
shallow(<EmailSettingsModal {...props} />);
});
it('calls hook w/ dispatch from redux hook, and closeModal, cardId from props', () => {
it('calls hook w/ closeModal and cardId from props', () => {
expect(hooks).toHaveBeenCalledWith({
closeModal: props.closeModal,
dispatch,
cardId: props.cardId,
});
});

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { StrictDict } from 'utils';
import track from 'tracking';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import * as module from './hooks';
@@ -14,7 +14,7 @@ const { modalOpened, modalClosed, modalCTAClicked } = track.enterpriseDashboard;
export const useEnterpriseDashboardHook = () => {
const [showModal, setShowModal] = module.state.showModal(true);
const dashboard = appHooks.useEnterpriseDashboardData();
const dashboard = reduxHooks.useEnterpriseDashboardData();
const trackOpened = modalOpened(dashboard.enterpriseUUID);
const trackClose = modalClosed(dashboard.enterpriseUUID, 'Cancel button');

View File

@@ -1,11 +1,11 @@
import { MockUseState } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
},
}));
@@ -33,7 +33,7 @@ const state = new MockUseState(hooks);
const enterpriseDashboardData = { label: 'edX, Inc.', url: '/edx-dashboard' };
describe('EnterpriseDashboard hooks', () => {
appHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
reduxHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
describe('state values', () => {
state.testGetter(state.keys.showModal);

View File

@@ -6,8 +6,8 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { useIsCollapsed, findCoursesNavDropdownClicked } from './hooks';
import messages from './messages';
@@ -15,10 +15,10 @@ import messages from './messages';
export const AuthenticatedUserDropdown = ({ username }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const { profileImage } = authenticatedUser;
const dashboard = appHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const { profileImage } = authenticatedUser;
return (
<Dropdown variant={isCollapsed ? 'light' : 'dark'} className="user-dropdown ml-1">

View File

@@ -1,6 +1,6 @@
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
import { useIsCollapsed } from './hooks';
@@ -11,8 +11,8 @@ jest.mock('@edx/frontend-platform/react', () => ({
},
},
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'test-course-search-url',
@@ -35,13 +35,13 @@ describe('AuthenticatedUserDropdown', () => {
describe('snapshots', () => {
test('with enterprise dashboard', () => {
appHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<AuthenticatedUserDropdown {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('without enterprise dashboard and expanded', () => {
appHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<AuthenticatedUserDropdown {...props} />);
expect(wrapper).toMatchSnapshot();

View File

@@ -1,9 +1,7 @@
import React from 'react';
import { StrictDict } from 'utils';
import { hooks as appHooks, thunkActions } from 'data/redux';
import { useDispatch } from 'react-redux';
import { apiHooks, reduxHooks } from 'hooks';
import * as module from './hooks';
@@ -13,16 +11,16 @@ export const state = StrictDict({
});
export const useConfirmEmailBannerData = () => {
const dispatch = useDispatch();
const { isNeeded } = appHooks.useEmailConfirmationData();
const { isNeeded } = reduxHooks.useEmailConfirmationData();
const [showPageBanner, setShowPageBanner] = module.state.showPageBanner(isNeeded);
const [showConfirmModal, setShowConfirmModal] = module.state.showConfirmModal(false);
const closePageBanner = () => setShowPageBanner(false);
const closeConfirmModal = () => setShowConfirmModal(false);
const openConfirmModal = () => setShowConfirmModal(true);
const sendConfirmEmail = apiHooks.useSendConfirmEmail();
const openConfirmModalButtonClick = () => {
dispatch(thunkActions.app.sendConfirmEmail());
sendConfirmEmail();
openConfirmModal();
closePageBanner();
};

View File

@@ -1,19 +1,20 @@
import { MockUseState } from 'testUtils';
import { hooks as appHooks, thunkActions } from 'data/redux';
import { reduxHooks, apiHooks } from 'hooks';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useEmailConfirmationData: jest.fn(),
},
thunkActions: {
app: {
sendConfirmEmail: jest.fn(),
},
apiHooks: {
useSendConfirmEmail: jest.fn(),
},
}));
const sendConfirmEmail = jest.fn();
apiHooks.useSendConfirmEmail.mockReturnValue(sendConfirmEmail);
const emailConfirmation = {
isNeeded: true,
};
@@ -34,14 +35,14 @@ describe('ConfirmEmailBanner hooks', () => {
afterEach(state.restore);
test('show page banner on unverified email', () => {
appHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
out = hooks.useConfirmEmailBannerData();
expect(out.isNeeded).toEqual(emailConfirmation.isNeeded);
appHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
});
test('hide page banner on verified email', () => {
appHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
out = hooks.useConfirmEmailBannerData();
expect(out.isNeeded).toEqual(false);
});
@@ -50,7 +51,7 @@ describe('ConfirmEmailBanner hooks', () => {
describe('behavior', () => {
beforeEach(() => {
state.mock();
appHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
out = hooks.useConfirmEmailBannerData();
});
afterEach(state.restore);
@@ -65,7 +66,7 @@ describe('ConfirmEmailBanner hooks', () => {
test('openConfirmModalButtonClick', () => {
out.openConfirmModalButtonClick();
expect(state.values.showConfirmModal).toEqual(true);
expect(thunkActions.app.sendConfirmEmail).toBeCalled();
expect(sendConfirmEmail).toBeCalled();
});
test('userConfirmEmailButtonClick', () => {
out.userConfirmEmailButtonClick();

View File

@@ -31,6 +31,7 @@ exports[`LearnerDashboardHeader snapshots with collapsed 1`] = `
className="my-auto ml-1 d-flex"
>
<IconButton
alt="Course search"
as="a"
href="test-course-search-url"
iconAs="Icon"

View File

@@ -10,7 +10,7 @@ import {
import topBanner from 'assets/top_stripe.svg';
import MasqueradeBar from 'containers/MasqueradeBar';
import urls from 'data/services/lms/urls';
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import GreetingBanner from './GreetingBanner';
@@ -28,7 +28,7 @@ export const UserMenu = () => {
export const LearnerDashboardHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
@@ -52,6 +52,7 @@ export const LearnerDashboardHeader = () => {
{isCollapsed ? (
<div className="my-auto ml-1 d-flex">
<IconButton
alt={formatMessage(messages.courseSearchAlt)}
as="a"
href={courseSearchUrl}
variant="primary"

View File

@@ -16,8 +16,8 @@ jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (href) => jest.fn().mockName(`findCoursesNavClicked('${href}')`),
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'test-course-search-url',
})),

View File

@@ -62,6 +62,11 @@ const messages = defineMessages({
defaultMessage: 'Explore courses',
description: 'Header link for switching to course page.',
},
courseSearchAlt: {
id: 'leanerDashboard.courseSearchAlt',
defaultMessage: 'Course search',
description: 'Alt-text for course search icon button',
},
});
export default messages;

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useDispatch } from 'react-redux';
import { thunkActions, hooks as appHooks } from 'data/redux';
import { apiHooks, reduxHooks } from 'hooks';
import { StrictDict } from 'utils';
import * as module from './hooks';
@@ -35,28 +34,26 @@ export const getMasqueradeErrorMessage = (errorStatus) => {
export const useMasqueradeBarData = ({
authenticatedUser,
}) => {
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const canMasquerade = authenticatedUser?.administrator;
const handleMasqueradeSubmit = (user) => (e) => {
dispatch(thunkActions.app.masqueradeAs(user));
e.preventDefault();
};
const handleClearMasquerade = () => dispatch(thunkActions.app.clearMasquerade());
const handleMasqueradeAs = apiHooks.useMasqueradeAs();
const handleClearMasquerade = apiHooks.useClearMasquerade();
const {
isMasquerading,
isMasqueradingFailed,
isMasqueradingPending,
masqueradeErrorStatus,
} = appHooks.useMasqueradeData();
} = reduxHooks.useMasqueradeData();
const { masqueradeInput, handleMasqueradeInputChange } = module.useMasqueradeInput();
const masqueradeErrorMessage = getMasqueradeErrorMessage(masqueradeErrorStatus);
const handleMasqueradeSubmit = (user) => (e) => {
handleMasqueradeAs(user);
e.preventDefault();
};
return {
canMasquerade,
canMasquerade: authenticatedUser?.administrator,
isMasquerading,
isMasqueradingFailed,
isMasqueradingPending,

View File

@@ -1,22 +1,25 @@
import { MockUseState } from 'testUtils';
import { thunkActions, hooks as appHooks } from 'data/redux';
import { apiHooks, reduxHooks } from 'hooks';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('data/redux', () => ({
thunkActions: {
app: {
masqueradeAs: jest.fn(),
clearMasquerade: jest.fn(),
},
jest.mock('hooks', () => ({
apiHooks: {
useMasqueradeAs: jest.fn(),
useClearMasquerade: jest.fn(),
},
hooks: {
reduxHooks: {
useMasqueradeData: jest.fn(),
},
}));
const masqueradeAs = jest.fn();
const clearMasquerade = jest.fn();
apiHooks.useMasqueradeAs.mockReturnValue(masqueradeAs);
apiHooks.useClearMasquerade.mockReturnValue(clearMasquerade);
const state = new MockUseState(hooks);
const testValue = 'test-value';
describe('MasqueradeBar hooks', () => {
const authenticatedUser = {
@@ -29,7 +32,7 @@ describe('MasqueradeBar hooks', () => {
masqueradeErrorStatus: null,
};
const createHook = (masqueradeData = {}, user) => {
appHooks.useMasqueradeData.mockReturnValueOnce({
reduxHooks.useMasqueradeData.mockReturnValueOnce({
...defaultMasqueradeData,
...masqueradeData,
});
@@ -65,23 +68,23 @@ describe('MasqueradeBar hooks', () => {
test('handleMasqueradeInputChange', () => {
const out = createHook();
expect(state.stateVals.masqueradeInput).toEqual('');
out.handleMasqueradeInputChange({ target: { value: 'test' } });
expect(state.setState.masqueradeInput).toHaveBeenCalledWith('test');
out.handleMasqueradeInputChange({ target: { value: testValue } });
expect(state.setState.masqueradeInput).toHaveBeenCalledWith(testValue);
});
test('handleMasqueradeSubmit', () => {
const out = createHook();
const preventDefault = jest.fn();
// make sure submit doesn't refresh the page
out.handleMasqueradeSubmit('test')({
out.handleMasqueradeSubmit(testValue)({
preventDefault,
});
expect(thunkActions.app.masqueradeAs).toHaveBeenCalledWith('test');
expect(masqueradeAs).toHaveBeenCalledWith(testValue);
expect(preventDefault).toHaveBeenCalled();
});
test('handleClearMasquerade', () => {
const out = createHook();
out.handleClearMasquerade();
expect(thunkActions.app.clearMasquerade).toHaveBeenCalled();
expect(clearMasquerade).toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,10 @@
import { hooks as appHooks } from 'data/redux';
import { reduxHooks } from 'hooks';
export const useProgramData = ({
cardId,
}) => ({
courseTitle: appHooks.useCardCourseData(cardId).title,
relatedPrograms: appHooks.useCardRelatedProgramsData(cardId).list,
courseTitle: reduxHooks.useCardCourseData(cardId).title,
relatedPrograms: reduxHooks.useCardRelatedProgramsData(cardId).list,
});
export default useProgramData;

View File

@@ -7,7 +7,7 @@ import {
Container, Row, Col, ModalDialog,
} from '@edx/paragon';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import ProgramCard from './components/ProgramCard';
import messages from './messages';
import './index.scss';
@@ -18,8 +18,8 @@ export const RelatedProgramsModal = ({
cardId,
}) => {
const { formatMessage } = useIntl();
const { courseName } = hooks.useCardCourseData(cardId);
const relatedPrograms = hooks.useCardRelatedProgramsData(cardId).list;
const { courseName } = reduxHooks.useCardCourseData(cardId);
const relatedPrograms = reduxHooks.useCardRelatedProgramsData(cardId).list;
return (
<ModalDialog
title={formatMessage(messages.header)}

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { hooks } from 'data/redux';
import { reduxHooks } from 'hooks';
import RelatedProgramsModal from '.';
jest.mock('./components/ProgramCard', () => 'ProgramCard');
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(),
useCardRelatedProgramsData: jest.fn(),
},
@@ -41,13 +41,13 @@ const props = {
describe('RelatedProgramsModal', () => {
beforeEach(() => {
hooks.useCardCourseData.mockReturnValueOnce(courseData);
hooks.useCardRelatedProgramsData.mockReturnValueOnce(programData);
reduxHooks.useCardCourseData.mockReturnValueOnce(courseData);
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce(programData);
});
it('initializes hooks with cardId', () => {
shallow(<RelatedProgramsModal {...props} />);
expect(hooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(hooks.useCardRelatedProgramsData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardRelatedProgramsData).toHaveBeenCalledWith(cardId);
});
test('snapshot: open', () => {
expect(shallow(<RelatedProgramsModal {...props} />)).toMatchSnapshot();

View File

@@ -6,7 +6,7 @@ exports[`SelectSessionModal snapshot empty modal with leave option 1`] = `
hasCloseButton={false}
isFullscreenOnMobile={true}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
size="md"
title="test-header"
>
@@ -50,7 +50,7 @@ exports[`SelectSessionModal snapshot modal with leave option 1`] = `
hasCloseButton={false}
isFullscreenOnMobile={true}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
size="md"
title="test-header"
>
@@ -118,7 +118,7 @@ exports[`SelectSessionModal snapshot modal without leave option 1`] = `
hasCloseButton={false}
isFullscreenOnMobile={true}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
size="md"
title="test-header"
>

View File

@@ -1,32 +1,35 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import track from 'tracking';
import { hooks as appHooks, thunkActions } from 'data/redux';
import * as module from './hooks';
import { reduxHooks, apiHooks } from 'hooks';
import { LEAVE_OPTION } from './constants';
import messages from './messages';
import * as module from './hooks';
export const state = StrictDict({
selectedSession: (val) => React.useState(val), // eslint-disable-line
});
export const useSelectSessionModalData = () => {
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const selectedCardId = appHooks.useSelectSessionModalData().cardId;
const selectedCardId = reduxHooks.useSelectSessionModalData().cardId;
const {
availableSessions,
isFulfilled,
} = appHooks.useCardEntitlementData(selectedCardId);
const { title: courseTitle } = appHooks.useCardCourseData(selectedCardId);
const { courseId } = appHooks.useCardCourseRunData(selectedCardId) || {};
const { isEnrolled } = appHooks.useCardEnrollmentData(selectedCardId);
} = reduxHooks.useCardEntitlementData(selectedCardId);
const { title: courseTitle } = reduxHooks.useCardCourseData(selectedCardId);
const { courseId } = reduxHooks.useCardCourseRunData(selectedCardId) || {};
const { isEnrolled } = reduxHooks.useCardEnrollmentData(selectedCardId);
const leaveEntitlementSession = apiHooks.useLeaveEntitlementSession(selectedCardId);
const switchEntitlementEnrollment = apiHooks.useSwitchEntitlementEnrollment(selectedCardId);
const newEntitlementEnrollment = apiHooks.useNewEntitlementEnrollment(selectedCardId);
const [selectedSession, setSelectedSession] = module.state.selectedSession(courseId || null);
@@ -39,8 +42,7 @@ export const useSelectSessionModalData = () => {
header = formatMessage(messages.selectSessionHeader, { courseTitle });
hint = formatMessage(messages.selectSessionHint);
}
const updateCardIdCallback = appHooks.useUpdateSelectSessionModalCallback;
const closeSessionModal = updateCardIdCallback(null);
const closeSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(null);
const trackNewSession = track.entitlements.newSession(selectedSession);
const trackSwitchSession = track.entitlements.switchSession(selectedCardId, selectedSession);
@@ -50,13 +52,13 @@ export const useSelectSessionModalData = () => {
const handleSubmit = () => {
if (selectedSession === LEAVE_OPTION) {
trackLeaveSession();
dispatch(thunkActions.app.leaveEntitlementSession(selectedCardId));
leaveEntitlementSession();
} else if (isEnrolled) {
trackSwitchSession();
dispatch(thunkActions.app.switchEntitlementEnrollment(selectedCardId, selectedSession));
switchEntitlementEnrollment(selectedSession);
} else {
trackNewSession();
dispatch(thunkActions.app.newEntitlementEnrollment(selectedCardId, selectedSession));
newEntitlementEnrollment(selectedSession);
}
closeSessionModal();
};

View File

@@ -1,10 +1,8 @@
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { MockUseState } from 'testUtils';
import { hooks as appHooks, thunkActions } from 'data/redux';
import track from 'tracking';
import { reduxHooks, apiHooks } from 'hooks';
import { LEAVE_OPTION } from './constants';
import messages from './messages';
@@ -17,31 +15,37 @@ jest.mock('tracking', () => ({
leaveSession: jest.fn(),
},
}));
jest.mock('data/redux', () => ({
hooks: {
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
useSelectSessionModalData: jest.fn(),
useUpdateSelectSessionModalCallback: jest.fn((...args) => () => ({
updateSelectSession: args,
})),
useUpdateSelectSessionModalCallback: jest.fn(),
},
actions: {
app: {
updateSelectSessionModal: jest.fn(),
},
},
thunkActions: {
app: {
switchEntitlementEnrollment: jest.fn((...args) => ({ switchEntitlementEnrollment: args })),
leaveEntitlementSession: jest.fn((...args) => ({ leaveEntitlementSession: args })),
newEntitlementEnrollment: jest.fn((...args) => ({ newEntitlementEnrollment: args })),
},
apiHooks: {
useSwitchEntitlementEnrollment: jest.fn((...args) => ({ switchEntitlementEnrollment: args })),
useLeaveEntitlementSession: jest.fn((...args) => ({ leaveEntitlementSession: args })),
useNewEntitlementEnrollment: jest.fn((...args) => ({ newEntitlementEnrollment: args })),
},
}));
const updateSelectSessionModalCallback = jest.fn();
reduxHooks.useUpdateSelectSessionModalCallback.mockReturnValue(updateSelectSessionModalCallback);
const newEntitlementEnrollment = jest.fn();
apiHooks.useNewEntitlementEnrollment.mockReturnValue(newEntitlementEnrollment);
const switchEntitlementEnrollment = jest.fn();
apiHooks.useSwitchEntitlementEnrollment.mockReturnValue(switchEntitlementEnrollment);
const leaveEntitlementSession = jest.fn();
apiHooks.useLeaveEntitlementSession.mockReturnValue(leaveEntitlementSession);
const trackNewSession = jest.fn();
track.entitlements.newSession.mockReturnValue(trackNewSession);
const trackLeaveSession = jest.fn();
track.entitlements.leaveSession.mockReturnValue(trackLeaveSession);
const trackSwitchSession = jest.fn();
track.entitlements.switchSession.mockReturnValue(trackSwitchSession);
const state = new MockUseState(hooks);
const selectedCardId = 'test-selected-card-id';
const courseTitle = 'course-title: brown fox';
@@ -58,19 +62,11 @@ const entitlementData = {
};
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const testValue = 'test-value';
const courseId = 'test-course-id';
appHooks.useCardCourseRunData.mockReturnValue({ courseId });
const newSession = jest.fn();
const switchSession = jest.fn();
const leaveSession = jest.fn();
track.entitlements.newSession.mockReturnValue(newSession);
track.entitlements.switchSession.mockReturnValue(switchSession);
track.entitlements.leaveSession.mockReturnValue(leaveSession);
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
describe('SelectSessionModal hooks', () => {
let out;
@@ -89,11 +85,11 @@ describe('SelectSessionModal hooks', () => {
entitlement = {},
selectSession = {},
}) => {
appHooks.useCardCourseData.mockReturnValueOnce({ title: courseTitle, ...course });
appHooks.useCardCourseRunData.mockReturnValueOnce({ courseId, ...courseRun });
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, ...enrollment });
appHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
appHooks.useSelectSessionModalData.mockReturnValueOnce({ cardId: selectedCardId, ...selectSession });
reduxHooks.useCardCourseData.mockReturnValueOnce({ title: courseTitle, ...course });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ courseId, ...courseRun });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, ...enrollment });
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
reduxHooks.useSelectSessionModalData.mockReturnValueOnce({ cardId: selectedCardId, ...selectSession });
out = hooks.useSelectSessionModalData();
};
beforeEach(() => {
@@ -101,11 +97,30 @@ describe('SelectSessionModal hooks', () => {
runHook({});
});
describe('initialization', () => {
test('loads entitlement data based on course number', () => {
expect(appHooks.useCardEntitlementData).toHaveBeenCalledWith(selectedCardId);
it('loads redux data based on selected card id', () => {
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(selectedCardId);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(selectedCardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(selectedCardId);
});
test('get course title based on course number', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(selectedCardId);
it('initializes enrollment hooks with selected card id', () => {
expect(apiHooks.useLeaveEntitlementSession).toHaveBeenCalledWith(selectedCardId);
expect(apiHooks.useNewEntitlementEnrollment).toHaveBeenCalledWith(selectedCardId);
expect(apiHooks.useSwitchEntitlementEnrollment).toHaveBeenCalledWith(selectedCardId);
});
it('initializes selected session with courseId if available', () => {
state.expectInitializedWith(state.keys.selectedSession, courseId);
});
it('initializes selected session with null if courseId not available', () => {
runHook({ courseRun: { courseId: undefined } });
state.expectInitializedWith(state.keys.selectedSession, null);
});
it('initializes update callback with null', () => {
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(null);
});
it('initializes tracking methods', () => {
expect(track.entitlements.newSession).toHaveBeenCalledWith(courseId);
expect(track.entitlements.leaveSession).toHaveBeenCalledWith(selectedCardId);
expect(track.entitlements.switchSession).toHaveBeenCalledWith(selectedCardId, courseId);
});
});
@@ -127,36 +142,30 @@ describe('SelectSessionModal hooks', () => {
});
describe('handleSubmit', () => {
describe('if LEAVE_OPTION is selected', () => {
it('dispatches leaveEntitlementSession', () => {
it('calls and tracks leaveEntitlementSession', () => {
state.mockVal(state.keys.selectedSession, LEAVE_OPTION);
runHook({});
out.handleSubmit();
expect(leaveSession).toHaveBeenCalledWith();
expect(dispatch).toHaveBeenCalledWith(
thunkActions.app.leaveEntitlementSession(selectedCardId),
);
expect(leaveEntitlementSession).toHaveBeenCalledWith();
expect(trackLeaveSession).toHaveBeenCalled();
});
});
describe('if not enrolled in a session yet', () => {
it('dispatches newEntitlementEnrollment with selected card ID and session', () => {
it('calls and tracks newEntitlementEnrollment with selected card ID and session', () => {
state.mockVal(state.keys.selectedSession, testValue);
runHook({});
out.handleSubmit();
expect(newSession).toHaveBeenCalledWith();
expect(dispatch).toHaveBeenCalledWith(
thunkActions.app.newEntitlementEnrollment(selectedCardId, testValue),
);
expect(newEntitlementEnrollment).toHaveBeenCalledWith(testValue);
expect(trackNewSession).toHaveBeenCalled();
});
});
describe('if enrolled in a session already, selecting a new session', () => {
it('dispatches swtichEntitlementEnrollment with selected card ID and session', () => {
it('calls and tracks swtichEntitlementEnrollment w/ selected card ID and session', () => {
state.mockVal(state.keys.selectedSession, testValue);
runHook({ enrollment: { isEnrolled: true } });
out.handleSubmit();
expect(switchSession).toHaveBeenCalledWith();
expect(dispatch).toHaveBeenCalledWith(
thunkActions.app.switchEntitlementEnrollment(selectedCardId, testValue),
);
expect(switchEntitlementEnrollment).toHaveBeenCalledWith(testValue);
expect(trackSwitchSession).toHaveBeenCalled();
});
});
});
@@ -178,7 +187,7 @@ describe('SelectSessionModal hooks', () => {
});
test('closeSessionModal returns update callback wth dispatch and null card id', () => {
expect(out.closeSessionModal()).toEqual(
appHooks.useUpdateSelectSessionModalCallback(null)(),
reduxHooks.useUpdateSelectSessionModalCallback(null)(),
);
});
});

View File

@@ -8,8 +8,8 @@ import {
ModalDialog,
} from '@edx/paragon';
import { nullMethod } from 'hooks';
import { dateFormatter } from 'utils';
import { utilHooks } from 'hooks';
import { nullMethod, dateFormatter } from 'utils';
import useSelectSessionModalData from './hooks';
import { LEAVE_OPTION } from './constants';
@@ -28,7 +28,8 @@ export const SelectSessionModal = () => {
selectedSession,
} = useSelectSessionModalData();
const { formatMessage, formatDate } = useIntl();
const formatDate = utilHooks.useFormatDate();
const { formatMessage } = useIntl();
return (
<ModalDialog

View File

@@ -5,7 +5,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.confirm 1`] = `
hasCloseButton={false}
isFullscreenOnMobile={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div
@@ -29,7 +29,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason g
hasCloseButton={false}
isFullscreenOnMobile={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div
@@ -53,7 +53,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason s
hasCloseButton={false}
isFullscreenOnMobile={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div
@@ -77,7 +77,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.reason, should be
hasCloseButton={false}
isFullscreenOnMobile={true}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[MockFunction utils.nullMethod]}
title=""
>
<div

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { StrictDict } from 'utils';
import { thunkActions } from 'data/redux';
import { apiHooks } from 'hooks';
import { useUnenrollReasons } from './reasons';
import * as module from '.';
@@ -16,10 +16,11 @@ export const modalStates = StrictDict({
finished: 'finished',
});
export const useUnenrollData = ({ closeModal, dispatch, cardId }) => {
export const useUnenrollData = ({ closeModal, cardId }) => {
const [isConfirmed, setIsConfirmed] = module.state.confirmed(false);
const confirm = () => setIsConfirmed(true);
const reason = useUnenrollReasons({ dispatch, cardId });
const reason = useUnenrollReasons({ cardId });
const refreshList = apiHooks.useInitializeApp();
let modalState;
if (isConfirmed) {
@@ -35,7 +36,7 @@ export const useUnenrollData = ({ closeModal, dispatch, cardId }) => {
reason.handleClear();
};
const closeAndRefresh = () => {
dispatch(thunkActions.app.refreshList());
refreshList();
close();
};

View File

@@ -1,4 +1,4 @@
import { thunkActions } from 'data/redux';
import { apiHooks } from 'hooks';
import { MockUseState } from 'testUtils';
import * as reasons from './reasons';
@@ -8,13 +8,16 @@ jest.mock('./reasons', () => ({
useUnenrollReasons: jest.fn(),
}));
jest.mock('data/redux/thunkActions/app', () => ({
refreshList: jest.fn((args) => ({ refreshList: args })),
unenrollFromCourse: jest.fn((...args) => ({ unenrollFromCourse: args })),
jest.mock('hooks', () => ({
apiHooks: {
useInitializeApp: jest.fn(),
},
}));
const state = new MockUseState(hooks);
const testValue = 'test-value';
const initializeApp = jest.fn();
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
let out;
const mockReason = {
@@ -29,11 +32,10 @@ describe('UnenrollConfirmModal hooks', () => {
beforeEach(() => {
reasons.useUnenrollReasons.mockImplementation(useUnenrollReasons);
});
const dispatch = jest.fn();
const closeModal = jest.fn();
const cardId = 'test-card-id';
const createUseUnenrollData = () => hooks.useUnenrollData({ closeModal, dispatch, cardId });
const createUseUnenrollData = () => hooks.useUnenrollData({ closeModal, cardId });
describe('state fields', () => {
state.testGetter(state.keys.confirmed);
@@ -72,9 +74,9 @@ describe('UnenrollConfirmModal hooks', () => {
expect(state.setState.confirmed).toHaveBeenCalledWith(false);
expect(mockReason.handleClear).toHaveBeenCalled();
});
it('dispatches refreshList thunkAction', () => {
it('calls initializeApp api method', () => {
out.closeAndRefresh();
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.refreshList());
expect(initializeApp).toHaveBeenCalled();
});
});
describe('modalState', () => {

View File

@@ -1,7 +1,10 @@
import React from 'react';
import { thunkActions, hooks as appHooks } from 'data/redux';
import { useValueCallback } from 'hooks';
import {
apiHooks,
reduxHooks,
utilHooks,
} from 'hooks';
import { StrictDict } from 'utils';
import track from 'tracking';
@@ -14,18 +17,7 @@ export const state = StrictDict({
isSubmitted: (val) => React.useState(val), //eslint-disable-line
});
export const useTrackUnenrollReasons = ({ cardId, submittedReason }) => {
const { isEntitlement } = appHooks.useCardEntitlementData(cardId);
return appHooks.useTrackCourseEvent(
track.engagement.unenrollReason,
cardId,
submittedReason,
isEntitlement,
);
};
export const useUnenrollReasons = ({
dispatch,
cardId,
}) => {
// The selected option element from the menu
@@ -38,10 +30,19 @@ export const useUnenrollReasons = ({
// Did the user submit an unenrollment reason
const [isSubmitted, setIsSubmitted] = module.state.isSubmitted(false);
const { isEntitlement } = reduxHooks.useCardEntitlementData(cardId);
const submittedReason = selectedReason === 'custom' ? customOption : selectedReason;
const hasReason = ![null, ''].includes(submittedReason);
const handleTrackReasons = module.useTrackUnenrollReasons({ cardId, submittedReason });
const handleTrackReasons = reduxHooks.useTrackCourseEvent(
track.engagement.unenrollReason,
cardId,
submittedReason,
isEntitlement,
);
const unenrollFromCourse = apiHooks.useUnenrollFromCourse(cardId);
const handleClear = () => {
setSelectedReason(null);
@@ -52,24 +53,27 @@ export const useUnenrollReasons = ({
const handleSkip = () => {
setIsSkipped(true);
dispatch(thunkActions.app.unenrollFromCourse(cardId));
unenrollFromCourse();
};
const handleSubmit = (e) => {
handleTrackReasons(e);
setIsSubmitted(true);
dispatch(thunkActions.app.unenrollFromCourse(cardId, submittedReason));
unenrollFromCourse();
};
const handleCustomOptionChange = utilHooks.useValueCallback(setCustomOption);
const handleSelectOption = utilHooks.useValueCallback(setSelectedReason);
return {
customOption: { value: customOption, onChange: useValueCallback(setCustomOption) },
customOption: { value: customOption, onChange: handleCustomOptionChange },
handleClear,
handleSkip,
handleSubmit,
hasReason,
isSkipped,
isSubmitted,
selectOption: useValueCallback(setSelectedReason),
selectOption: handleSelectOption,
submittedReason,
};
};

View File

@@ -1,154 +1,191 @@
import { keyStore } from 'utils';
import { thunkActions, hooks as appHooks } from 'data/redux';
import { useValueCallback } from 'hooks';
import { MockUseState } from 'testUtils';
import track from 'tracking';
import {
apiHooks,
reduxHooks,
utilHooks,
} from 'hooks';
import * as hooks from './reasons';
jest.mock('data/redux', () => ({
thunkActions: {
app: {
refreshList: jest.fn((args) => ({ refreshList: args })),
unenrollFromCourse: jest.fn((...args) => ({ unenrollFromCourse: args })),
},
jest.mock('hooks', () => ({
apiHooks: {
useUnenrollFromCourse: jest.fn((...args) => ({ unenrollFromCourse: args })),
},
hooks: {
reduxHooks: {
useCardEntitlementData: jest.fn(),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('hooks', () => ({
useValueCallback: jest.fn((cb, prereqs) => ({ useValueCallback: { cb, prereqs } })),
utilHooks: {
useValueCallback: jest.fn((cb, prereqs) => ({ useValueCallback: { cb, prereqs } })),
},
}));
const state = new MockUseState(hooks);
const testValue = 'test-value';
const testValue2 = 'test-value2';
const moduleKeys = keyStore(hooks);
const unenrollFromCourse = jest.fn((...args) => ({ unenrollFromCourse: args }));
const trackCourseEvent = jest.fn((e) => ({ courseEvent: e }));
apiHooks.useUnenrollFromCourse.mockReturnValue(unenrollFromCourse);
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
let out;
const cardId = 'test-card-id';
const loadHook = (isEntitlement = false) => {
reduxHooks.useCardEntitlementData.mockReturnValue({ isEntitlement });
out = hooks.useUnenrollReasons({ cardId });
};
describe('UnenrollConfirmModal reasons hooks', () => {
const dispatch = jest.fn();
const cardId = 'test-card-id';
const createUseUnenrollReasons = () => hooks.useUnenrollReasons({ dispatch, cardId });
describe('state fields', () => {
state.testGetter(state.keys.customOption);
state.testGetter(state.keys.isSkipped);
state.testGetter(state.keys.isSubmitted);
state.testGetter(state.keys.selectedReason);
});
describe('useTrackUnenrollReasons', () => {
it('returns trackCourseEvent for unenroll with submitted reason and isEntitlement', () => {
appHooks.useCardEntitlementData.mockReturnValue({ isEntitlement: false });
const args = { cardId, submittedReason: testValue };
expect(hooks.useTrackUnenrollReasons(args)).toEqual(appHooks.useTrackCourseEvent(
track.engagement.unenrollReason,
args.cardId,
args.submittedReason,
false,
));
});
});
describe('useUnenrollReasons', () => {
const trackReasonsEvent = jest.fn((e) => ({ trackReasonsEvent: e }));
let useTrackUnenrollReasons;
beforeEach(() => {
jest.clearAllMocks();
state.mock();
appHooks.useCardEntitlementData.mockReturnValue({ isEntitlement: false });
useTrackUnenrollReasons = jest.spyOn(hooks, moduleKeys.useTrackUnenrollReasons)
.mockImplementation(() => trackReasonsEvent);
out = createUseUnenrollReasons();
loadHook();
});
afterEach(() => {
state.restore();
});
describe('customOption', () => {
test('customOption.value returns custom option', () => {
state.mockVal(state.keys.customOption, testValue);
expect(createUseUnenrollReasons().customOption.value).toEqual(testValue);
});
test('customOption.onChange returns valueCallback for setCustomOption', () => {
expect(out.customOption.onChange).toEqual(useValueCallback(state.setState.customOption));
});
});
describe('handleClear method', () => {
it('resets selected and submitted reasons, custom option and isSkipped', () => {
out.handleClear();
expect(state.setState.selectedReason).toHaveBeenCalledWith(null);
expect(state.setState.customOption).toHaveBeenCalledWith('');
expect(state.setState.isSkipped).toHaveBeenCalledWith(false);
expect(state.setState.isSubmitted).toHaveBeenCalledWith(false);
});
});
test('handleSkip sets isSkipped and isSubmitted, and unenrolls w/out a reason', () => {
out.handleSkip();
expect(state.setState.isSkipped).toHaveBeenCalledWith(true);
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.unenrollFromCourse(cardId));
});
describe('handleSubmit', () => {
it('tracks reason event and dispatches unenroll thunk action', () => {
state.mockVal(state.keys.selectedReason, testValue);
out = createUseUnenrollReasons();
expect(useTrackUnenrollReasons).toHaveBeenCalledWith({
cardId,
submittedReason: testValue,
describe('behavior', () => {
describe('state fields', () => {
it('initializes selectedReason with null', () => {
state.expectInitializedWith(state.keys.selectedReason, null);
});
expect(trackReasonsEvent).not.toHaveBeenCalled();
const event = { test: 'event' };
out.handleSubmit(event);
expect(trackReasonsEvent).toHaveBeenCalledWith(event);
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.unenrollFromCourse(cardId));
it('initializes customOption with empty string', () => {
state.expectInitializedWith(state.keys.customOption, '');
});
it('initializes isSkipped with false', () => {
state.expectInitializedWith(state.keys.isSkipped, false);
});
it('initializes isSubmitted with false', () => {
state.expectInitializedWith(state.keys.isSubmitted, false);
});
});
describe('useTrackCourseEvent inititalization', () => {
it('passes custom option if selectedReason is custom', () => {
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, testValue);
loadHook();
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.engagement.unenrollReason,
cardId,
testValue,
false, // isEntitlement
);
});
it('passes selected reason if not custom', () => {
state.mockVal(state.keys.selectedReason, testValue2);
state.mockVal(state.keys.customOption, testValue);
loadHook(true);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.engagement.unenrollReason,
cardId,
testValue2,
true, // isEntitlement
);
});
});
it('initializes card entitlement data with cardId', () => {
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
});
it('initializes unenerollFromCourse event with cardId', () => {
expect(apiHooks.useUnenrollFromCourse).toHaveBeenCalledWith(cardId);
});
});
describe('hasReason', () => {
it('returns true if an option is selected other than custom', () => {
state.mockVal(state.keys.selectedReason, testValue);
out = createUseUnenrollReasons();
expect(out.hasReason).toEqual(true);
describe('output', () => {
describe('customOption', () => {
test('customOption.value returns custom option', () => {
state.mockVal(state.keys.customOption, testValue);
loadHook();
expect(out.customOption.value).toEqual(testValue);
});
test('customOption.onChange returns valueCallback for setCustomOption', () => {
expect(out.customOption.onChange).toEqual(
utilHooks.useValueCallback(state.setState.customOption),
);
});
});
it('returns true if custom option is selected and provided', () => {
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, testValue2);
out = createUseUnenrollReasons();
expect(out.hasReason).toEqual(true);
describe('hasReason', () => {
it('returns true if an option is selected other than custom', () => {
state.mockVal(state.keys.selectedReason, testValue);
loadHook();
expect(out.hasReason).toEqual(true);
});
it('returns true if custom option is selected and provided', () => {
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, testValue2);
loadHook();
expect(out.hasReason).toEqual(true);
});
it('returns false if no option is selected', () => {
state.mockVal(state.keys.selectedReason, null);
loadHook();
expect(out.hasReason).toEqual(false);
});
it('returns false if custom option is selcted but not provided', () => {
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, '');
loadHook();
expect(out.hasReason).toEqual(false);
});
});
it('returns false if no option is selected', () => {
state.mockVal(state.keys.selectedReason, null);
out = createUseUnenrollReasons();
expect(out.hasReason).toEqual(false);
describe('handleClear method', () => {
it('resets selected and submitted reasons, custom option and isSkipped', () => {
out.handleClear();
expect(state.setState.selectedReason).toHaveBeenCalledWith(null);
expect(state.setState.customOption).toHaveBeenCalledWith('');
expect(state.setState.isSkipped).toHaveBeenCalledWith(false);
expect(state.setState.isSubmitted).toHaveBeenCalledWith(false);
});
});
it('returns false if custom option is selcted but not provided', () => {
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, '');
out = createUseUnenrollReasons();
expect(out.hasReason).toEqual(false);
test('handleSkip sets isSkipped and isSubmitted, and unenrolls w/out a reason', () => {
out.handleSkip();
expect(state.setState.isSkipped).toHaveBeenCalledWith(true);
expect(unenrollFromCourse).toHaveBeenCalledWith();
});
});
test('isSkipped returns state value', () => {
state.mockVal(state.keys.isSkipped, testValue);
expect(createUseUnenrollReasons().isSkipped).toEqual(testValue);
});
test('isSubmitted returns state value', () => {
state.mockVal(state.keys.isSubmitted, testValue);
expect(createUseUnenrollReasons().isSubmitted).toEqual(testValue);
});
test('selectedOption returns valueCallback for setSelectedReason', () => {
expect(out.selectOption).toEqual(useValueCallback(state.setState.selectedReason));
});
describe('submittedReason', () => {
it('returns the selected reason unless custom is selcted, then shows custom option', () => {
state.mockVal(state.keys.selectedReason, testValue);
state.mockVal(state.keys.customOption, testValue2);
out = createUseUnenrollReasons();
expect(out.submittedReason).toEqual(testValue);
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, testValue2);
out = createUseUnenrollReasons();
expect(out.submittedReason).toEqual(testValue2);
describe('handleSubmit', () => {
it('tracks reason event and calls unenroll action', () => {
state.mockVal(state.keys.selectedReason, testValue);
loadHook();
expect(trackCourseEvent).not.toHaveBeenCalled();
const event = { test: 'event' };
out.handleSubmit(event);
expect(trackCourseEvent).toHaveBeenCalledWith(event);
expect(unenrollFromCourse).toHaveBeenCalledWith();
});
});
test('isSkipped returns state value', () => {
state.mockVal(state.keys.isSkipped, testValue);
loadHook();
expect(out.isSkipped).toEqual(testValue);
});
test('isSubmitted returns state value', () => {
state.mockVal(state.keys.isSubmitted, testValue);
loadHook();
expect(out.isSubmitted).toEqual(testValue);
});
test('selectedOption returns valueCallback for setSelectedReason', () => {
expect(out.selectOption).toEqual(
utilHooks.useValueCallback(state.setState.selectedReason),
);
});
describe('submittedReason', () => {
it('returns the selected reason unless is custom, then shows custom option', () => {
state.mockVal(state.keys.selectedReason, testValue);
state.mockVal(state.keys.customOption, testValue2);
loadHook();
expect(out.submittedReason).toEqual(testValue);
state.mockVal(state.keys.selectedReason, 'custom');
state.mockVal(state.keys.customOption, testValue2);
loadHook();
expect(out.submittedReason).toEqual(testValue2);
});
});
});
});

View File

@@ -1,13 +1,10 @@
import React from 'react';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
ModalDialog,
} from '@edx/paragon';
import { ModalDialog } from '@edx/paragon';
import { nullMethod } from 'hooks';
import { nullMethod } from 'utils';
import ConfirmPane from './components/ConfirmPane';
import ReasonPane from './components/ReasonPane';
@@ -20,14 +17,13 @@ export const UnenrollConfirmModal = ({
show,
cardId,
}) => {
const dispatch = useDispatch();
const {
confirm,
reason,
closeAndRefresh,
close,
modalState,
} = useUnenrollData({ dispatch, closeModal, cardId });
} = useUnenrollData({ closeModal, cardId });
const showFullscreen = modalState === modalStates.reason;
return (
<ModalDialog

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { useDispatch } from 'react-redux';
import { UnenrollConfirmModal } from '.';
@@ -17,7 +16,6 @@ jest.mock('./hooks', () => ({
}));
describe('UnenrollConfirmModal component', () => {
const dispatch = useDispatch();
const hookProps = {
confirm: jest.fn().mockName('hooks.confirm'),
reason: {
@@ -35,10 +33,10 @@ describe('UnenrollConfirmModal component', () => {
show: true,
cardId,
};
test('hooks called with dispatch and closeModal props', () => {
test('hooks called with closeModal and cardId', () => {
hooks.useUnenrollData.mockReturnValueOnce(hookProps);
shallow(<UnenrollConfirmModal {...props} />);
expect(hooks.useUnenrollData).toHaveBeenCalledWith({ dispatch, closeModal, cardId });
expect(hooks.useUnenrollData).toHaveBeenCalledWith({ closeModal, cardId });
});
test('snapshot: modalStates.confirm', () => {
hooks.useUnenrollData.mockReturnValueOnce(hookProps);

View File

@@ -1,73 +0,0 @@
import { useSelector, useDispatch } from 'react-redux';
import { actions as appActions } from './app/reducer';
import appSelectors from './app/selectors';
import requestSelectors from './requests/selectors';
import * as module from './hooks';
const { courseCard } = appSelectors;
export const usePageNumber = () => useSelector(appSelectors.pageNumber);
export const useEmailConfirmationData = () => useSelector(appSelectors.emailConfirmation);
export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpriseDashboard);
export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings);
export const useSelectSessionModalData = () => useSelector(appSelectors.selectSessionModal);
export const useSocialSettingsData = () => useSelector(appSelectors.socialSettingsData);
export const useHasCourses = () => useSelector(appSelectors.hasCourses);
export const useHasAvailableDashboards = () => useSelector(appSelectors.hasAvailableDashboards);
export const useCurrentCourseList = (opts) => useSelector(
state => appSelectors.currentList(state, opts),
);
export const useShowSelectSessionModal = () => useSelector(appSelectors.showSelectSessionModal);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (cardId) => useSelector(
(state) => selector(state, cardId),
);
export const useCardCertificateData = useCourseCardData(courseCard.certificate);
export const useCardCourseData = useCourseCardData(courseCard.course);
export const useCardCourseRunData = useCourseCardData(courseCard.courseRun);
export const useCardCreditData = useCourseCardData(courseCard.credit);
export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment);
export const useCardEntitlementData = useCourseCardData(courseCard.entitlement);
export const useCardGradeData = useCourseCardData(courseCard.gradeData);
export const useCardProviderData = useCourseCardData(courseCard.courseProvider);
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);
export const useCardSocialSettingsData = (cardId) => {
const { socialShareUrl } = module.useCardCourseData(cardId);
const socialShareSettings = useSelector(appSelectors.socialShareSettings);
if (!socialShareSettings) {
return {
facebook: { isEnabled: false, shareUrl: '' },
twitter: { isEnabled: false, shareUrl: '' },
};
}
return {
facebook: {
isEnabled: socialShareSettings.facebook.isEnabled,
shareUrl: `${socialShareUrl}?${socialShareSettings.facebook.utmParams}`,
},
twitter: {
isEnabled: socialShareSettings.twitter.isEnabled,
shareUrl: `${socialShareUrl}?${socialShareSettings.twitter.utmParams}`,
},
};
};
export const useUpdateSelectSessionModalCallback = (cardId) => {
const dispatch = useDispatch();
return () => dispatch(appActions.updateSelectSessionModal(cardId));
};
export const useMasqueradeData = () => useSelector(requestSelectors.masquerade);
export const useRequestIsPending = (requestName) => useSelector(requestSelectors.isPending(requestName));
export const useRequestIsFailed = (requestName) => useSelector(requestSelectors.isFailed(requestName));
export const useTrackCourseEvent = (tracker, cardId, ...args) => {
const { courseId } = module.useCardCourseRunData(cardId);
return (e) => tracker(courseId, ...args)(e);
};

View File

@@ -0,0 +1,80 @@
import { useSelector, useDispatch } from 'react-redux';
import * as redux from 'data/redux';
import * as module from './app';
const selectors = redux.selectors.app;
const actions = redux.actions.app;
/** Simple Selectors **/
export const usePageNumber = () => useSelector(selectors.pageNumber);
export const useEmailConfirmationData = () => useSelector(selectors.emailConfirmation);
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboard);
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
export const useSelectSessionModalData = () => useSelector(selectors.selectSessionModal);
export const useSocialShareSettings = () => useSelector(selectors.socialShareSettings);
/** global-level meta-selectors **/
export const useHasCourses = () => useSelector(selectors.hasCourses);
export const useHasAvailableDashboards = () => useSelector(selectors.hasAvailableDashboards);
export const useCurrentCourseList = (opts) => useSelector(
state => selectors.currentList(state, opts),
);
export const useShowSelectSessionModal = () => useSelector(selectors.showSelectSessionModal);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (cardId) => useSelector(
(state) => selector(state, cardId),
);
/** Course Card selectors **/
const { courseCard } = selectors;
export const useCardCertificateData = useCourseCardData(courseCard.certificate);
export const useCardCourseData = useCourseCardData(courseCard.course);
export const useCardCourseRunData = useCourseCardData(courseCard.courseRun);
export const useCardCreditData = useCourseCardData(courseCard.credit);
export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment);
export const useCardEntitlementData = useCourseCardData(courseCard.entitlement);
export const useCardGradeData = useCourseCardData(courseCard.gradeData);
export const useCardProviderData = useCourseCardData(courseCard.courseProvider);
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);
export const useCardSocialSettingsData = (cardId) => {
const socialShareSettings = module.useSocialShareSettings();
const { socialShareUrl } = module.useCardCourseData(cardId);
const defaultSettings = { isEnabled: false, shareUrl: '' };
if (!socialShareSettings) {
return { facebook: defaultSettings, twitter: defaultSettings };
}
const { facebook, twitter } = socialShareSettings;
const loadSettings = (target) => ({
isEnabled: target.isEnabled,
shareUrl: `${socialShareUrl}?${target.utmParams}`,
});
return { facebook: loadSettings(facebook), twitter: loadSettings(twitter) };
};
/** Events **/
export const useUpdateSelectSessionModalCallback = (cardId) => {
const dispatch = useDispatch();
return () => dispatch(actions.updateSelectSessionModal(cardId));
};
export const useTrackCourseEvent = (tracker, cardId, ...args) => {
const { courseId } = module.useCardCourseRunData(cardId);
return (e) => tracker(courseId, ...args)(e);
};
export const useSetPageNumber = () => {
const dispatch = useDispatch();
return (value) => dispatch(actions.setPageNumber(value));
};
export const useLoadData = () => {
const dispatch = useDispatch();
return ({ courses, ...globalData }) => {
dispatch(actions.setPageNumber(1));
dispatch(actions.loadGlobalData(globalData));
dispatch(actions.loadCourses({ courses }));
};
};

View File

@@ -0,0 +1,2 @@
export * from './app';
export * from './requests';

View File

@@ -0,0 +1,45 @@
import { useSelector, useDispatch } from 'react-redux';
import * as redux from 'data/redux';
import * as module from './requests';
const selectors = redux.selectors.requests;
const actions = redux.actions.requests;
export const useMasqueradeData = () => useSelector(selectors.masquerade);
export const statusSelector = selector => (requestName) => useSelector(selector(requestName));
export const useRequestIsPending = module.statusSelector(selectors.isPending);
export const useRequestIsFailed = module.statusSelector(selectors.isFailed);
export const useRequestIsCompleted = module.statusSelector(selectors.isCompleted);
export const useRequestIsInactive = module.statusSelector(selectors.isInactive);
export const useRequestError = module.statusSelector(selectors.error);
export const useRequestErrorCode = module.statusSelector(selectors.errorCode);
export const useRequestErrorStatus = module.statusSelector(selectors.errorStatus);
export const useRequestData = module.statusSelector(selectors.data);
export const useMakeNetworkRequest = () => {
const dispatch = useDispatch();
return ({
requestKey,
promise,
onSuccess,
onFailure,
}) => {
dispatch(actions.startRequest({ requestKey }));
return promise.then((response) => {
if (onSuccess) { onSuccess(response); }
dispatch(actions.completeRequest({ requestKey, response }));
}).catch((error) => {
if (onFailure) { onFailure(error); }
dispatch(actions.failRequest({ requestKey, error }));
});
};
};
export const useClearRequest = () => {
const dispatch = useDispatch();
return (requestKey) => {
dispatch(actions.clearRequest({ requestKey }));
};
};

View File

@@ -5,10 +5,6 @@ import { StrictDict } from 'utils';
import * as app from './app';
import * as requests from './requests';
import * as hooks from './hooks';
export { default as thunkActions } from './thunkActions';
const modules = {
app,
requests,
@@ -28,6 +24,6 @@ const actions = StrictDict(moduleProps('actions'));
const selectors = StrictDict(moduleProps('selectors'));
export { actions, selectors, hooks };
export { actions, selectors };
export default rootReducer;

View File

@@ -19,7 +19,7 @@ const requests = createSlice({
reducers: {
startRequest: (state, { payload }) => ({
...state,
[payload]: {
[payload.requestKey]: {
status: RequestStates.pending,
},
}),

View File

@@ -18,7 +18,7 @@ describe('requests reducer', () => {
it('adds a pending status for the given key', () => {
expect(reducer(
testingState,
actions.startRequest(testKey),
actions.startRequest({ requestKey: testKey }),
)).toEqual({
...testingState,
[testKey]: { status: RequestStates.pending },

View File

@@ -1,97 +0,0 @@
import { StrictDict } from 'utils';
import { actions, selectors } from 'data/redux';
import { post } from 'data/services/lms/utils';
import requests from './requests';
// import { locationId } from 'data/constants/app';
// import { } from './requests';
import * as module from './app';
export const loadData = ({ courses, ...globalData }) => dispatch => {
dispatch(actions.app.setPageNumber(1));
dispatch(actions.app.loadGlobalData(globalData));
dispatch(actions.app.loadCourses({ courses }));
};
/**
* initialize the app, loading ora and course metadata from the api, and loading the initial
* submission list data.
*/
export const initialize = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: ({ data }) => dispatch(module.loadData(data)),
}))
);
// TODO: connect hook to actual api later
export const sendConfirmEmail = () => (dispatch, getState) => post(
selectors.app.emailConfirmation(getState()).sendEmailUrl,
);
export const newEntitlementEnrollment = (cardId, selection) => (dispatch, getState) => {
const { uuid } = selectors.app.courseCard.entitlement(getState(), cardId);
dispatch(requests.newEntitlementEnrollment({
uuid,
courseId: selection,
onSuccess: () => dispatch(module.initialize()),
}));
};
export const switchEntitlementEnrollment = (cardId, selection) => (dispatch, getState) => {
const { uuid } = selectors.app.courseCard.entitlement(getState(), cardId);
dispatch(requests.switchEntitlementEnrollment({
uuid,
courseId: selection,
onSuccess: () => dispatch(module.initialize()),
}));
};
export const leaveEntitlementSession = (cardId) => (dispatch, getState) => {
const { uuid, isRefundable } = selectors.app.courseCard.entitlement(getState(), cardId);
dispatch(requests.leaveEntitlementSession({
uuid,
isRefundable,
onSuccess: () => dispatch(module.initialize()),
}));
};
export const unenrollFromCourse = (cardId) => (dispatch, getState) => {
const { courseId } = selectors.app.courseCard.courseRun(getState(), cardId);
dispatch(requests.unenrollFromCourse({ courseId }));
};
export const masqueradeAs = (user) => (dispatch) => (
dispatch(requests.masqueradeAs({
user,
onSuccess: ({ data }) => dispatch(module.loadData(data)),
}))
);
export const clearMasquerade = () => (dispatch) => {
dispatch(requests.clearMasquerade());
dispatch(module.initialize());
};
export const updateEmailSettings = (cardId, enable) => (dispatch, getState) => {
const { courseId } = selectors.app.courseCard.courseRun(getState(), cardId);
dispatch(requests.updateEmailSettings({
courseId,
enable,
}));
};
export default StrictDict({
loadData,
initialize,
refreshList: initialize,
sendConfirmEmail,
newEntitlementEnrollment,
switchEntitlementEnrollment,
leaveEntitlementSession,
unenrollFromCourse,
masqueradeAs,
clearMasquerade,
updateEmailSettings,
});

View File

@@ -1,181 +0,0 @@
import { keyStore } from 'utils';
import { actions, selectors } from 'data/redux';
import { post } from 'data/services/lms/utils';
import requests from './requests';
import * as module from './app';
jest.mock('data/services/lms/utils', () => ({
post: jest.fn(),
}));
jest.mock('data/redux', () => ({
actions: {
app: {
setPageNumber: jest.fn(v => ({ setPageNumber: v })),
loadGlobalData: jest.fn(v => ({ loadGlobalData: v })),
loadCourses: jest.fn(v => ({ loadCourses: v })),
loadRecommendedCourses: jest.fn(v => ({ loadRecommendedCourses: v })),
},
},
selectors: {
app: {
emailConfirmation: jest.fn(),
courseCard: {
courseRun: jest.fn(),
entitlement: jest.fn(),
},
},
},
}));
jest.mock('./requests', () => ({
initializeList: jest.fn((args) => ({ initializeList: args })),
newEntitlementEnrollment: jest.fn((args) => ({ newEntitlementEnrollment: args })),
switchEntitlementEnrollment: jest.fn((args) => ({ switchEntitlementEnrollment: args })),
leaveEntitlementSession: jest.fn((args) => ({ leaveEntitlementSession: args })),
unenrollFromCourse: jest.fn((args) => ({ unenrollFromCourse: args })),
masqueradeAs: jest.fn((args) => ({ masqueradeAs: args })),
clearMasquerade: jest.fn((args) => ({ clearMasquerade: args })),
updateEmailSettings: jest.fn((args) => ({ updateEmailSettings: args })),
}));
const dispatch = jest.fn(action => action);
const checkDispatch = (call) => { expect(dispatch).toHaveBeenCalledWith(call); };
const moduleKeys = keyStore(module);
const testString = 'TEST-string';
const uuid = 'test-UUID';
const cardId = 'test-card-id';
const selection = 'test-selection';
const courseId = 'test-COURSE-id';
const isRefundable = 'test-is-refundable';
const loadDataSpy = jest.spyOn(module, moduleKeys.loadData);
const mockLoadData = data => ({ loadData: data });
const initializeSpy = jest.spyOn(module, moduleKeys.initialize);
const mockInitialize = () => 'mock-initialize';
const testState = { test: 'state' };
const getState = () => testState;
describe('app thunk actions', () => {
beforeEach(() => {
jest.clearAllMocks();
selectors.app.emailConfirmation.mockReturnValueOnce({ sendEmailUrl: testString });
selectors.app.courseCard.entitlement.mockReturnValueOnce({ uuid, isRefundable });
selectors.app.courseCard.courseRun.mockReturnValueOnce({ courseId });
});
describe('loadData', () => {
const courses = 'test-courses';
const globalData = { some: 'global', data: 'fields' };
beforeEach(() => {
module.loadData({ courses, ...globalData })(dispatch);
});
it('initializes pageNumber to 1', () => {
checkDispatch(actions.app.setPageNumber(1));
});
it('loads courses', () => {
checkDispatch(actions.app.loadCourses({ courses }));
});
it('loads remaining passed args as global data', () => {
checkDispatch(actions.app.loadGlobalData(globalData));
});
});
describe('initialize', () => {
beforeEach(() => {
loadDataSpy.mockImplementationOnce(mockLoadData);
module.initialize()(dispatch);
});
it('dispatches initializeList event, calling loadData on response', () => {
const { onSuccess } = dispatch.mock.calls[0][0].initializeList;
onSuccess({ data: testString });
checkDispatch(mockLoadData(testString));
});
});
describe('sendConfirmEmail', () => {
it('sends post request fo sendEmailUrl', () => {
expect(module.sendConfirmEmail()(dispatch, getState)).toEqual(post(testString));
expect(selectors.app.emailConfirmation).toHaveBeenCalledWith(testState);
});
});
describe('newEntitlementEnrollment', () => {
beforeEach(() => {
module.newEntitlementEnrollment(cardId, selection)(dispatch, getState);
});
it('dispatches newEntitlementEnrollment request then re-init on success', () => {
const request = dispatch.mock.calls[0][0];
expect(request.newEntitlementEnrollment.uuid).toEqual(uuid);
expect(request.newEntitlementEnrollment.courseId).toEqual(selection);
expect(request.newEntitlementEnrollment.onSuccess).toBeDefined();
expect(initializeSpy).not.toHaveBeenCalled();
request.newEntitlementEnrollment.onSuccess();
expect(initializeSpy).toHaveBeenCalled();
});
});
describe('switchEntitlementEnrollmnent', () => {
beforeEach(() => {
module.switchEntitlementEnrollment(cardId, selection)(dispatch, getState);
});
it('dispatches switchEntitlementEnrollment request then re-init on success', () => {
const request = dispatch.mock.calls[0][0];
expect(request.switchEntitlementEnrollment.uuid).toEqual(uuid);
expect(request.switchEntitlementEnrollment.courseId).toEqual(selection);
expect(request.switchEntitlementEnrollment.onSuccess).toBeDefined();
expect(initializeSpy).not.toHaveBeenCalled();
request.switchEntitlementEnrollment.onSuccess();
expect(initializeSpy).toHaveBeenCalled();
});
});
describe('leaveEntitlementSession', () => {
beforeEach(() => {
module.leaveEntitlementSession(cardId)(dispatch, getState);
});
it('dispatches leaveEntitlementEnrollment request then re-init on success', () => {
const request = dispatch.mock.calls[0][0];
expect(request.leaveEntitlementSession.uuid).toEqual(uuid);
expect(request.leaveEntitlementSession.isRefundable).toEqual(isRefundable);
expect(request.leaveEntitlementSession.onSuccess).toBeDefined();
expect(initializeSpy).not.toHaveBeenCalled();
request.leaveEntitlementSession.onSuccess();
expect(initializeSpy).toHaveBeenCalled();
});
});
describe('unenrollFromCourse', () => {
const reason = 'test-reason';
it('dispatches unenrollFromCourse request action', () => {
module.unenrollFromCourse(cardId, reason)(dispatch, getState);
const request = dispatch.mock.calls[0][0];
expect(request.unenrollFromCourse.courseId).toEqual(courseId);
});
});
describe('masqueradeAs', () => {
it('dispatches masqueradeAS request action, loading data on success', () => {
loadDataSpy.mockImplementationOnce(mockLoadData);
module.masqueradeAs(testString)(dispatch);
const request = dispatch.mock.calls[0][0];
request.masqueradeAs.onSuccess({ data: testString });
expect(dispatch).toHaveBeenCalledWith(mockLoadData(testString));
});
});
describe('clearMasquerade', () => {
it('dispatches clearMasquerade action and re-initializes', () => {
initializeSpy.mockImplementationOnce(mockInitialize);
module.clearMasquerade()(dispatch);
expect(dispatch).toHaveBeenCalledWith(requests.clearMasquerade());
expect(dispatch).toHaveBeenCalledWith(mockInitialize());
});
});
describe('update email settings', () => {
it('dispatches updateEmailSettings request action', () => {
module.updateEmailSettings(cardId, testString)(dispatch, getState);
expect(selectors.app.courseCard.courseRun).toHaveBeenCalledWith(testState, cardId);
expect(dispatch).toHaveBeenCalledWith(requests.updateEmailSettings({
courseId,
enable: testString,
}));
});
});
});

View File

@@ -1,7 +0,0 @@
import { StrictDict } from 'utils';
import app from './app';
export default StrictDict({
app,
});

Some files were not shown because too many files have changed in this diff Show More