Skip to content

Commit

Permalink
Merge pull request #231 from edx/fsheets/checklists-analytics
Browse files Browse the repository at this point in the history
feat(checklists): add analytics tracking to checklists
  • Loading branch information
MichaelRoytman authored Jul 20, 2018
2 parents 4c5b28e + b482046 commit 92c10a2
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 36 deletions.
65 changes: 52 additions & 13 deletions src/components/CourseChecklist/CourseChecklist.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntlProvider, FormattedMessage, FormattedNumber } from 'react-intl';
import React from 'react';

import CourseChecklist from '.';
import { courseDetails } from '../../utils/testConstants';
import getFilteredChecklist from '../../utils/CourseChecklist/getFilteredChecklist';
import getValidatedValue from '../../utils/CourseChecklist/getValidatedValue';
import messages from './displayMessages';
Expand Down Expand Up @@ -43,6 +44,10 @@ getFilteredChecklist.mockImplementation(
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
const { intl } = intlProvider.getChildContext();

global.analytics = {
track: () => {},
};

let wrapper;

const testData = {
Expand All @@ -61,12 +66,16 @@ const defaultProps = {
dataHeading: <WrappedMessage message="test" />,
dataList: testChecklistData,
idPrefix: 'test',
links: {
certificates: 'certificatesTest',
course_outline: 'courseOutlineTest',
course_updates: 'welcomeMessageTest',
grading_policy: 'gradingPolicyTest',
settings: 'settingsTest',
studioDetails: {
course: courseDetails,
enable_quality: true,
links: {
certificates: 'certificatesTest',
course_outline: 'courseOutlineTest',
course_updates: 'welcomeMessageTest',
grading_policy: 'gradingPolicyTest',
settings: 'settingsTest',
},
},
};

Expand Down Expand Up @@ -368,10 +377,24 @@ describe('CourseChecklist', () => {
content === assignmentsWithDatesBeforeStart[0].display_name).toEqual(true);

const destination = assignmentLink.prop('destination');
expect(destination === `${defaultProps.links.course_outline}#${assignmentsWithDatesAfterEnd[0].id}` ||
destination === `${defaultProps.links.course_outline}#${assignmentsWithDatesBeforeStart[0].id}`).toEqual(true);
expect(destination === `${defaultProps.studioDetails.links.course_outline}#${assignmentsWithDatesAfterEnd[0].id}` ||
destination === `${defaultProps.studioDetails.links.course_outline}#${assignmentsWithDatesBeforeStart[0].id}`).toEqual(true);
});
});

it('calls trackEvent when an assignment Hyperlink is clicked', () => {
const comment = wrapper.find('#checklist-item-assignmentDeadlines').find('[data-identifier="comment"]');

const assignmentList = comment.find('ul');
const assignments = assignmentList.find('li');

const assignmentLink = assignments.find(Hyperlink).at(0);
const trackEventSpy = jest.fn();
global.analytics.track = trackEventSpy;

assignmentLink.simulate('click');
expect(trackEventSpy).toHaveBeenCalledTimes(1);
});
});
});
});
Expand All @@ -382,11 +405,16 @@ describe('CourseChecklist', () => {
dataHeading: <WrappedMessage message="" />,
dataList: [],
idPrefix: '',
links: {
course_updates: '',
grading_policy: '',
certificates: '',
settings: '',
studioDetails: {
course: courseDetails,
enable_quality: true,
links: {
certificates: '',
course_outline: '',
course_updates: '',
grading_policy: '',
settings: '',
},
},
};

Expand Down Expand Up @@ -425,5 +453,16 @@ describe('CourseChecklist', () => {
it('getCommentSection returns null for unknown checklist item id', () => {
expect(wrapper.instance().getCommentSection('test')).toBeNull();
});

it('calls trackEvent when an update link is clicked', () => {
wrapper = shallowWithIntl(<CourseChecklist {...defaultProps} />);

const updateLink = wrapper.find(Hyperlink).at(0);
const trackEventSpy = jest.fn();
global.analytics.track = trackEventSpy;

updateLink.simulate('click');
expect(trackEventSpy).toHaveBeenCalledTimes(1);
});
});
});
2 changes: 1 addition & 1 deletion src/components/CourseChecklist/container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import CourseChecklist from '.';

const mapStateToProps = state => ({
links: state.studioDetails.links,
studioDetails: state.studioDetails,
});

const mapDispatchToProps = () => ({});
Expand Down
60 changes: 54 additions & 6 deletions src/components/CourseChecklist/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Hyperlink, Icon } from '@edx/paragon';
import PropTypes from 'prop-types';
import React from 'react';

