diff --git a/src/containers/DashboardModal/hooks.js b/src/containers/DashboardModal/hooks.js new file mode 100644 index 0000000..5bc88ce --- /dev/null +++ b/src/containers/DashboardModal/hooks.js @@ -0,0 +1,48 @@ +import React from 'react'; + +import { StrictDict } from 'utils'; +import track from 'tracking'; +import { reduxHooks } from 'hooks'; + +import * as module from './hooks'; + +export const state = StrictDict({ + showModal: (val) => React.useState(val), // eslint-disable-line +}); + +const { modalOpened, modalClosed, modalCTAClicked } = track.enterpriseDashboard; + +export const useEnterpriseDashboardHook = () => { + const [showModal, setShowModal] = module.state.showModal(true); + const dashboard = reduxHooks.useEnterpriseDashboardData(); + + const trackOpened = modalOpened(dashboard.enterpriseUUID); + const trackClose = modalClosed(dashboard.enterpriseUUID, 'Cancel button'); + const trackEscape = modalClosed(dashboard.enterpriseUUID, 'Escape'); + + const handleCTAClick = modalCTAClicked(dashboard.enterpriseUUID, dashboard.url); + const handleClose = () => { + trackClose(); + setShowModal(false); + }; + const handleEscape = () => { + trackEscape(); + setShowModal(false); + }; + + React.useEffect(() => { + if (dashboard && dashboard.label) { + trackOpened(); + } + }, []); // eslint-disable-line + + return { + showModal, + handleCTAClick, + handleClose, + handleEscape, + dashboard, + }; +}; + +export default useEnterpriseDashboardHook; diff --git a/src/containers/DashboardModal/hooks.test.js b/src/containers/DashboardModal/hooks.test.js new file mode 100644 index 0000000..b309dcb --- /dev/null +++ b/src/containers/DashboardModal/hooks.test.js @@ -0,0 +1,75 @@ +import { MockUseState } from 'testUtils'; +import { reduxHooks } from 'hooks'; +import track from 'tracking'; + +import * as hooks from './hooks'; + +jest.mock('hooks', () => ({ + reduxHooks: { + useEnterpriseDashboardData: jest.fn(), + }, +})); +jest.mock('tracking', () => { + const modalOpenedEvent = jest.fn(); + const modalClosedEvent = jest.fn(); + const modalCTAClickedEvent = jest.fn(); + return { + __esModule: true, + default: { + enterpriseDashboard: { + modalOpenedEvent, + modalClosedEvent, + modalCTAClickedEvent, + modalOpened: jest.fn(() => modalOpenedEvent), + modalClosed: jest.fn(() => modalClosedEvent), + modalCTAClicked: jest.fn(() => modalCTAClickedEvent), + }, + }, + }; +}); + +const state = new MockUseState(hooks); + +const enterpriseDashboardData = { label: 'edX, Inc.', url: '/edx-dashboard' }; + +describe('EnterpriseDashboard hooks', () => { + reduxHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData }); + + describe('state values', () => { + state.testGetter(state.keys.showModal); + }); + + describe('behavior', () => { + let out; + + beforeEach(() => { + state.mock(); + out = hooks.useEnterpriseDashboardHook(); + }); + afterEach(state.restore); + + test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => { + expect(out.dashboard).toMatchObject(enterpriseDashboardData); + }); + + test('modal initializes to shown when rendered and closes on click', () => { + state.expectInitializedWith(state.keys.showModal, true); + out.handleClose(); + expect(state.values.showModal).toEqual(false); + }); + + test('modal initializes to shown when rendered and closes on escape', () => { + state.expectInitializedWith(state.keys.showModal, true); + out.handleEscape(); + expect(state.values.showModal).toEqual(false); + }); + + test('CTA click tracks modalCTAClicked', () => { + out.handleCTAClick(); + expect(track.enterpriseDashboard.modalCTAClicked).toHaveBeenCalledWith( + enterpriseDashboardData.enterpriseUUID, + enterpriseDashboardData.url, + ); + }); + }); +}); diff --git a/src/plugin-slots/DashboardModalSlot/README.md b/src/plugin-slots/DashboardModalSlot/README.md index 1df0c34..9477acc 100644 --- a/src/plugin-slots/DashboardModalSlot/README.md +++ b/src/plugin-slots/DashboardModalSlot/README.md @@ -22,9 +22,6 @@ const config = { type: DIRECT_PLUGIN, priority: 60, RenderWidget: DashboardModal, - content: { - store, - }, }, }, ], diff --git a/src/plugin-slots/README.md b/src/plugin-slots/README.md index d6c897f..9adff41 100644 --- a/src/plugin-slots/README.md +++ b/src/plugin-slots/README.md @@ -5,4 +5,4 @@ * [`widget_sidebar_slot`](./WidgetSidebarSlot/) * [`course_list_slot`](./CourseListSlot/) * [`no_courses_view_slot`](./NoCoursesViewSlot/) -* [`dashboard_modal_slot`](./DashboardModalSlot) \ No newline at end of file +* [`dashboard_modal_slot](./DashboardModalSlot) \ No newline at end of file diff --git a/src/tracking/constants.js b/src/tracking/constants.js index 731d2fb..cc51a48 100644 --- a/src/tracking/constants.js +++ b/src/tracking/constants.js @@ -20,8 +20,13 @@ export const events = StrictDict({ leaveSession: 'leaveSession', unenrollReason: 'unenrollReason', entitlementUnenrollReason: 'entitlementUnenrollReason', + dashboardModalOpened: 'dashboardModalOpened', + dashboardModalCTAClicked: 'dashboardModalCTAClicked', + dashboardModalClosed: 'dashboardModalClosed', }); +const learnerPortal = 'edx.ui.enterprise.lms.dashboard.learner_portal_modal'; + export const eventNames = StrictDict({ enterCourseClicked: 'edx.bi.dashboard.enter_course.clicked', courseImageClicked: 'edx.bi.dashboard.course_image.clicked', @@ -34,6 +39,9 @@ export const eventNames = StrictDict({ leaveSession: 'course-dashboard.leave-session', unenrollReason: 'unenrollment_reason.selected', entitlementUnenrollReason: 'entitlement_unenrollment_reason.selected', + dashboardModalOpened: `${learnerPortal}.opened`, + dashboardModalCTAClicked: `${learnerPortal}.dashboard_cta.clicked`, + dashboardModalClosed: `${learnerPortal}.closed`, findCoursesClicked: 'edx.bi.dashboard.find_courses_button.clicked', purchaseCredit: 'edx.bi.credit.clicked_purchase_credit', filterClicked: 'course-dashboard.filter.clicked', diff --git a/src/tracking/index.js b/src/tracking/index.js index 15fdf2c..4036a6e 100644 --- a/src/tracking/index.js +++ b/src/tracking/index.js @@ -1,6 +1,7 @@ import course from './trackers/course'; import credit from './trackers/credit'; import engagement from './trackers/engagement'; +import enterpriseDashboard from './trackers/enterpriseDashboard'; import entitlements from './trackers/entitlements'; import socialShare from './trackers/socialShare'; import findCourses from './trackers/findCourses'; @@ -10,6 +11,7 @@ export default { course, credit, engagement, + enterpriseDashboard, entitlements, socialShare, findCourses, diff --git a/src/tracking/trackers/enterpriseDashboard.js b/src/tracking/trackers/enterpriseDashboard.js new file mode 100644 index 0000000..6707d16 --- /dev/null +++ b/src/tracking/trackers/enterpriseDashboard.js @@ -0,0 +1,44 @@ +import { createEventTracker, createLinkTracker } from 'data/services/segment/utils'; +import { eventNames } from '../constants'; + +/** Enterprise Dashboard events**/ +/** + * Creates tracking callback for Enterprise Dashboard Modal open event + * @param {string} enterpriseUUID - enterprise identifier + * @return {func} - Callback that tracks the event when fired. + */ +export const modalOpened = (enterpriseUUID) => () => createEventTracker( + eventNames.dashboardModalOpened, + { enterpriseUUID }, +); + +/** + * Creates tracking callback for Enterprise Dashboard Modal Call-to-action click-event + * @param {string} enterpriseUUID - enterprise identifier + * @param {string} href - destination url + * @return {func} - Callback that tracks the event when fired and then loads the passed href. + */ +export const modalCTAClicked = (enterpriseUUID, href) => createLinkTracker( + createEventTracker( + eventNames.dashboardModalCTAClicked, + { enterpriseUUID }, + ), + href, +); + +/** + * Creates tracking callback for Enterprise Dashboard Modal close event + * @param {string} enterpriseUUID - enterprise identifier + * @param {string} source - close event soruce ("Cancel button" vs "Escape") + * @return {func} - Callback that tracks the event when fired. + */ +export const modalClosed = (enterpriseUUID, source) => createEventTracker( + eventNames.dashboardModalClosed, + { enterpriseUUID, source }, +); + +export default { + modalOpened, + modalCTAClicked, + modalClosed, +}; diff --git a/src/tracking/trackers/enterpriseDashboard.test.js b/src/tracking/trackers/enterpriseDashboard.test.js new file mode 100644 index 0000000..efbb62b --- /dev/null +++ b/src/tracking/trackers/enterpriseDashboard.test.js @@ -0,0 +1,47 @@ +import { createEventTracker } from 'data/services/segment/utils'; +import { eventNames } from '../constants'; +import * as trackers from './enterpriseDashboard'; + +jest.mock('data/services/segment/utils', () => ({ + createEventTracker: jest.fn(args => ({ createEventTracker: args })), + createLinkTracker: jest.fn((cb, href) => ({ createLinkTracker: { cb, href } })), +})); + +const enterpriseUUID = 'test-enterprise-uuid'; +const source = 'test-source'; + +describe('enterpriseDashboard trackers', () => { + describe('modalOpened', () => { + it('creates event tracker for dashboard modal opened event', () => { + expect(trackers.modalOpened(enterpriseUUID, source)()).toEqual( + createEventTracker( + eventNames.dashboardModalOpened, + { enterpriseUUID, source }, + ), + ); + }); + }); + describe('modalCTAClicked', () => { + const testHref = 'test-href'; + it('creates link tracker for dashboard modal cta click event', () => { + const { cb, href } = trackers.modalCTAClicked(enterpriseUUID, testHref).createLinkTracker; + expect(href).toEqual(testHref); + expect(cb).toEqual( + createEventTracker( + eventNames.dashboardModalCTAClicked, + { enterpriseUUID, source }, + ), + ); + }); + }); + describe('modalClosed', () => { + it('creates event tracker for dashboard modal closed event with close source', () => { + expect(trackers.modalClosed(enterpriseUUID, source)).toEqual( + createEventTracker( + eventNames.dashboardModalClosed, + { enterpriseUUID, source }, + ), + ); + }); + }); +});