diff --git a/src/components/CourseChecklist/CourseChecklist.test.jsx b/src/components/CourseChecklist/CourseChecklist.test.jsx
index 30ffd119..9eaeb41d 100644
--- a/src/components/CourseChecklist/CourseChecklist.test.jsx
+++ b/src/components/CourseChecklist/CourseChecklist.test.jsx
@@ -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';
@@ -43,6 +44,10 @@ getFilteredChecklist.mockImplementation(
const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
const { intl } = intlProvider.getChildContext();
+global.analytics = {
+ track: () => {},
+};
+
let wrapper;
const testData = {
@@ -61,12 +66,16 @@ const defaultProps = {
dataHeading: ,
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',
+ },
},
};
@@ -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);
+ });
});
});
});
@@ -382,11 +405,16 @@ describe('CourseChecklist', () => {
dataHeading: ,
dataList: [],
idPrefix: '',
- links: {
- course_updates: '',
- grading_policy: '',
- certificates: '',
- settings: '',
+ studioDetails: {
+ course: courseDetails,
+ enable_quality: true,
+ links: {
+ certificates: '',
+ course_outline: '',
+ course_updates: '',
+ grading_policy: '',
+ settings: '',
+ },
},
};
@@ -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();
+
+ const updateLink = wrapper.find(Hyperlink).at(0);
+ const trackEventSpy = jest.fn();
+ global.analytics.track = trackEventSpy;
+
+ updateLink.simulate('click');
+ expect(trackEventSpy).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/src/components/CourseChecklist/container.jsx b/src/components/CourseChecklist/container.jsx
index f243f7ee..181d7ff0 100644
--- a/src/components/CourseChecklist/container.jsx
+++ b/src/components/CourseChecklist/container.jsx
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import CourseChecklist from '.';
const mapStateToProps = state => ({
- links: state.studioDetails.links,
+ studioDetails: state.studioDetails,
});
const mapDispatchToProps = () => ({});
diff --git a/src/components/CourseChecklist/index.jsx b/src/components/CourseChecklist/index.jsx
index 9277e01b..29770e35 100644
--- a/src/components/CourseChecklist/index.jsx
+++ b/src/components/CourseChecklist/index.jsx
@@ -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';
@@ -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() {
@@ -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 = () => (
@@ -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;
}
}
@@ -119,6 +143,7 @@ class CourseChecklist extends React.Component {
className={classNames('px-3', styles.btn, styles['btn-primary'], styles['checklist-item-link'])}
content={}
destination={this.getUpdateLinkDestination(checkID)}
+ onClick={() => this.onCheckUpdateHyperlinkClick(checkID)}
/>
);
@@ -197,7 +222,11 @@ class CourseChecklist extends React.Component {
{
gradedAssignmentsOutsideDateRange.map(assignment => (
-
+ this.onAssignmentHyperlinkClick(assignment.id)}
+ />
))
}
@@ -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,
};
diff --git a/src/components/CourseOutlineStatus/CourseOutlineStatus.test.jsx b/src/components/CourseOutlineStatus/CourseOutlineStatus.test.jsx
index 3874b24e..a00accfc 100644
--- a/src/components/CourseOutlineStatus/CourseOutlineStatus.test.jsx
+++ b/src/components/CourseOutlineStatus/CourseOutlineStatus.test.jsx
@@ -1,6 +1,8 @@
+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';
@@ -8,7 +10,14 @@ import { launchChecklist, bestPracticesChecklist } from '../../utils/CourseCheck
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,
@@ -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', () => {
@@ -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();
- 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', () => {
@@ -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`);
});
});
@@ -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`);
});
});
});
@@ -167,5 +191,16 @@ describe('CourseOutlineStatus', () => {
defaultProps.studioDetails.course,
);
});
+
+ it('calls trackEvent when checklist link is clicked', () => {
+ wrapper = shallowWithIntl();
+
+ const completionLink = wrapper.find(Hyperlink);
+ const trackEventSpy = jest.fn();
+ global.analytics.track = trackEventSpy;
+
+ completionLink.simulate('click');
+ expect(trackEventSpy).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/src/components/CourseOutlineStatus/displayMessages.jsx b/src/components/CourseOutlineStatus/displayMessages.jsx
new file mode 100644
index 00000000..7e81d06e
--- /dev/null
+++ b/src/components/CourseOutlineStatus/displayMessages.jsx
@@ -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;
diff --git a/src/components/CourseOutlineStatus/index.jsx b/src/components/CourseOutlineStatus/index.jsx
index da13cf89..364cb346 100644
--- a/src/components/CourseOutlineStatus/index.jsx
+++ b/src/components/CourseOutlineStatus/index.jsx
@@ -1,11 +1,15 @@
import classNames from 'classnames';
-import React from 'react';
+import { Hyperlink } 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 { launchChecklist, bestPracticesChecklist } from '../../utils/CourseChecklist/courseChecklistData';
+import messages from './displayMessages';
import styles from './CourseOutlineStatus.scss';
+import WrappedMessage from '../../utils/i18n/formattedMessageWrapper';
export default class CourseOutlineStatus extends React.Component {
constructor(props) {
@@ -90,7 +94,7 @@ export default class CourseOutlineStatus extends React.Component {
totalCourseLaunchChecks,
} = this.state;
- const totalCompleteChecks = this.props.studioDetails.enable_quality ?
+ const totalCompletedChecks = this.props.studioDetails.enable_quality ?
completedCourseBestPracticesChecks + completedCourseLaunchChecks :
completedCourseLaunchChecks;
@@ -101,9 +105,33 @@ export default class CourseOutlineStatus extends React.Component {
return (
+
Checklists
+
+ {displayText =>
+ (
+ {displayText}
+ )
+ }
+
+ }
+ destination={`/checklists/${this.props.studioDetails.course.id}`}
+ onClick={() => trackEvent(
+ 'edx.bi.studio.course.checklist.accessed', {
+ category: 'click',
+ event_type: 'outline-access',
+ label: this.props.studioDetails.course.id,
+ },
+ )}
+ />
+
);
diff --git a/src/utils/analytics.js b/src/utils/analytics.js
new file mode 100644
index 00000000..62cb1524
--- /dev/null
+++ b/src/utils/analytics.js
@@ -0,0 +1,7 @@
+// eslint-disable-next-line import/prefer-default-export
+export const trackEvent = (key, value) => {
+ // TODO: Ticket EDUCATOR-3192
+ // Add window.analytics.identify(user_id)
+ // that uses LMS auth user_id
+ window.analytics.track(key, value);
+};
diff --git a/src/utils/analytics.test.jsx b/src/utils/analytics.test.jsx
new file mode 100644
index 00000000..8a1ab12b
--- /dev/null
+++ b/src/utils/analytics.test.jsx
@@ -0,0 +1,30 @@
+import { trackEvent } from './analytics';
+
+const key = 'testKey';
+const category = 'testCategory';
+const courseId = 'testId';
+const eventType = 'testEventType';
+global.analytics = {
+ track: () => {},
+};
+
+describe('Analytics Utility Functions', () => {
+ describe('trackEvent', () => {
+ it('calls the global track function', () => {
+ const analyticsSpy = jest.fn();
+ global.analytics.track = analyticsSpy;
+ trackEvent(key, {
+ category,
+ event_type: eventType,
+ label: courseId,
+ });
+
+ expect(analyticsSpy).toHaveBeenCalledTimes(1);
+ expect(analyticsSpy).toHaveBeenCalledWith(key, {
+ category,
+ label: courseId,
+ event_type: eventType,
+ });
+ });
+ });
+});