import { trackEvent } from '../../utils/analytics';
import getFilteredChecklist from '../../utils/CourseChecklist/getFilteredChecklist';
import getValidatedValue from '../../utils/CourseChecklist/getValidatedValue';
import messages from './displayMessages';
Expand All @@ -21,6 +22,9 @@ class CourseChecklist extends React.Component {
totalCompletedChecks: 0,
values: {},
};

this.onAssignmentHyperlinkClick = this.onAssignmentHyperlinkClick.bind(this);
this.onCheckUpdateHyperlinkClick = this.onCheckUpdateHyperlinkClick.bind(this);
}

componentWillMount() {
Expand All @@ -31,6 +35,26 @@ class CourseChecklist extends React.Component {
this.updateChecklistState(nextProps);
}

onAssignmentHyperlinkClick = (assignmentID) => {
trackEvent(
'edx.bi.studio.course.checklist.invalid-assignment.clicked', {
category: 'click',
event_type: `invalid-assignment-${assignmentID}`,
label: this.props.studioDetails.course.id,
},
);
}

onCheckUpdateHyperlinkClick = (checkID) => {
trackEvent(
'edx.bi.studio.course.checklist.update.clicked', {
category: 'click',
event_type: `update-${checkID}`,
label: this.props.studioDetails.course.id,
},
);
}

getCompletionCountID = () => (`${this.props.idPrefix.split(/\s/).join('-')}-completion-count`);

getHeading = () => (
Expand Down Expand Up @@ -105,10 +129,10 @@ class CourseChecklist extends React.Component {

getUpdateLinkDestination = (checkID) => {
switch (checkID) {
case 'welcomeMessage': return this.props.links.course_updates;
case 'gradingPolicy': return this.props.links.grading_policy;
case 'certificate': return this.props.links.certificates;
case 'courseDates': return `${this.props.links.settings}#schedule`;
case 'welcomeMessage': return this.props.studioDetails.links.course_updates;
case 'gradingPolicy': return this.props.studioDetails.links.grading_policy;
case 'certificate': return this.props.studioDetails.links.certificates;
case 'courseDates': return `${this.props.studioDetails.links.settings}#schedule`;
default: return null;
}
}
Expand All @@ -119,6 +143,7 @@ class CourseChecklist extends React.Component {
className={classNames('px-3', styles.btn, styles['btn-primary'], styles['checklist-item-link'])}
content={<WrappedMessage message={messages.updateLinkLabel} />}
destination={this.getUpdateLinkDestination(checkID)}
onClick={() => this.onCheckUpdateHyperlinkClick(checkID)}
/>
</div>
);
Expand Down Expand Up @@ -197,7 +222,11 @@ class CourseChecklist extends React.Component {
{
gradedAssignmentsOutsideDateRange.map(assignment => (
<li className={classNames(styles['assignment-list-item'], 'pr-2')} key={assignment.id}>
<Hyperlink content={assignment.display_name} destination={`${this.props.links.course_outline}#${assignment.id}`} />
<Hyperlink
content={assignment.display_name}
destination={`${this.props.studioDetails.links.course_outline}#${assignment.id}`}
onClick={() => this.onAssignmentHyperlinkClick(assignment.id)}
/>
</li>
))
}
Expand Down Expand Up @@ -367,5 +396,24 @@ CourseChecklist.propTypes = {
// eslint-disable-next-line react/no-unused-prop-types
dataList: PropTypes.arrayOf(PropTypes.object).isRequired,
idPrefix: PropTypes.string.isRequired,
links: PropTypes.objectOf(PropTypes.string).isRequired,
studioDetails: PropTypes.shape({
course: PropTypes.shape({
base_url: PropTypes.string,
course_release_date: PropTypes.string,
display_course_number: PropTypes.string,
enable_quality: PropTypes.bool,
id: PropTypes.string,
is_course_self_paced: PropTypes.boolean,
lang: PropTypes.string,
name: PropTypes.string,
num: PropTypes.string,
org: PropTypes.string,
revision: PropTypes.string,
url_name: PropTypes.string,
}),
enable_quality: PropTypes.boolean,
help_tokens: PropTypes.objectOf(PropTypes.string),
lang: PropTypes.string,
links: PropTypes.objectOf(PropTypes.string),
}).isRequired,
};
61 changes: 48 additions & 13 deletions src/components/CourseOutlineStatus/CourseOutlineStatus.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { Hyperlink } from '@edx/paragon';
import { IntlProvider, FormattedMessage } from 'react-intl';
import React from 'react';
import CourseOutlineStatus from './';

import CourseOutlineStatus from './';
import { courseDetails } from '../../utils/testConstants';
import getFilteredChecklist from '../../utils/CourseChecklist/getFilteredChecklist';
import getValidatedValue from '../../utils/CourseChecklist/getValidatedValue';
import { launchChecklist, bestPracticesChecklist } from '../../utils/CourseChecklist/courseChecklistData';
import { shallowWithIntl } from '../../utils/i18n/enzymeHelper';

// generating test checklist to avoid relying on actual data
const testChecklistData = ['a', 'b', 'c', 'd'].reduce(((accumulator, currentValue) => { accumulator.push({ id: currentValue, shortDescription: currentValue, longDescription: currentValue }); return accumulator; }), []);
const testChecklistData = ['a', 'b', 'c', 'd'].reduce(((accumulator, currentValue) => {
accumulator.push({
id: currentValue,
shortDescription: currentValue,
longDescription: currentValue,
});
return accumulator;
}), []);
const testChecklist = {
heading: 'test',
data: testChecklistData,
Expand Down Expand Up @@ -57,15 +66,22 @@ getFilteredChecklist.mockImplementation(
dataList => (dataList),
);

const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
const { intl } = intlProvider.getChildContext();

global.analytics = {
track: () => {},
};

let wrapper;

const defaultProps = {
studioDetails: { course: courseDetails, enable_quality: true },
getCourseBestPractices: () => { },
getCourseLaunch: () => { },
courseBestPracticesData: {},
courseLaunchData: {},
enable_quality: true,
getCourseBestPractices: () => { },
getCourseLaunch: () => { },
studioDetails: { course: courseDetails, enable_quality: true },
};

describe('CourseOutlineStatus', () => {
Expand All @@ -78,12 +94,12 @@ describe('CourseOutlineStatus', () => {
expect(header.text()).toEqual('Checklists');
});

it('an anchor with correct href', () => {
it('a Hyperlink with correct href', () => {
wrapper = shallowWithIntl(<CourseOutlineStatus {...defaultProps} />);

const anchor = wrapper.find('a');
expect(anchor).toHaveLength(1);
expect(anchor.prop('href')).toEqual(`/checklists/${defaultProps.studioDetails.course.id}`);
const checklistsLink = wrapper.find(Hyperlink);
expect(checklistsLink).toHaveLength(1);
expect(checklistsLink.prop('destination')).toEqual(`/checklists/${defaultProps.studioDetails.course.id}`);
});

describe('if enable_quality prop is true', () => {
Expand All @@ -99,8 +115,12 @@ describe('CourseOutlineStatus', () => {
courseLaunchData: testChecklist,
});

const anchor = wrapper.find('a');
expect(anchor.text()).toEqual(`${completed}/${total} complete`);
const checklistsLink = wrapper.find(Hyperlink);
const checklistsLinkContent = shallowWithIntl(checklistsLink.prop('content'), { context: { intl } });
const completionCount = checklistsLinkContent.dive({ context: { intl } })
.find(FormattedMessage).dive({ context: { intl } });

expect(completionCount.text()).toEqual(`${completed}/${total} completed`);
});
});

Expand All @@ -127,8 +147,12 @@ describe('CourseOutlineStatus', () => {
courseLaunchData: testChecklist,
});

const anchor = wrapper.find('a');
expect(anchor.text()).toEqual(`${completed}/${total} complete`);
const checklistsLink = wrapper.find(Hyperlink);
const checklistsLinkContent = shallowWithIntl(checklistsLink.prop('content'), { context: { intl } });
const completionCount = checklistsLinkContent.dive({ context: { intl } })
.find(FormattedMessage).dive({ context: { intl } });

expect(completionCount.text()).toEqual(`${completed}/${total} completed`);
});
});
});
Expand Down Expand Up @@ -167,5 +191,16 @@ describe('CourseOutlineStatus', () => {
defaultProps.studioDetails.course,
);
});

it('calls trackEvent when checklist link is clicked', () => {
wrapper = shallowWithIntl(<CourseOutlineStatus {...defaultProps} />);

const completionLink = wrapper.find(Hyperlink);
const trackEventSpy = jest.fn();
global.analytics.track = trackEventSpy;

completionLink.simulate('click');
expect(trackEventSpy).toHaveBeenCalledTimes(1);
});
});
});
11 changes: 11 additions & 0 deletions src/components/CourseOutlineStatus/displayMessages.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';

const messages = defineMessages({
completionCountLabel: {
id: 'completionCountLabel',
defaultMessage: '{completed}/{total} completed',
description: 'Label that describes how many tasks have been completed out of a total number of tasks',
},
});

export default messages;
Loading

0 comments on commit 92c10a2

Please sign in to comment.