diff --git a/src/App.jsx b/src/App.jsx index 50f71fe..0d08d32 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 ( diff --git a/src/App.test.jsx b/src/App.test.jsx index e18a207..bc57ea7 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -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(); }); 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(); }); 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(); }); runBasicTests(); diff --git a/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.jsx b/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.jsx index 0a2c565..f13525a 100644 --- a/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.jsx @@ -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, diff --git a/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx index 6c7c10e..b10e737 100644 --- a/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx @@ -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(); 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(); - expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); + expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); }); it('initializes enrollment data with cardId', () => { wrapper = shallow(); - 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(); expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); test('masquerading', () => { - hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); wrapper = shallow(); expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/ResumeButton.jsx b/src/containers/CourseCard/components/CourseCardActions/ResumeButton.jsx index 9e79235..ee6db24 100644 --- a/src/containers/CourseCard/components/CourseCardActions/ResumeButton.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/ResumeButton.jsx @@ -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, diff --git a/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx index d6958b1..51deee8 100644 --- a/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx @@ -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(); - expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); + expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); }); it('initializes course enrollment data based on cardId', () => { shallow(); - 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(); 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, diff --git a/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx b/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx index bd37f72..c2de744 100644 --- a/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx @@ -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 ( ({ - 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(); expect(wrapper).toMatchSnapshot(); }); it('renders disabled button if masquerading', () => { - hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); @@ -39,26 +39,26 @@ describe('SelectSessionButton', () => { wrapper = shallow(); 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(); 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(); 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(); expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); test('user is masquerading', () => { - hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); wrapper = shallow(); expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.jsx b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.jsx index 411d865..1e720d4 100644 --- a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.jsx @@ -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, diff --git a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx index d9b6167..2d26764 100644 --- a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx @@ -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(); 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(); 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(); expect(wrapper).toMatchSnapshot(); expect(wrapper.prop(htmlProps.disabled)).toEqual(true); diff --git a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx index 7844793..90a4433 100644 --- a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx @@ -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 ( ({ @@ -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(); }; @@ -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, diff --git a/src/containers/CourseCard/components/CourseCardActions/index.jsx b/src/containers/CourseCard/components/CourseCardActions/index.jsx index 64b20f2..05e097d 100644 --- a/src/containers/CourseCard/components/CourseCardActions/index.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/index.jsx @@ -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; diff --git a/src/containers/CourseCard/components/CourseCardActions/index.test.jsx b/src/containers/CourseCard/components/CourseCardActions/index.test.jsx index d3574be..38637ff 100644 --- a/src/containers/CourseCard/components/CourseCardActions/index.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/index.test.jsx @@ -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(); }; describe('snapshot', () => { diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx index 0ee054f..7c4bd0b 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx @@ -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(); diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx index d819a17..36fb523 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx @@ -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(); }; describe('snapshot', () => { diff --git a/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.jsx b/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.jsx index 06a35eb..f8c111e 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.jsx @@ -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; diff --git a/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.test.jsx index 35d12a0..8b0504d 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CourseBanner.test.jsx @@ -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 } }); diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.js b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.js index 918cf81..89e92eb 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.js +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.js @@ -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; diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.test.js b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.test.js index df91c7a..729de8f 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.test.js @@ -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', () => { diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/ApprovedContent.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/ApprovedContent.jsx index ba3d84f..8dc3cbe 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/ApprovedContent.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/ApprovedContent.jsx @@ -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 ( ({ - 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', () => { diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.jsx index 7c0a700..2a48f5b 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.jsx @@ -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); diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.test.jsx index 328356a..87c4cd7 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.test.jsx @@ -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(); @@ -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(); diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.jsx index 913cf30..af3c680 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.jsx @@ -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 ( diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.test.jsx index 8c93101..79cd941 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.test.jsx @@ -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); diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/RejectedContent.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/RejectedContent.jsx index c02eadc..afd66e7 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/RejectedContent.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/RejectedContent.jsx @@ -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 ( ({ - 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', () => { diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/components/ProviderLink.jsx b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/components/ProviderLink.jsx index 9c22570..6b757d3 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/components/ProviderLink.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/components/ProviderLink.jsx @@ -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 ( ({ - 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(); }); describe('behavior', () => { it('initializes credit hook with cardId', () => { - expect(appHooks.useCardCreditData).toHaveBeenCalledWith(cardId); + expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); }); }); describe('render', () => { diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.js b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.js index 6c18e93..2f3c1fa 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.js +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.js @@ -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 }; }; diff --git a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.test.js b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.test.js index 8b6ad40..01e4d07 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.test.js @@ -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); }); }); }); diff --git a/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.jsx b/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.jsx index 3bd800b..f3c3ebb 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.jsx @@ -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; diff --git a/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.test.jsx index 16ce5e4..05b0b3e 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.test.jsx @@ -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(); }; 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 } }); diff --git a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap index fd35687..7cce84d 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap @@ -15,7 +15,7 @@ exports[`CertificateBanner snapshot is passing and is downloadable 1`] = ` exports[`CertificateBanner snapshot is passing and is earned but unavailable 1`] = ` - Your grade and certificate will be ready after Invalid Date. + Your grade and certificate will be ready after 10/20/3030. `; diff --git a/src/containers/CourseCard/components/CourseCardBanners/index.jsx b/src/containers/CourseCard/components/CourseCardBanners/index.jsx index 3b693ce..ee77e1b 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/index.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/index.jsx @@ -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 (
diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.js index 0e703e7..b7d72c5 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/hooks.js +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.js @@ -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), diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js index 5c813bf..3032791 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js @@ -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) }, )); }); }); diff --git a/src/containers/CourseCard/components/CourseCardImage.jsx b/src/containers/CourseCard/components/CourseCardImage.jsx index cc458f0..46cf056 100644 --- a/src/containers/CourseCard/components/CourseCardImage.jsx +++ b/src/containers/CourseCard/components/CourseCardImage.jsx @@ -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 = ( <> diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.js index f15b740..5d64610 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/hooks.js +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.js @@ -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(); } }; diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js index 971453e..fb5eea5 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js @@ -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(); + }); + }); + }); }); diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx index f41c591..f2c4524 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -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', diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.test.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.test.jsx index bf63756..c1e81ad 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.test.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.test.jsx @@ -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(); }); test('snapshot', () => { @@ -117,7 +117,7 @@ describe('CourseCardMenu', () => { }); describe('masquerading', () => { beforeEach(() => { - appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true }); + reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true }); wrapper = shallow(); }); test('snapshot', () => { diff --git a/src/containers/CourseCard/components/CourseCardTitle.jsx b/src/containers/CourseCard/components/CourseCardTitle.jsx index 8a88689..717af8f 100644 --- a/src/containers/CourseCard/components/CourseCardTitle.jsx +++ b/src/containers/CourseCard/components/CourseCardTitle.jsx @@ -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 (

