feat: tracking upgrade and discovery (#66)
This commit is contained in:
@@ -5,6 +5,7 @@ import { Locked } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import useTrackUpgradeData from './hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -14,11 +15,14 @@ export const UpgradeButton = ({ cardId }) => {
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const isEnabled = (!isMasquerading && canUpgrade);
|
||||
const { trackUpgradeClick } = useTrackUpgradeData();
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!isEnabled}
|
||||
onClick={trackUpgradeClick}
|
||||
{...isEnabled && { as: 'a', href: upgradeUrl }}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
|
||||
@@ -12,6 +12,9 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
jest.mock('./hooks', () => () => ({
|
||||
trackUpgradeClick: jest.fn().mockName('trackUpgradeClick'),
|
||||
}));
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
const props = {
|
||||
|
||||
@@ -6,6 +6,7 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
|
||||
disabled={false}
|
||||
href="upgradeUrl"
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
onClick={[MockFunction trackUpgradeClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
@@ -16,6 +17,7 @@ exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
onClick={[MockFunction trackUpgradeClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
@@ -26,6 +28,7 @@ exports[`UpgradeButton snapshot masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
onClick={[MockFunction trackUpgradeClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { handleEvent } from 'data/services/segment/utils';
|
||||
import { eventNames } from 'data/services/segment/constants';
|
||||
|
||||
export const useTrackUpgradeData = () => {
|
||||
const trackUpgradeClick = () => {
|
||||
handleEvent(eventNames.upgradeCourse, {
|
||||
pageName: 'learner_home',
|
||||
linkType: 'button',
|
||||
linkCategory: 'green_upgrade',
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
trackUpgradeClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTrackUpgradeData;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { handleEvent } from 'data/services/segment/utils';
|
||||
import { eventNames } from 'data/services/segment/constants';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/services/segment/utils', () => ({
|
||||
handleEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CourseCardActions hooks', () => {
|
||||
describe('useTrackUpgradeData', () => {
|
||||
it('calls handleEvent with correct params', () => {
|
||||
const out = hooks.useTrackUpgradeData();
|
||||
out.trackUpgradeClick();
|
||||
expect(handleEvent).toHaveBeenCalledWith(eventNames.upgradeCourse, {
|
||||
pageName: 'learner_home',
|
||||
linkType: 'button',
|
||||
linkCategory: 'green_upgrade',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,12 @@ export const events = StrictDict({
|
||||
entitlementUnenroll: 'entitlementUnenroll',
|
||||
sessionChange: 'sessionChange',
|
||||
unenrollReason: 'unenrollReason',
|
||||
upgradeCourse: 'upgradeCourse',
|
||||
});
|
||||
|
||||
export const eventNames = StrictDict({
|
||||
[events.courseEnroll]: 'edx.bi.user.program-details.enrollment',
|
||||
[events.upgradeCourse]: 'learner_home.course_card.upgrade',
|
||||
[events.entitlementUnenroll]: 'entitlement_unenrollment_reason.selected',
|
||||
[events.sessionChange]: ({ action }) => `course-dashboard.${action}-session`, // 'switch', 'new', 'leave'
|
||||
[events.unenrollReason]: 'unenrollment_reason.selected',
|
||||
|
||||
@@ -19,6 +19,7 @@ exports[`LookingForChallengeWidget snapshots default 1`] = `
|
||||
<Hyperlink
|
||||
className="d-flex align-items-center"
|
||||
destination="course-search-url"
|
||||
onClick={[MockFunction courseSearchClickTracker]}
|
||||
variant="brand"
|
||||
>
|
||||
<format-message-function
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Card, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { ArrowForward } from '@edx/paragon/icons';
|
||||
@@ -11,9 +13,11 @@ import './index.scss';
|
||||
|
||||
export const arrowIcon = (<Icon className="mx-1" src={ArrowForward} />);
|
||||
|
||||
export const LookingForChallengeWidget = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
export const LookingForChallengeWidget = ({
|
||||
courseSearchClickTracker,
|
||||
}) => {
|
||||
const { courseSearchUrl } = hooks.usePlatformSettingsData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<Card orientation="horizontal" id="looking-for-challenge-widget">
|
||||
<Card.ImageCap
|
||||
@@ -25,7 +29,12 @@ export const LookingForChallengeWidget = () => {
|
||||
{formatMessage(messages.lookingForChallengePrompt)}
|
||||
</h4>
|
||||
<h5>
|
||||
<Hyperlink variant="brand" destination={courseSearchUrl} className="d-flex align-items-center">
|
||||
<Hyperlink
|
||||
variant="brand"
|
||||
destination={courseSearchUrl}
|
||||
onClick={courseSearchClickTracker}
|
||||
className="d-flex align-items-center"
|
||||
>
|
||||
{formatMessage(messages.findCoursesButton, { arrow: arrowIcon })}
|
||||
</Hyperlink>
|
||||
</h5>
|
||||
@@ -34,4 +43,8 @@ export const LookingForChallengeWidget = () => {
|
||||
);
|
||||
};
|
||||
|
||||
LookingForChallengeWidget.propTypes = {
|
||||
courseSearchClickTracker: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LookingForChallengeWidget;
|
||||
|
||||
@@ -11,9 +11,12 @@ jest.mock('data/redux', () => ({
|
||||
}));
|
||||
|
||||
describe('LookingForChallengeWidget', () => {
|
||||
const props = {
|
||||
courseSearchClickTracker: jest.fn().mockName('courseSearchClickTracker'),
|
||||
};
|
||||
describe('snapshots', () => {
|
||||
test('default', () => {
|
||||
const wrapper = shallow(<LookingForChallengeWidget />);
|
||||
const wrapper = shallow(<LookingForChallengeWidget {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import './index.scss';
|
||||
|
||||
export const LoadedView = ({
|
||||
courses,
|
||||
courseSearchClickTracker,
|
||||
}) => {
|
||||
const { courseSearchUrl } = hooks.usePlatformSettingsData();
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -31,6 +32,7 @@ export const LoadedView = ({
|
||||
iconBefore={Search}
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
onClick={courseSearchClickTracker}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
</Button>
|
||||
@@ -46,6 +48,7 @@ LoadedView.propTypes = {
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
})).isRequired,
|
||||
courseSearchClickTracker: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import LoadedView from './LoadedView';
|
||||
import mockData from './mockData';
|
||||
|
||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: 'course-search-url',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const courseSearchUrl = 'test-course-search-url';
|
||||
hooks.usePlatformSettingsData.mockReturnValue(courseSearchUrl);
|
||||
|
||||
describe('RecommendationsPanel LoadedView', () => {
|
||||
const props = {
|
||||
courses: mockData.courses,
|
||||
courseSearchClickTracker: jest.fn().mockName('courseSearchClickTracker'),
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<LoadedView courses={mockData.courses} />)).toMatchSnapshot();
|
||||
expect(shallow(<LoadedView {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,8 @@ exports[`RecommendationsPanel LoadedView snapshot 1`] = `
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
onClick={[MockFunction courseSearchClickTracker]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Explore courses
|
||||
|
||||
@@ -2,10 +2,13 @@ import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { RequestStates } from 'data/constants/requests';
|
||||
import { handleEvent } from 'data/services/segment/utils';
|
||||
|
||||
import * as module from './hooks';
|
||||
import api from './api';
|
||||
|
||||
export const searchCourseEventName = 'learner_home.widget.search_course';
|
||||
|
||||
export const state = StrictDict({
|
||||
requestState: (val) => React.useState(val), // eslint-disable-line
|
||||
data: (val) => React.useState(val), // eslint-disable-line
|
||||
@@ -34,12 +37,18 @@ export const useRecommendationPanelData = () => {
|
||||
const [data, setData] = module.state.data({});
|
||||
module.useFetchCourses(setRequestState, setData);
|
||||
const courses = data.data?.courses || [];
|
||||
const courseSearchClickTracker = () => handleEvent(searchCourseEventName, {
|
||||
pageName: 'learner_home',
|
||||
linkType: 'button',
|
||||
linkCategory: 'search_button',
|
||||
});
|
||||
return {
|
||||
courses,
|
||||
isLoaded: requestState === RequestStates.completed && courses.length > 0,
|
||||
isFailed: requestState === RequestStates.failed
|
||||
|| (requestState === RequestStates.completed && courses.length === 0),
|
||||
isLoading: requestState === RequestStates.pending,
|
||||
courseSearchClickTracker,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { RequestStates } from 'data/constants/requests';
|
||||
import { handleEvent } from 'data/services/segment/utils';
|
||||
|
||||
import api from './api';
|
||||
import * as hooks from './hooks';
|
||||
@@ -9,6 +10,9 @@ import * as hooks from './hooks';
|
||||
jest.mock('./api', () => ({
|
||||
fetchRecommendedCourses: jest.fn(),
|
||||
}));
|
||||
jest.mock('data/services/segment/utils', () => ({
|
||||
handleEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
@@ -96,6 +100,16 @@ describe('RecommendationsPanel hooks', () => {
|
||||
it('initializes requestState as RequestStates.pending', () => {
|
||||
state.expectInitializedWith(state.keys.requestState, RequestStates.pending);
|
||||
});
|
||||
describe('courseSearchClickTracker behavior', () => {
|
||||
it('calls handleEvent with correct args', () => {
|
||||
out.courseSearchClickTracker();
|
||||
expect(handleEvent).toHaveBeenCalledWith(hooks.searchCourseEventName, {
|
||||
pageName: 'learner_home',
|
||||
linkType: 'button',
|
||||
linkCategory: 'search_button',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('request is completed, with returned courses', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -11,19 +11,20 @@ export const RecommendationsPanel = () => {
|
||||
isFailed,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
courseSearchClickTracker,
|
||||
} = hooks.useRecommendationPanelData();
|
||||
|
||||
if (isLoading) {
|
||||
return (<LoadingView />);
|
||||
}
|
||||
if (isLoaded) {
|
||||
return (<LoadedView courses={courses} />);
|
||||
return (<LoadedView courses={courses} courseSearchClickTracker={courseSearchClickTracker} />);
|
||||
}
|
||||
if (isFailed) {
|
||||
return (<LookingForChallengeWidget />);
|
||||
return (<LookingForChallengeWidget courseSearchClickTracker={courseSearchClickTracker} />);
|
||||
}
|
||||
// default fallback
|
||||
return (<LookingForChallengeWidget />);
|
||||
return (<LookingForChallengeWidget courseSearchClickTracker={courseSearchClickTracker} />);
|
||||
};
|
||||
|
||||
export default RecommendationsPanel;
|
||||
|
||||
@@ -18,46 +18,48 @@ jest.mock('./LoadedView', () => 'LoadedView');
|
||||
const { courses } = mockData;
|
||||
|
||||
describe('RecommendationsPanel snapshot', () => {
|
||||
const defaultProps = {
|
||||
courseSearchClickTracker: jest.fn().mockName('courseSearchClickTracker'),
|
||||
};
|
||||
const defaultValues = {
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
courses: [],
|
||||
...defaultProps,
|
||||
};
|
||||
it('displays LoadingView if request is loading', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses: [],
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
...defaultValues,
|
||||
isLoading: true,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
|
||||
});
|
||||
it('displays LoadedView with courses if request is loaded', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
courses,
|
||||
isFailed: false,
|
||||
isLoaded: true,
|
||||
isLoading: false,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LoadedView courses={courses} />),
|
||||
shallow(<LoadedView courses={courses} {...defaultProps} />),
|
||||
);
|
||||
});
|
||||
it('displays LookingForChallengeWidget if request is failed', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses: [],
|
||||
...defaultValues,
|
||||
isFailed: true,
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LookingForChallengeWidget />),
|
||||
shallow(<LookingForChallengeWidget {...defaultProps} />),
|
||||
);
|
||||
});
|
||||
it('defaults to LookingForChallengeWidget if no flags are true', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses: [],
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
...defaultValues,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LookingForChallengeWidget />),
|
||||
shallow(<LookingForChallengeWidget {...defaultProps} />),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user