{ 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), }; }; diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js b/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js index cf412f7..aefc97d 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js @@ -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, diff --git a/src/containers/CourseCard/hooks.js b/src/containers/CourseCard/hooks.js index 715bfa5..75d2528 100644 --- a/src/containers/CourseCard/hooks.js +++ b/src/containers/CourseCard/hooks.js @@ -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, diff --git a/src/containers/CourseCard/hooks.test.js b/src/containers/CourseCard/hooks.test.js index b3c3bd8..ece458f 100644 --- a/src/containers/CourseCard/hooks.test.js +++ b/src/containers/CourseCard/hooks.test.js @@ -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); }); diff --git a/src/containers/CourseFilterControls/CourseFilterControls.jsx b/src/containers/CourseFilterControls/CourseFilterControls.jsx index 4a63f85..23325fb 100644 --- a/src/containers/CourseFilterControls/CourseFilterControls.jsx +++ b/src/containers/CourseFilterControls/CourseFilterControls.jsx @@ -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, diff --git a/src/containers/CourseFilterControls/CourseFilterControls.test.jsx b/src/containers/CourseFilterControls/CourseFilterControls.test.jsx index a832398..21de9de 100644 --- a/src/containers/CourseFilterControls/CourseFilterControls.test.jsx +++ b/src/containers/CourseFilterControls/CourseFilterControls.test.jsx @@ -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(); expect(wrapper).toMatchSnapshot(); diff --git a/src/containers/CourseList/NoCoursesView/index.jsx b/src/containers/CourseList/NoCoursesView/index.jsx index d44649c..0f609a8 100644 --- a/src/containers/CourseList/NoCoursesView/index.jsx +++ b/src/containers/CourseList/NoCoursesView/index.jsx @@ -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 (
{ - 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, diff --git a/src/containers/CourseList/hooks.test.js b/src/containers/CourseList/hooks.test.js index b7ea33f..124e445 100644 --- a/src/containers/CourseList/hooks.test.js +++ b/src/containers/CourseList/hooks.test.js @@ -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)); }); }); }); diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx index 88480ef..81ab9f3 100644 --- a/src/containers/CourseList/index.jsx +++ b/src/containers/CourseList/index.jsx @@ -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, diff --git a/src/containers/CourseList/index.test.jsx b/src/containers/CourseList/index.test.jsx index e138622..4b798f0 100644 --- a/src/containers/CourseList/index.test.jsx +++ b/src/containers/CourseList/index.test.jsx @@ -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(); }); diff --git a/src/containers/Dashboard/hooks.js b/src/containers/Dashboard/hooks.js index 059aefb..cdb7796 100644 --- a/src/containers/Dashboard/hooks.js +++ b/src/containers/Dashboard/hooks.js @@ -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 = () => { diff --git a/src/containers/Dashboard/hooks.test.js b/src/containers/Dashboard/hooks.test.js index b5d5e4f..158310c 100644 --- a/src/containers/Dashboard/hooks.test.js +++ b/src/containers/Dashboard/hooks.test.js @@ -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', () => { diff --git a/src/containers/Dashboard/index.jsx b/src/containers/Dashboard/index.jsx index 629c964..a13d376 100644 --- a/src/containers/Dashboard/index.jsx +++ b/src/containers/Dashboard/index.jsx @@ -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 (

{pageTitle}

diff --git a/src/containers/Dashboard/index.test.jsx b/src/containers/Dashboard/index.test.jsx index 5e6bbd8..2a50cd9 100644 --- a/src/containers/Dashboard/index.test.jsx +++ b/src/containers/Dashboard/index.test.jsx @@ -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(); }; diff --git a/src/containers/EmailSettingsModal/__snapshots__/index.test.jsx.snap b/src/containers/EmailSettingsModal/__snapshots__/index.test.jsx.snap index 8a8b943..1a81f5d 100644 --- a/src/containers/EmailSettingsModal/__snapshots__/index.test.jsx.snap +++ b/src/containers/EmailSettingsModal/__snapshots__/index.test.jsx.snap @@ -4,7 +4,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
{ - 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, diff --git a/src/containers/EmailSettingsModal/hooks.test.js b/src/containers/EmailSettingsModal/hooks.test.js index 718dd6f..c701853 100644 --- a/src/containers/EmailSettingsModal/hooks.test.js +++ b/src/containers/EmailSettingsModal/hooks.test.js @@ -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(); }); }); diff --git a/src/containers/EmailSettingsModal/index.jsx b/src/containers/EmailSettingsModal/index.jsx index f0fdebd..e542f97 100644 --- a/src/containers/EmailSettingsModal/index.jsx +++ b/src/containers/EmailSettingsModal/index.jsx @@ -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 ( diff --git a/src/containers/EmailSettingsModal/index.test.jsx b/src/containers/EmailSettingsModal/index.test.jsx index fff74cd..13d9301 100644 --- a/src/containers/EmailSettingsModal/index.test.jsx +++ b/src/containers/EmailSettingsModal/index.test.jsx @@ -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(); }); - 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, }); }); diff --git a/src/containers/EnterpriseDashboardModal/hooks.js b/src/containers/EnterpriseDashboardModal/hooks.js index 339b39a..5bc88ce 100644 --- a/src/containers/EnterpriseDashboardModal/hooks.js +++ b/src/containers/EnterpriseDashboardModal/hooks.js @@ -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'); diff --git a/src/containers/EnterpriseDashboardModal/hooks.test.js b/src/containers/EnterpriseDashboardModal/hooks.test.js index a3fb6d4..b309dcb 100644 --- a/src/containers/EnterpriseDashboardModal/hooks.test.js +++ b/src/containers/EnterpriseDashboardModal/hooks.test.js @@ -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); diff --git a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx index 6a0e46c..78fe741 100644 --- a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx +++ b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx @@ -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 ( diff --git a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.test.jsx b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.test.jsx index fe76384..0e9fa13 100644 --- a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.test.jsx +++ b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.test.jsx @@ -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(); expect(wrapper).toMatchSnapshot(); }); test('without enterprise dashboard and expanded', () => { - appHooks.useEnterpriseDashboardData.mockReturnValueOnce(null); + reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null); useIsCollapsed.mockReturnValueOnce(false); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js index ef29d8c..3797c68 100644 --- a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js @@ -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(); }; diff --git a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js index 50e1047..6d8c562 100644 --- a/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js +++ b/src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js @@ -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(); diff --git a/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap b/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap index 65337f2..a4fe20d 100644 --- a/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap +++ b/src/containers/LearnerDashboardHeader/__snapshots__/index.test.jsx.snap @@ -31,6 +31,7 @@ exports[`LearnerDashboardHeader snapshots with collapsed 1`] = ` className="my-auto ml-1 d-flex" > { 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 ? (
({ 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', })), diff --git a/src/containers/LearnerDashboardHeader/messages.js b/src/containers/LearnerDashboardHeader/messages.js index 3105055..01e02a9 100644 --- a/src/containers/LearnerDashboardHeader/messages.js +++ b/src/containers/LearnerDashboardHeader/messages.js @@ -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; diff --git a/src/containers/MasqueradeBar/hooks.js b/src/containers/MasqueradeBar/hooks.js index 04ef7e7..4310c2d 100644 --- a/src/containers/MasqueradeBar/hooks.js +++ b/src/containers/MasqueradeBar/hooks.js @@ -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, diff --git a/src/containers/MasqueradeBar/hooks.test.js b/src/containers/MasqueradeBar/hooks.test.js index d86e828..db0675a 100644 --- a/src/containers/MasqueradeBar/hooks.test.js +++ b/src/containers/MasqueradeBar/hooks.test.js @@ -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(); }); }); diff --git a/src/containers/RelatedProgramsModal/hooks.js b/src/containers/RelatedProgramsModal/hooks.js index ad134b1..5c4105d 100644 --- a/src/containers/RelatedProgramsModal/hooks.js +++ b/src/containers/RelatedProgramsModal/hooks.js @@ -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; diff --git a/src/containers/RelatedProgramsModal/index.jsx b/src/containers/RelatedProgramsModal/index.jsx index aa05f56..d70bfb1 100644 --- a/src/containers/RelatedProgramsModal/index.jsx +++ b/src/containers/RelatedProgramsModal/index.jsx @@ -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 ( '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(); - 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()).toMatchSnapshot(); diff --git a/src/containers/SelectSessionModal/__snapshots__/index.test.jsx.snap b/src/containers/SelectSessionModal/__snapshots__/index.test.jsx.snap index af5f130..1afb69c 100644 --- a/src/containers/SelectSessionModal/__snapshots__/index.test.jsx.snap +++ b/src/containers/SelectSessionModal/__snapshots__/index.test.jsx.snap @@ -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" > diff --git a/src/containers/SelectSessionModal/hooks.js b/src/containers/SelectSessionModal/hooks.js index 1aeb381..e0b030e 100644 --- a/src/containers/SelectSessionModal/hooks.js +++ b/src/containers/SelectSessionModal/hooks.js @@ -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(); }; diff --git a/src/containers/SelectSessionModal/hooks.test.js b/src/containers/SelectSessionModal/hooks.test.js index 136c0cb..c54636d 100644 --- a/src/containers/SelectSessionModal/hooks.test.js +++ b/src/containers/SelectSessionModal/hooks.test.js @@ -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)(), ); }); }); diff --git a/src/containers/SelectSessionModal/index.jsx b/src/containers/SelectSessionModal/index.jsx index 8d95a61..3d39ac2 100644 --- a/src/containers/SelectSessionModal/index.jsx +++ b/src/containers/SelectSessionModal/index.jsx @@ -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 (
{ +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(); }; diff --git a/src/containers/UnenrollConfirmModal/hooks/index.test.js b/src/containers/UnenrollConfirmModal/hooks/index.test.js index 8b4be0d..1db9a11 100644 --- a/src/containers/UnenrollConfirmModal/hooks/index.test.js +++ b/src/containers/UnenrollConfirmModal/hooks/index.test.js @@ -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', () => { diff --git a/src/containers/UnenrollConfirmModal/hooks/reasons.js b/src/containers/UnenrollConfirmModal/hooks/reasons.js index 720b893..a80d3b4 100644 --- a/src/containers/UnenrollConfirmModal/hooks/reasons.js +++ b/src/containers/UnenrollConfirmModal/hooks/reasons.js @@ -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, }; }; diff --git a/src/containers/UnenrollConfirmModal/hooks/reasons.test.js b/src/containers/UnenrollConfirmModal/hooks/reasons.test.js index cfed357..3347192 100644 --- a/src/containers/UnenrollConfirmModal/hooks/reasons.test.js +++ b/src/containers/UnenrollConfirmModal/hooks/reasons.test.js @@ -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); + }); }); }); }); diff --git a/src/containers/UnenrollConfirmModal/index.jsx b/src/containers/UnenrollConfirmModal/index.jsx index dc66dd2..2090bae 100644 --- a/src/containers/UnenrollConfirmModal/index.jsx +++ b/src/containers/UnenrollConfirmModal/index.jsx @@ -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 ( ({ })); 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(); - expect(hooks.useUnenrollData).toHaveBeenCalledWith({ dispatch, closeModal, cardId }); + expect(hooks.useUnenrollData).toHaveBeenCalledWith({ closeModal, cardId }); }); test('snapshot: modalStates.confirm', () => { hooks.useUnenrollData.mockReturnValueOnce(hookProps); diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js deleted file mode 100644 index bbad44d..0000000 --- a/src/data/redux/hooks.js +++ /dev/null @@ -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); -}; diff --git a/src/data/redux/hooks/app.js b/src/data/redux/hooks/app.js new file mode 100644 index 0000000..c06eb29 --- /dev/null +++ b/src/data/redux/hooks/app.js @@ -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 })); + }; +}; diff --git a/src/data/redux/hooks/index.js b/src/data/redux/hooks/index.js new file mode 100644 index 0000000..ba6f574 --- /dev/null +++ b/src/data/redux/hooks/index.js @@ -0,0 +1,2 @@ +export * from './app'; +export * from './requests'; diff --git a/src/data/redux/hooks/requests.js b/src/data/redux/hooks/requests.js new file mode 100644 index 0000000..79537b8 --- /dev/null +++ b/src/data/redux/hooks/requests.js @@ -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 })); + }; +}; diff --git a/src/data/redux/index.js b/src/data/redux/index.js index 22f41c4..827570b 100644 --- a/src/data/redux/index.js +++ b/src/data/redux/index.js @@ -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; diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js index 58ac64c..e29bbf1 100644 --- a/src/data/redux/requests/reducer.js +++ b/src/data/redux/requests/reducer.js @@ -19,7 +19,7 @@ const requests = createSlice({ reducers: { startRequest: (state, { payload }) => ({ ...state, - [payload]: { + [payload.requestKey]: { status: RequestStates.pending, }, }), diff --git a/src/data/redux/requests/reducer.test.js b/src/data/redux/requests/reducer.test.js index 4283473..38784bd 100644 --- a/src/data/redux/requests/reducer.test.js +++ b/src/data/redux/requests/reducer.test.js @@ -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 }, diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js deleted file mode 100644 index 5de5640..0000000 --- a/src/data/redux/thunkActions/app.js +++ /dev/null @@ -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, -}); diff --git a/src/data/redux/thunkActions/app.test.js b/src/data/redux/thunkActions/app.test.js deleted file mode 100644 index e731fe6..0000000 --- a/src/data/redux/thunkActions/app.test.js +++ /dev/null @@ -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, - })); - }); - }); -}); diff --git a/src/data/redux/thunkActions/index.js b/src/data/redux/thunkActions/index.js deleted file mode 100644 index 1a1d0e6..0000000 --- a/src/data/redux/thunkActions/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { StrictDict } from 'utils'; - -import app from './app'; - -export default StrictDict({ - app, -}); diff --git a/src/data/redux/thunkActions/requests.js b/src/data/redux/thunkActions/requests.js deleted file mode 100644 index 748fbd9..0000000 --- a/src/data/redux/thunkActions/requests.js +++ /dev/null @@ -1,106 +0,0 @@ -import { StrictDict } from 'utils'; - -import { RequestKeys } from 'data/constants/requests'; -import { actions } from 'data/redux'; -import api from 'data/services/lms/api'; - -import * as module from './requests'; - -/** - * Wrapper around a network request promise, that sends actions to the redux store to - * track the state of that promise. - * Tracks the promise by requestKey, and sends an action when it is started, succeeds, or - * fails. It also accepts onSuccess and onFailure methods to be called with the output - * of failure or success of the promise. - * @param {string} requestKey - request tracking identifier - * @param {Promise} promise - api event promise - * @param {[func]} onSuccess - onSuccess method ((response) => { ... }) - * @param {[func]} onFailure - onFailure method ((error) => { ... }) - */ -export const networkRequest = ({ - requestKey, - promise, - onSuccess, - onFailure, -}) => (dispatch) => { - dispatch(actions.requests.startRequest(requestKey)); - return promise.then((response) => { - if (onSuccess) { onSuccess(response); } - dispatch(actions.requests.completeRequest({ requestKey, response })); - }).catch((error) => { - if (onFailure) { onFailure(error); } - dispatch(actions.requests.failRequest({ requestKey, error })); - }); -}; - -export const networkAction = ({ requestKey, promise, options }) => (dispatch) => ( - dispatch(module.networkRequest({ - requestKey, - promise, - ...options, - }))); - -export const initializeList = (options) => module.networkAction({ - requestKey: RequestKeys.initialize, - promise: api.initializeList(), - options, -}); - -export const newEntitlementEnrollment = ({ - uuid, - courseId, - ...options -}) => module.networkAction({ - requestKey: RequestKeys.newEntitlementEnrollment, - promise: api.updateEntitlementEnrollment({ uuid, courseId }), - options, -}); - -export const switchEntitlementEnrollment = ({ - uuid, - courseId, - ...options -}) => module.networkAction({ - requestKey: RequestKeys.switchEntitlementSession, - promise: api.updateEntitlementEnrollment({ uuid, courseId }), - options, -}); - -export const leaveEntitlementSession = ({ uuid, isRefundable, ...options }) => module.networkAction({ - requestKey: RequestKeys.leaveEntitlementSession, - promise: api.deleteEntitlementEnrollment({ uuid, isRefundable }), - options, -}); - -export const unenrollFromCourse = ({ courseId, ...options }) => module.networkAction({ - requestKey: RequestKeys.unenrollFromCourse, - promise: api.unenrollFromCourse({ courseId }), - options, -}); - -export const updateEmailSettings = ({ courseId, enable, ...options }) => module.networkAction({ - requestKey: RequestKeys.updateEmailSettings, - promise: api.updateEmailSettings({ courseId, enable }), - options, -}); - -export const masqueradeAs = ({ user, ...options }) => module.networkAction({ - requestKey: RequestKeys.masquerade, - promise: api.initializeList({ user }), - options, -}); - -export const clearMasquerade = () => (dispatch) => dispatch( - actions.requests.clearRequest({ requestKey: RequestKeys.masquerade }), -); - -export default StrictDict({ - initializeList, - masqueradeAs, - clearMasquerade, - leaveEntitlementSession, - newEntitlementEnrollment, - switchEntitlementEnrollment, - unenrollFromCourse, - updateEmailSettings, -}); diff --git a/src/data/redux/thunkActions/requests.test.js b/src/data/redux/thunkActions/requests.test.js deleted file mode 100644 index 9489c7a..0000000 --- a/src/data/redux/thunkActions/requests.test.js +++ /dev/null @@ -1,229 +0,0 @@ -import { keyStore } from 'utils'; -import { actions } from 'data/redux'; -import { RequestKeys } from 'data/constants/requests'; -import api from 'data/services/lms/api'; -import * as module from './requests'; - -jest.mock('data/services/lms/api', () => ({ - initializeList: jest.fn(args => ({ initializeList: args })), - updateEntitlementEnrollment: jest.fn(args => ({ updateEntitlementEnrollment: args })), - deleteEntitlementEnrollment: jest.fn(args => ({ deleteEntitlementEnrollment: args })), - unenrollFromCourse: jest.fn(args => ({ unenrollFromCourse: args })), - updateEmailSettings: jest.fn(args => ({ updateEmailSettings: args })), -})); - -jest.mock('data/redux', () => ({ - actions: { - requests: { - clearRequest: jest.fn(args => ({ clearRequest: args })), - completeRequest: jest.fn(args => ({ completeRequest: args })), - failRequest: jest.fn(args => ({ failRequest: args })), - startRequest: jest.fn(args => ({ startRequest: args })), - }, - }, -})); - -const dispatch = jest.fn(); -const onSuccess = jest.fn(); -const onFailure = jest.fn(); - -const moduleKeys = keyStore(module); - -const networkRequestSpy = jest.spyOn(module, moduleKeys.networkRequest); -const mockNetworkRequest = (args) => ({ networkRequest: args }); - -const networkActionSpy = jest.spyOn(module, moduleKeys.networkAction); -const mockNetworkAction = (args) => ({ mockNetworkAction: args }); - -const courseId = 'test-course-id'; -const enable = 'test-enable'; -const options = { some: 'test', option: 'fields' }; -const promise = 'test-promise'; -const requestKey = 'test-request-key'; -const user = 'test-user'; -const uuid = 'test-uuid'; -const isRefundable = 'test-is-refundable'; - -describe('requests thunkActions module', () => { - beforeEach(jest.clearAllMocks); - describe('networkRequest', () => { - const testData = { some: 'test data' }; - let resolveFn; - let rejectFn; - describe('with both handlers', () => { - beforeEach(() => { - module.networkRequest({ - requestKey, - promise: new Promise((resolve, reject) => { - resolveFn = resolve; - rejectFn = reject; - }), - onSuccess, - onFailure, - })(dispatch); - }); - test('calls startRequest action with requestKey', async () => { - expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]); - }); - describe('on success', () => { - beforeEach(async () => { - await resolveFn(testData); - }); - it('dispatches completeRequest', async () => { - expect(dispatch.mock.calls).toEqual([ - [actions.requests.startRequest(requestKey)], - [actions.requests.completeRequest({ requestKey, response: testData })], - ]); - }); - it('calls onSuccess with response', async () => { - expect(onSuccess).toHaveBeenCalledWith(testData); - expect(onFailure).not.toHaveBeenCalled(); - }); - }); - describe('on failure', () => { - beforeEach(async () => { - await rejectFn(testData); - }); - test('dispatches completeRequest', async () => { - expect(dispatch.mock.calls).toEqual([ - [actions.requests.startRequest(requestKey)], - [actions.requests.failRequest({ requestKey, error: testData })], - ]); - }); - test('calls onSuccess with response', async () => { - expect(onFailure).toHaveBeenCalledWith(testData); - expect(onSuccess).not.toHaveBeenCalled(); - }); - }); - }); - describe('without onSuccess and onFailure', () => { - test('calls startRequest action with requestKey', async () => { - module.networkRequest({ requestKey, promise: Promise.resolve(testData) })(dispatch); - expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]); - }); - it('on success dispatches completeRequest', async () => { - await module.networkRequest({ requestKey, promise: Promise.resolve(testData) })(dispatch); - expect(dispatch.mock.calls).toEqual([ - [actions.requests.startRequest(requestKey)], - [actions.requests.completeRequest({ requestKey, response: testData })], - ]); - }); - it('on failure disaptches completeRequest', async () => { - await module.networkRequest({ requestKey, promise: Promise.reject(testData) })(dispatch); - expect(dispatch.mock.calls).toEqual([ - [actions.requests.startRequest(requestKey)], - [actions.requests.failRequest({ requestKey, error: testData })], - ]); - }); - }); - }); - - describe('networkAction', () => { - it('dispatches network request with key, promise, and options', () => { - networkRequestSpy.mockImplementationOnce(mockNetworkRequest); - module.networkAction({ requestKey, promise, options })(dispatch); - expect(dispatch).toHaveBeenCalledWith(mockNetworkRequest({ - requestKey, - promise, - ...options, - })); - }); - }); - - describe('network actions', () => { - beforeEach(() => { - networkActionSpy.mockImplementationOnce(mockNetworkAction); - }); - - describe('initializeList', () => { - it('dispatches initialize networkAction with api.initializeList', () => { - expect(module.initializeList(options)).toEqual(mockNetworkAction({ - requestKey: RequestKeys.initialize, - promise: api.initializeList(), - options, - })); - }); - }); - - describe('newEntitlementEnrollment', () => { - it('dispatches newEntitlementEnrollment networkAction', () => { - expect(module.newEntitlementEnrollment({ uuid, courseId, ...options })).toEqual( - mockNetworkAction({ - requestKey: RequestKeys.newEntitlementEnrollment, - promise: api.updateEntitlementEnrollment({ uuid, courseId }), - options, - }), - ); - }); - }); - - describe('switchEntitlementEnrollment', () => { - it('dispatches switchEntitlementEnrollment networkAction', () => { - expect(module.switchEntitlementEnrollment({ uuid, courseId, ...options })).toEqual( - mockNetworkAction({ - requestKey: RequestKeys.switchEntitlementSession, - promise: api.updateEntitlementEnrollment({ uuid, courseId }), - options, - }), - ); - }); - }); - - describe('leaveEntitlementSession', () => { - it('dispatches leaveEntitlementSession networkAction', () => { - expect(module.leaveEntitlementSession({ uuid, isRefundable, ...options })).toEqual( - mockNetworkAction({ - requestKey: RequestKeys.leaveEntitlementSession, - promise: api.deleteEntitlementEnrollment({ uuid, isRefundable }), - options, - }), - ); - }); - }); - - describe('unenrollFromCourse', () => { - it('dispatches unenrollFromCourse networkAction', () => { - expect(module.unenrollFromCourse({ courseId, ...options })).toEqual( - mockNetworkAction({ - requestKey: RequestKeys.unenrollFromCourse, - promise: api.unenrollFromCourse({ courseId }), - options, - }), - ); - }); - }); - - describe('updateEmailSettings', () => { - it('dispatches updateEmailSettings networkAction', () => { - expect(module.updateEmailSettings({ courseId, enable, ...options })).toEqual( - mockNetworkAction({ - requestKey: RequestKeys.updateEmailSettings, - promise: api.updateEmailSettings({ courseId, enable }), - options, - }), - ); - }); - }); - - describe('masqueradeAs', () => { - it('dispatches masqueradeAs initialize networkAction', () => { - expect(module.masqueradeAs({ user, ...options })).toEqual( - mockNetworkAction({ - requestKey: RequestKeys.masquerade, - promise: api.initializeList({ user }), - options, - }), - ); - }); - }); - - describe('clearMasquerade', () => { - it('dispatches clearRequest for masquerade request', () => { - module.clearMasquerade()(dispatch); - expect(dispatch).toHaveBeenCalledWith(actions.requests.clearRequest({ - requestKey: RequestKeys.masquerade, - })); - }); - }); - }); -}); diff --git a/src/data/redux/thunkActions/testUtils.js b/src/data/redux/thunkActions/testUtils.js deleted file mode 100644 index 387d84c..0000000 --- a/src/data/redux/thunkActions/testUtils.js +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -const mockStore = configureMockStore([thunk]); - -/** createTestFetcher(mockedMethod, thunkAction, args, onDispatch) - * Creates a testFetch method, which will test a given thunkAction of the form: - * ``` - * const = () => (dispatch, getState) => { - * ... - * return .then().catch(); - * ``` - * The returned function will take a promise handler function, a list of expected actions - * to have been dispatched (objects only), and an optional verifyFn method to be called after - * the fetch has been completed. - * - * @param {fn} mockedMethod - already-mocked api method being exercised by the thunkAction. - * @param {fn} thunkAction - thunkAction to call/test - * @param {array} args - array of args to dispatch the thunkAction with - * @param {[fn]} onDispatch - optional function to be called after dispatch - * - * @return {fn} testFetch method - * @param {fn} resolveFn - promise handler of the form (resolve, reject) => {}. - * should return a call to resolve or reject with response data. - * @param {object[]} expectedActions - array of action objects expected to have been dispatched - * will be verified after the thunkAction resolves - * @param {[fn]} verifyFn - optional function to be called after dispatch - */ -export const createTestFetcher = ( - mockedMethod, - thunkAction, - args, - onDispatch, -) => ( - resolveFn, - expectedActions, -) => { - const store = mockStore({}); - mockedMethod.mockReturnValue(new Promise(resolve => { - resolve(new Promise(resolveFn)); - })); - return store.dispatch(thunkAction(...args)).then(() => { - onDispatch(); - if (expectedActions !== undefined) { - expect(store.getActions()).toEqual(expectedActions); - } - }); -}; - -export default { - createTestFetcher, -}; diff --git a/src/hooks/api.js b/src/hooks/api.js new file mode 100644 index 0000000..23634b2 --- /dev/null +++ b/src/hooks/api.js @@ -0,0 +1,104 @@ +import React from 'react'; + +import { AppContext } from '@edx/frontend-platform/react'; + +import { RequestKeys } from 'data/constants/requests'; +import { post } from 'data/services/lms/utils'; +import api from 'data/services/lms/api'; + +import * as reduxHooks from 'data/redux/hooks'; +import * as module from './api'; + +const { useMakeNetworkRequest } = reduxHooks; + +export const useNetworkRequest = (action, args) => { + const makeNetworkRequest = useMakeNetworkRequest(); + return () => makeNetworkRequest({ + promise: action(), + ...args, + }); +}; + +/** + * initialize the app, loading ora and course metadata from the api, and loading the initial + * submission list data. + */ +export const useInitializeApp = () => { + const loadData = reduxHooks.useLoadData(); + return module.useNetworkRequest(api.initializeList, { + requestKey: RequestKeys.initialize, + onSuccess: ({ data }) => loadData(data), + }); +}; + +export const useNewEntitlementEnrollment = (cardId) => { + const { uuid } = reduxHooks.useCardEntitlementData(cardId); + const onSuccess = module.useInitializeApp(); + return (selection) => module.useNetworkRequest( + () => api.updateEntitlementEnrollment({ uuid, courseId: selection }), + { onSuccess, requestKey: RequestKeys.newEntitlementEnrollment }, + )(); +}; + +export const useSwitchEntitlementEnrollment = (cardId) => { + const { uuid } = reduxHooks.useCardEntitlementData(cardId); + const onSuccess = module.useInitializeApp(); + return (selection) => module.useNetworkRequest( + () => api.updateEntitlementEnrollment({ uuid, courseId: selection }), + { onSuccess, requestKey: RequestKeys.switchEntitlementSession }, + )(); +}; + +export const useLeaveEntitlementSession = (cardId) => { + const { uuid, isRefundable } = reduxHooks.useCardEntitlementData(cardId); + const onSuccess = module.useInitializeApp(); + return module.useNetworkRequest( + () => api.deleteEntitlementEnrollment({ uuid, isRefundable }), + { onSuccess, requestKey: RequestKeys.leaveEntitlementSession }, + ); +}; + +export const useUnenrollFromCourse = (cardId) => { + const { courseId } = reduxHooks.useCardCourseRunData(cardId); + return module.useNetworkRequest( + () => api.unenrollFromCourse({ courseId }), + { requestKey: RequestKeys.unenrollFromCourse }, + ); +}; + +export const useMasqueradeAs = () => { + const loadData = reduxHooks.useLoadData(); + return (user) => module.useNetworkRequest( + () => api.initializeList({ user }), + { onSuccess: ({ data }) => loadData(data), requestKey: RequestKeys.masquerade }, + )(); +}; + +export const useClearMasquerade = () => { + const clearRequest = reduxHooks.useClearRequest(); + const initializeApp = module.useInitializeApp(); + return () => { + clearRequest(RequestKeys.masquerade); + initializeApp(); + }; +}; + +export const useUpdateEmailSettings = (cardId) => { + const { courseId } = reduxHooks.useCardCourseRunData(cardId); + return (enable) => module.useNetworkRequest( + () => api.updateEmailSettings({ courseId, enable }), + { requestKey: RequestKeys.updateEmailSettings }, + )(); +}; + +export const useSendConfirmEmail = () => { + const { sendEmailUrl } = reduxHooks.useEmailConfirmationData(); + return () => post(sendEmailUrl); +}; + +export const useCreateCreditRequest = (cardId) => { + const { providerId } = reduxHooks.useCardCreditData(cardId); + const { authenticatedUser: { username } } = React.useContext(AppContext); + const { courseId } = reduxHooks.useCardCourseRunData(cardId); + return () => api.createCreditRequest({ providerId, courseId, username }); +}; diff --git a/src/hooks/api.test.js b/src/hooks/api.test.js new file mode 100644 index 0000000..29a785d --- /dev/null +++ b/src/hooks/api.test.js @@ -0,0 +1,271 @@ +import React from 'react'; +import { AppContext } from '@edx/frontend-platform/react'; +import { keyStore } from 'utils'; +import { RequestKeys } from 'data/constants/requests'; +import { post } from 'data/services/lms/utils'; +import api from 'data/services/lms/api'; + +import * as reduxHooks from 'data/redux/hooks'; +import * as apiHooks from './api'; + +const reduxKeys = keyStore(reduxHooks); + +jest.mock('data/services/lms/utils', () => ({ + post: jest.fn((...args) => ({ post: args })), +})); +jest.mock('data/services/lms/api', () => ({ + initializeList: jest.fn(), + updateEntitlementEnrollment: jest.fn(), + unenrollFromCourse: jest.fn(), + deleteEntitlementEnrollment: jest.fn(), + updateEmailSettings: jest.fn(), + createCreditRequest: jest.fn(), +})); +jest.mock('data/redux/hooks', () => ({ + useCardCourseRunData: jest.fn(), + useCardCreditData: jest.fn(), + useCardEntitlementData: jest.fn(), + useLoadData: jest.fn(), + useMakeNetworkRequest: jest.fn(), + useClearRequest: jest.fn(), + useEmailConfirmationData: jest.fn(), +})); + +const moduleKeys = keyStore(apiHooks); +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 user = 'test-user'; + +const loadData = jest.fn(); +reduxHooks.useLoadData.mockReturnValue(loadData); +const clearRequest = jest.fn(); +reduxHooks.useClearRequest.mockReturnValue(clearRequest); + +reduxHooks.useCardCourseRunData.mockReturnValue({ courseId }); +reduxHooks.useCardEntitlementData.mockReturnValue({ uuid, isRefundable }); + +let hook; +let out; + +const testInitCardHook = (hookKey) => { + test(`initializes ${hookKey} with cardId`, () => { + expect(reduxHooks[hookKey]).toHaveBeenCalledWith(cardId); + }); +}; + +const initializeApp = jest.fn(); + +const testRequestKey = (requestKey) => { + test('requestKey', () => { expect(out.requestKey).toEqual(requestKey); }); +}; + +describe('api hooks', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('useNetworkRequest', () => { + const makeNetworkRequest = jest.fn(args => ({ networkRequest: args })); + it('returns network request based on incoming action', () => { + reduxHooks.useMakeNetworkRequest.mockReturnValue(makeNetworkRequest); + const promise = Promise.resolve(testString); + const action = () => promise; + const args = { some: 'test', args: 'for you' }; + hook = apiHooks.useNetworkRequest(action, args); + expect(hook()).toEqual(makeNetworkRequest({ promise, ...args })); + }); + }); + describe('network requests', () => { + const useNetworkRequest = (action, args) => () => ({ action, ...args }); + beforeEach(() => { + jest.spyOn(apiHooks, moduleKeys.useNetworkRequest).mockImplementation(useNetworkRequest); + }); + describe('useInitializeApp', () => { + beforeEach(() => { + hook = apiHooks.useInitializeApp(); + out = hook(); + }); + it('calls initialize api method', () => { + expect(out.action).toEqual(api.initializeList); + }); + testRequestKey(RequestKeys.initialize); + it('initializes load data hook', () => { + expect(reduxHooks.useLoadData).toHaveBeenCalledWith(); + }); + it('calls loadData with data on success', () => { + out.onSuccess({ data: testString }); + expect(loadData).toHaveBeenCalledWith(testString); + }); + }); + + describe('entitlement enrollment hooks', () => { + const testInitialization = () => { + it('initializes useInitializeApp', () => { + expect(apiHooks.useInitializeApp).toHaveBeenCalledWith(); + }); + testInitCardHook(reduxKeys.useCardEntitlementData); + }; + const testArgs = (requestKey) => { + testRequestKey(requestKey); + it('initializes app on success', () => { + expect(out.onSuccess).toEqual(initializeApp); + }); + }; + beforeEach(() => { + jest.spyOn(apiHooks, moduleKeys.useInitializeApp).mockReturnValue(initializeApp); + }); + describe('useNewEntitlementEnrollment', () => { + beforeEach(() => { + hook = apiHooks.useNewEntitlementEnrollment(cardId); + out = hook(selection); + }); + testInitialization(); + testArgs(RequestKeys.newEntitlementEnrollment); + it('calls updateEntitlementEnrollment api method', () => { + out.action(selection); + expect(api.updateEntitlementEnrollment).toHaveBeenCalledWith({ + uuid, + courseId: selection, + }); + }); + }); + + describe('useSwitchEntitlementEnrollment', () => { + beforeEach(() => { + hook = apiHooks.useSwitchEntitlementEnrollment(cardId); + out = hook(selection); + }); + testInitialization(); + testArgs(RequestKeys.switchEntitlementSession); + it('calls updateEntitlementEnrollment api method', () => { + out.action(selection); + expect(api.updateEntitlementEnrollment).toHaveBeenCalledWith({ + uuid, + courseId: selection, + }); + }); + }); + + describe('useLeaveEntitlementSession', () => { + beforeEach(() => { + hook = apiHooks.useLeaveEntitlementSession(cardId); + out = hook(selection); + }); + testInitialization(); + testArgs(RequestKeys.leaveEntitlementSession); + it('calls updateEntitlementEnrollment api method', () => { + out.action(); + expect(api.deleteEntitlementEnrollment).toHaveBeenCalledWith({ + uuid, + isRefundable, + }); + }); + }); + }); + + describe('useUnenrollFromCourse', () => { + beforeEach(() => { + hook = apiHooks.useUnenrollFromCourse(cardId); + out = hook(); + }); + testInitCardHook(reduxKeys.useCardCourseRunData); + testRequestKey(RequestKeys.unenrollFromCourse); + it('calls unenrollFromCourse api method with courseId', () => { + out.action(); + expect(api.unenrollFromCourse).toHaveBeenCalledWith({ courseId }); + }); + }); + + describe('useMasqueradeAs', () => { + beforeEach(() => { + hook = apiHooks.useMasqueradeAs(cardId); + out = hook(user); + }); + it('initializes load data hook', () => { + expect(reduxHooks.useLoadData).toHaveBeenCalledWith(); + }); + testRequestKey(RequestKeys.masquerade); + it('calls initializeList api method', () => { + out.action(); + expect(api.initializeList).toHaveBeenCalledWith({ user }); + }); + it('loads data on success', () => { + out.onSuccess({ data: testString }); + expect(loadData).toHaveBeenCalledWith(testString); + }); + }); + + describe('useClearMasquerade', () => { + beforeEach(() => { + jest.spyOn(apiHooks, moduleKeys.useInitializeApp).mockReturnValue(initializeApp); + hook = apiHooks.useClearMasquerade(cardId); + }); + it('initializes clear request redux hook', () => { + expect(reduxHooks.useClearRequest).toHaveBeenCalledWith(); + }); + it('initializes useInitializeApp hook', () => { + expect(apiHooks.useInitializeApp).toHaveBeenCalledWith(); + }); + it('clears masquerade state and initializes app on call', () => { + hook(); + expect(clearRequest).toHaveBeenCalledWith(RequestKeys.masquerade); + expect(initializeApp).toHaveBeenCalledWith(); + }); + }); + + describe('useUpdateEmailSettings', () => { + const enable = 'test-enable'; + beforeEach(() => { + hook = apiHooks.useUpdateEmailSettings(cardId); + out = hook(enable); + }); + testInitCardHook(reduxKeys.useCardCourseRunData); + testRequestKey(RequestKeys.updateEmailSettings); + it('calls updateEmailSettings api method on call', () => { + out.action(); + expect(api.updateEmailSettings).toHaveBeenCalledWith({ courseId, enable }); + }); + }); + + describe('useSendConfirmEmail', () => { + const sendEmailUrl = 'test-send-email-url'; + beforeEach(() => { + reduxHooks.useEmailConfirmationData.mockReturnValue({ sendEmailUrl }); + hook = apiHooks.useSendConfirmEmail(cardId); + out = hook(); + }); + it('initializes useEmailConfirmationData hook', () => { + expect(reduxHooks.useEmailConfirmationData).toHaveBeenCalledWith(); + }); + it('posts to email url on call', () => { + expect(out).toEqual(post(sendEmailUrl)); + }); + }); + + describe('useCreateCreditRequest', () => { + const username = 'test-username'; + const providerId = 'test-provider-id'; + beforeEach(() => { + React.useContext.mockReturnValue({ authenticatedUser: { username } }); + reduxHooks.useCardCreditData.mockReturnValue({ providerId }); + hook = apiHooks.useCreateCreditRequest(cardId); + }); + testInitCardHook(reduxKeys.useCardCreditData); + testInitCardHook(reduxKeys.useCardCourseRunData); + it('initializes username from app context', () => { + expect(React.useContext).toHaveBeenCalledWith(AppContext); + }); + it('calls createCreditRequest api method on call', () => { + out = hook(); + expect(api.createCreditRequest).toHaveBeenCalledWith({ + providerId, + courseId, + username, + }); + }); + }); + }); +}); diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..4026160 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,7 @@ +import * as redux from 'data/redux/hooks'; +import * as api from './api'; +import * as utils from './utils'; + +export const reduxHooks = redux; +export const apiHooks = api; +export const utilHooks = utils; diff --git a/src/hooks.js b/src/hooks/utils.js similarity index 57% rename from src/hooks.js rename to src/hooks/utils.js index 9c28275..2f0d76c 100644 --- a/src/hooks.js +++ b/src/hooks/utils.js @@ -1,16 +1,17 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import dateFormatter from 'utils/dateFormatter'; export const useValueCallback = (cb, prereqs = []) => ( React.useCallback(e => cb(e.target.value), prereqs) // eslint-disable-line ); -export const nullMethod = () => ({}); - -export { useIntl }; +export const useFormatDate = () => { + const { formatDate } = useIntl(); + return (date) => dateFormatter(formatDate, date); +}; export default { + useFormatDate, useValueCallback, - nullMethod, - useIntl, }; diff --git a/src/hooks.test.jsx b/src/hooks/utils.test.js similarity index 62% rename from src/hooks.test.jsx rename to src/hooks/utils.test.js index 5685856..e5420ad 100644 --- a/src/hooks.test.jsx +++ b/src/hooks/utils.test.js @@ -1,19 +1,14 @@ -import * as hooks from './hooks'; +import * as utils from './utils'; -jest.unmock('./hooks'); +jest.unmock('./utils'); describe('app-level hooks', () => { - describe('nullMethod', () => { - it('returns an empty object', () => { - expect(hooks.nullMethod()).toEqual({}); - }); - }); describe('useValuecallback', () => { it('returns react callback with event target value', () => { const cb = val => ({ cb: val }); const prereqs = ['test', 'prereqs']; const value = 'test-value'; - const out = hooks.useValueCallback(cb, prereqs); + const out = utils.useValueCallback(cb, prereqs); expect(out.useCallback.cb({ target: { value } })).toEqual({ cb: value }); expect(out.useCallback.prereqs).toEqual(prereqs); }); diff --git a/src/setupTest.jsx b/src/setupTest.jsx index 78e426a..fa5e8f8 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -215,9 +215,9 @@ jest.mock('data/constants/app', () => ({ locationId: 'fake-location-id', })); -jest.mock('hooks', () => ({ - ...jest.requireActual('hooks'), - nullMethod: jest.fn().mockName('hooks.nullMethod'), +jest.mock('utils', () => ({ + ...jest.requireActual('utils'), + nullMethod: jest.fn().mockName('utils.nullMethod'), })); jest.mock('utils/hooks', () => { diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx index 09663c4..5de1011 100644 --- a/src/test/app.test.jsx +++ b/src/test/app.test.jsx @@ -15,7 +15,6 @@ import { mergeConfig, } from '@edx/frontend-platform'; -import thunk from 'redux-thunk'; import { useIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { useFormatDate } from 'utils/hooks'; @@ -24,7 +23,8 @@ import api from 'data/services/lms/api'; import * as fakeData from 'data/services/lms/fakeData/courses'; import { RequestKeys, RequestStates } from 'data/constants/requests'; import reducers from 'data/redux'; -import { selectors, thunkActions } from 'data/redux'; +import { selectors } from 'data/redux'; +import { apiHooks } from 'hooks'; import { cardId as genCardId } from 'data/redux/app/reducer'; import messages from 'i18n'; @@ -50,6 +50,19 @@ jest.mock('@edx/frontend-platform', () => ({ getConfig: () => jest.requireActual('../config').configuration, })); +jest.mock('@edx/frontend-platform/analytics', () => ({ + sendTrackEvent: jest.fn(), +})); + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), + getLoginRedirectUrl: jest.fn(), +})); + +jest.mock('@edx/frontend-enterprise-hotjar', () => ({ + initializeHotjar: jest.fn(), +})); + jest.mock('@edx/frontend-platform/i18n', () => ({ ...jest.requireActual('@edx/frontend-platform/i18n'), useIntl: () => ({ @@ -58,6 +71,10 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ }), })); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + jest.mock('utils/hooks', () => { const formatDate = jest.fn(date => `Date-${date}`); return { @@ -66,18 +83,9 @@ jest.mock('utils/hooks', () => { }; }); -jest.mock('@edx/frontend-platform/auth', () => ({ - getAuthenticatedHttpClient: jest.fn(), - getLoginRedirectUrl: jest.fn(), -})); - -jest.mock('@edx/frontend-platform/analytics', () => ({ - sendTrackEvent: jest.fn(), -})); const configureStore = () => redux.createStore( reducers, - redux.compose(redux.applyMiddleware(thunk)), ); let el; @@ -121,6 +129,8 @@ const { compileCourseRunData, compileEntitlementData } = fakeData; const initCourses = jest.fn(() => []); +let initializeApp; + const mockApi = () => { api.initializeList = jest.fn(() => new Promise( (resolve, reject) => { @@ -190,10 +200,9 @@ describe('ESG app integration tests', () => { 'course-name-1', 'course-name-2', ]; - const testCourse = async (tests) => { + const testCourse = async (index, tests) => { await getState(); const cards = inspector.get.courseCards; - const index = 0; const card = cards.at(index); const cardId = genCardId(index); const cardDetails = inspector.get.card.details(card); @@ -206,70 +215,70 @@ describe('ESG app integration tests', () => { } const loadCourse = async (course) => { - initCourses.mockReturnValue([course].map(compileCourseRunData)); - store.dispatch(thunkActions.app.initialize()); + await loadApp([course].map(compileCourseRunData)); await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending); resolveFns.init.success(); await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed); }; - test('audit', async () => { - const courses = [ - { courseName: courseNames[0] }, // audit, course run not started - { - courseName: courseNames[1], - enrollment: { - coursewareAccess: { - isTooEarly: true, - hasUnmetPrerequisites: false, - isStaff: false, + describe('audit courses', () => { + test('audit', async () => { + const courses = [ + { courseName: courseNames[0] }, // audit, course run not started + { + courseName: courseNames[1], + enrollment: { + coursewareAccess: { + isTooEarly: true, + hasUnmetPrerequisites: false, + isStaff: false, + }, }, + }, // audit, course run not started, is too early + { + courseName: courseNames[2], + courseRun: { + courseRun: { isStarted: true }, + }, + enrollment: { + accessExpirationDate: fakeData.pastDate, + canUpgrade: false, + isAuditAccessExpired: true, + hasStarted: true, + }, + }, // audit, course run and learner started, access expired, cannot upgrade + ]; + const formatDate = useFormatDate(); + await loadApp(courses); + await testCourse(0, [ + ({ cardId, cardDetails }) => { + const enrollment = selectors.app.courseCard.enrollment(state, cardId); + const courseRun = selectors.app.courseCard.courseRun(state, cardId); + const courseProvider = selectors.app.courseCard.courseProvider(state, cardId); + const course = selectors.app.courseCard.course(state, cardId); + expect(enrollment.isAudit).toEqual(true); + expect(courseRun.isStarted).toEqual(false); + expect(enrollment.canUpgrade).toEqual(true); + [ + courseProvider.name, + course.courseNumber, + appMessages.withValues.CourseCardDetails.courseStarts({ + startDate: formatDate(new Date(courseRun.startDate)), + }), + ].forEach(value => inspector.verifyTextIncludes(cardDetails, value)); }, - }, // audit, course run not started, is too early - { - courseName: courseNames[2], - courseRun: { - courseRun: { isStarted: true }, + ]); + await testCourse(1, [ + ({ cardId, cardDetails }) => { + const enrollment = selectors.app.courseCard.enrollment(state, cardId); + const courseRun = selectors.app.courseCard.courseRun(state, cardId); + expect(enrollment.isAudit).toEqual(true); + expect(courseRun.isStarted).toEqual(false); + expect(enrollment.coursewareAccess.isTooEarly).toEqual(true); + expect(enrollment.hasAccess).toEqual(false); }, - enrollment: { - accessExpirationDate: fakeData.pastDate, - canUpgrade: false, - isAuditAccessExpired: true, - hasStarted: true, - }, - }, // audit, course run and learner started, access expired, cannot upgrade - ]; - const formatDate = useFormatDate(); - await loadApp([courses[0]]); - await testCourse([ - ({ cardId, cardDetails }) => { - const enrollment = selectors.app.courseCard.enrollment(state, cardId); - const courseRun = selectors.app.courseCard.courseRun(state, cardId); - const courseProvider = selectors.app.courseCard.courseProvider(state, cardId); - const course = selectors.app.courseCard.course(state, cardId); - expect(enrollment.isAudit).toEqual(true); - expect(courseRun.isStarted).toEqual(false); - expect(enrollment.canUpgrade).toEqual(true); - [ - courseProvider.name, - course.courseNumber, - appMessages.withValues.CourseCardDetails.courseStarts({ - startDate: formatDate(new Date(courseRun.startDate)), - }), - ].forEach(value => inspector.verifyTextIncludes(cardDetails, value)); - }, - ]); - await loadCourse(courses[1]); - await testCourse([ - ({ cardId, cardDetails }) => { - const enrollment = selectors.app.courseCard.enrollment(state, cardId); - const courseRun = selectors.app.courseCard.courseRun(state, cardId); - expect(enrollment.isAudit).toEqual(true); - expect(courseRun.isStarted).toEqual(false); - expect(enrollment.coursewareAccess.isTooEarly).toEqual(true); - expect(enrollment.hasAccess).toEqual(false); - }, - ]); + ]); + }); }); }); }); diff --git a/src/utils/index.js b/src/utils/index.js index 0db13b4..113e236 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,3 +1,5 @@ +export const nullMethod = () => ({}); + export { default as StrictDict } from './StrictDict'; export { default as keyStore } from './keyStore'; export { default as dateFormatter } from './dateFormatter'; diff --git a/src/widgets/LookingForChallengeWidget/index.jsx b/src/widgets/LookingForChallengeWidget/index.jsx index 993a126..4c4fbd6 100644 --- a/src/widgets/LookingForChallengeWidget/index.jsx +++ b/src/widgets/LookingForChallengeWidget/index.jsx @@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Card, Hyperlink, Icon } from '@edx/paragon'; import { ArrowForward } from '@edx/paragon/icons'; -import { hooks } from 'data/redux'; +import { reduxHooks } from 'hooks'; import moreCoursesSVG from 'assets/more-courses-sidewidget.svg'; import track from '../RecommendationsPanel/track'; @@ -14,8 +14,8 @@ import './index.scss'; export const arrowIcon = (); export const LookingForChallengeWidget = () => { - const { courseSearchUrl } = hooks.usePlatformSettingsData(); const { formatMessage } = useIntl(); + const { courseSearchUrl } = reduxHooks.usePlatformSettingsData(); return ( ({ - hooks: { +jest.mock('hooks', () => ({ + reduxHooks: { usePlatformSettingsData: () => ({ courseSearchUrl: 'course-search-url', }), diff --git a/src/widgets/RecommendationsPanel/LoadedView.jsx b/src/widgets/RecommendationsPanel/LoadedView.jsx index 2b8832b..d286fc2 100644 --- a/src/widgets/RecommendationsPanel/LoadedView.jsx +++ b/src/widgets/RecommendationsPanel/LoadedView.jsx @@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; import { Search } from '@edx/paragon/icons'; -import { hooks } from 'data/redux'; +import { reduxHooks } from 'hooks'; import track from './track'; import CourseCard from './components/CourseCard'; import messages from './messages'; @@ -16,8 +16,8 @@ export const LoadedView = ({ courses, isControl, }) => { - const { courseSearchUrl } = hooks.usePlatformSettingsData(); const { formatMessage } = useIntl(); + const { courseSearchUrl } = reduxHooks.usePlatformSettingsData(); return (
diff --git a/src/widgets/RecommendationsPanel/LoadedView.test.jsx b/src/widgets/RecommendationsPanel/LoadedView.test.jsx index f466d15..54a29b7 100644 --- a/src/widgets/RecommendationsPanel/LoadedView.test.jsx +++ b/src/widgets/RecommendationsPanel/LoadedView.test.jsx @@ -6,8 +6,8 @@ import mockData from './mockData'; import messages from './messages'; jest.mock('./components/CourseCard', () => 'CourseCard'); -jest.mock('data/redux', () => ({ - hooks: { +jest.mock('hooks', () => ({ + reduxHooks: { usePlatformSettingsData: () => ({ courseSearchUrl: 'course-search-url', }),