diff --git a/src/components/join-flow/birthdate-step.jsx b/src/components/join-flow/birthdate-step.jsx index 4773d6d9e..6b6a7209f 100644 --- a/src/components/join-flow/birthdate-step.jsx +++ b/src/components/join-flow/birthdate-step.jsx @@ -54,6 +54,12 @@ class BirthDateStep extends React.Component { 'validateSelect' ]); } + componentDidMount () { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-birthdate'); + } + } + validateSelect (selection) { if (selection === 'null') { return this.props.intl.formatMessage({id: 'general.required'}); @@ -162,7 +168,8 @@ class BirthDateStep extends React.Component { BirthDateStep.propTypes = { intl: intlShape, - onNextStep: PropTypes.func + onNextStep: PropTypes.func, + sendAnalytics: PropTypes.func.isRequired }; const IntlBirthDateStep = injectIntl(BirthDateStep); diff --git a/src/components/join-flow/country-step.jsx b/src/components/join-flow/country-step.jsx index 2164a99cd..625717c0e 100644 --- a/src/components/join-flow/country-step.jsx +++ b/src/components/join-flow/country-step.jsx @@ -23,6 +23,9 @@ class CountryStep extends React.Component { this.countryOptions = []; } componentDidMount () { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-country'); + } this.setCountryOptions(); } setCountryOptions () { @@ -120,7 +123,8 @@ class CountryStep extends React.Component { CountryStep.propTypes = { intl: intlShape, - onNextStep: PropTypes.func + onNextStep: PropTypes.func, + sendAnalytics: PropTypes.func.isRequired }; const IntlCountryStep = injectIntl(CountryStep); diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index ed13f9808..6bcca7305 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -35,6 +35,9 @@ class EmailStep extends React.Component { this.emailRemoteCache = {}; } componentDidMount () { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-email'); + } // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); @@ -231,6 +234,7 @@ EmailStep.propTypes = { intl: intlShape, onCaptchaError: PropTypes.func, onNextStep: PropTypes.func, + sendAnalytics: PropTypes.func.isRequired, waiting: PropTypes.bool }; diff --git a/src/components/join-flow/gender-step.jsx b/src/components/join-flow/gender-step.jsx index fa450eb44..30089899d 100644 --- a/src/components/join-flow/gender-step.jsx +++ b/src/components/join-flow/gender-step.jsx @@ -61,6 +61,11 @@ class GenderStep extends React.Component { 'handleValidSubmit' ]); } + componentDidMount () { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-gender'); + } + } handleSetCustomRef (customInputRef) { this.customInput = customInputRef; } @@ -180,7 +185,8 @@ class GenderStep extends React.Component { GenderStep.propTypes = { intl: intlShape, - onNextStep: PropTypes.func + onNextStep: PropTypes.func, + sendAnalytics: PropTypes.func.isRequired }; module.exports = injectIntl(GenderStep); diff --git a/src/components/join-flow/join-flow.jsx b/src/components/join-flow/join-flow.jsx index 876bab9b6..fd6760da6 100644 --- a/src/components/join-flow/join-flow.jsx +++ b/src/components/join-flow/join-flow.jsx @@ -215,6 +215,18 @@ class JoinFlow extends React.Component { resetState () { this.setState(this.initialState); } + sendAnalytics (path) { + const gaID = window.GA_ID; + if (!window.ga) { + return; + } + window.ga('send', { + hitType: 'pageview', + page: path, + tid: gaID + }); + } + render () { return ( @@ -222,17 +234,33 @@ class JoinFlow extends React.Component { ) : ( - - - - + + + + + + diff --git a/src/components/join-flow/registration-error-step.jsx b/src/components/join-flow/registration-error-step.jsx index 74c9318f3..a7151840d 100644 --- a/src/components/join-flow/registration-error-step.jsx +++ b/src/components/join-flow/registration-error-step.jsx @@ -15,6 +15,11 @@ class RegistrationErrorStep extends React.Component { 'handleSubmit' ]); } + componentDidMount () { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-error'); + } + } handleSubmit (e) { // JoinFlowStep includes a
that handles a submit action. // But here, we're not really submitting, so we need to prevent @@ -60,7 +65,8 @@ RegistrationErrorStep.propTypes = { canTryAgain: PropTypes.bool.isRequired, errorMsg: PropTypes.string, intl: intlShape, - onSubmit: PropTypes.func.isRequired + onSubmit: PropTypes.func.isRequired, + sendAnalytics: PropTypes.func.isRequired }; RegistrationErrorStep.defaultProps = { diff --git a/src/components/join-flow/username-step.jsx b/src/components/join-flow/username-step.jsx index aef6d5617..ed6c23c13 100644 --- a/src/components/join-flow/username-step.jsx +++ b/src/components/join-flow/username-step.jsx @@ -37,6 +37,14 @@ class UsernameStep extends React.Component { this.usernameRemoteCache = {}; } componentDidMount () { + // Send info to analytics when we aren't on the standalone page. + // If we are on the standalone join page, the page load will take care of this. + if (!window.location.pathname.includes('/join')) { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-username-modal'); + } + } + // automatically start with focus on username field if (this.usernameInput) this.usernameInput.focus(); } @@ -282,7 +290,8 @@ class UsernameStep extends React.Component { UsernameStep.propTypes = { intl: intlShape, - onNextStep: PropTypes.func + onNextStep: PropTypes.func, + sendAnalytics: PropTypes.func.isRequired }; const IntlUsernameStep = injectIntl(UsernameStep); diff --git a/src/components/join-flow/welcome-step.jsx b/src/components/join-flow/welcome-step.jsx index f4694a1c2..7610a0fad 100644 --- a/src/components/join-flow/welcome-step.jsx +++ b/src/components/join-flow/welcome-step.jsx @@ -17,6 +17,12 @@ class WelcomeStep extends React.Component { 'validateForm' ]); } + componentDidMount () { + if (this.props.sendAnalytics) { + this.props.sendAnalytics('join-welcome'); + } + } + validateForm () { return {}; } @@ -87,6 +93,7 @@ WelcomeStep.propTypes = { email: PropTypes.string, intl: intlShape, onNextStep: PropTypes.func, + sendAnalytics: PropTypes.func, username: PropTypes.string }; diff --git a/test/unit/components/email-step.test.jsx b/test/unit/components/email-step.test.jsx index 65dc7ca91..3f2c866d0 100644 --- a/test/unit/components/email-step.test.jsx +++ b/test/unit/components/email-step.test.jsx @@ -1,5 +1,6 @@ const React = require('react'); const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); +const {mountWithIntl} = require('../../helpers/intl-helpers.jsx'); const JoinFlowStep = require('../../../src/components/join-flow/join-flow-step.jsx'); const FormikInput = require('../../../src/components/formik-forms/formik-input.jsx'); const FormikCheckbox = require('../../../src/components/formik-forms/formik-checkbox.jsx'); @@ -36,13 +37,17 @@ jest.mock('../../../src/lib/validate.js', () => ( const EmailStep = require('../../../src/components/join-flow/email-step.jsx'); describe('EmailStep test', () => { + const defaultProps = () => ({ + sendAnalytics: jest.fn() + }); afterEach(() => { jest.clearAllMocks(); }); test('send correct props to formik', () => { - const intlWrapper = shallowWithIntl(); - + const intlWrapper = shallowWithIntl(); const emailStepWrapper = intlWrapper.dive(); expect(emailStepWrapper.props().initialValues.subscribe).toBe(false); expect(emailStepWrapper.props().initialValues.email).toBe(''); @@ -53,7 +58,9 @@ describe('EmailStep test', () => { }); test('props sent to JoinFlowStep', () => { - const intlWrapper = shallowWithIntl(); + const intlWrapper = shallowWithIntl(); // Dive to get past the intl wrapper const emailStepWrapper = intlWrapper.dive(); // Dive to get past the anonymous component. @@ -69,7 +76,9 @@ describe('EmailStep test', () => { }); test('props sent to FormikInput for email', () => { - const intlWrapper = shallowWithIntl(); + const intlWrapper = shallowWithIntl(); // Dive to get past the intl wrapper const emailStepWrapper = intlWrapper.dive(); // Dive to get past the anonymous component. @@ -86,7 +95,10 @@ describe('EmailStep test', () => { }); test('props sent to FormikCheckbox for subscribe', () => { - const intlWrapper = shallowWithIntl(); + const intlWrapper = shallowWithIntl(); + // Dive to get past the intl wrapper const emailStepWrapper = intlWrapper.dive(); // Dive to get past the anonymous component. @@ -109,7 +121,10 @@ describe('EmailStep test', () => { }; const formData = {item1: 'thing', item2: 'otherthing'}; const intlWrapper = shallowWithIntl( - ); + ); + const emailStepWrapper = intlWrapper.dive(); emailStepWrapper.instance().onCaptchaLoad(); // to setup catpcha state @@ -133,6 +148,7 @@ describe('EmailStep test', () => { const formData = {item1: 'thing', item2: 'otherthing'}; const intlWrapper = shallowWithIntl( ); @@ -162,6 +178,7 @@ describe('EmailStep test', () => { global.grecaptcha = null; const intlWrapper = shallowWithIntl( ); @@ -171,9 +188,20 @@ describe('EmailStep test', () => { expect(props.onCaptchaError).toHaveBeenCalled(); }); + test('Component logs analytics', () => { + const sendAnalyticsFn = jest.fn(); + mountWithIntl( + ); + expect(sendAnalyticsFn).toHaveBeenCalledWith('join-email'); + }); + test('validateEmail test email empty', () => { const intlWrapper = shallowWithIntl( - ); + ); const emailStepWrapper = intlWrapper.dive(); const val = emailStepWrapper.instance().validateEmail(''); expect(val).toBe('general.required'); @@ -181,7 +209,9 @@ describe('EmailStep test', () => { test('validateEmail test email null', () => { const intlWrapper = shallowWithIntl( - ); + ); const emailStepWrapper = intlWrapper.dive(); const val = emailStepWrapper.instance().validateEmail(null); expect(val).toBe('general.required'); @@ -189,7 +219,9 @@ describe('EmailStep test', () => { test('validateEmail test email undefined', () => { const intlWrapper = shallowWithIntl( - ); + ); const emailStepWrapper = intlWrapper.dive(); const val = emailStepWrapper.instance().validateEmail(); expect(val).toBe('general.required'); @@ -204,7 +236,8 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => { test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => { const intlWrapper = shallowWithIntl( - ); + + ); const instance = intlWrapper.dive().instance(); instance.validateEmailRemotelyWithCache('some-email@some-domain.com') diff --git a/test/unit/components/join-flow.test.jsx b/test/unit/components/join-flow.test.jsx index 66505d1b8..91d4b188d 100644 --- a/test/unit/components/join-flow.test.jsx +++ b/test/unit/components/join-flow.test.jsx @@ -67,6 +67,19 @@ describe('JoinFlow', () => { }); }); + test('sendAnalytics calls google analytics with correct params', () => { + const joinFlowInstance = getJoinFlowWrapper().instance(); + global.window.ga = jest.fn(); + global.window.GA_ID = '1234'; + joinFlowInstance.sendAnalytics('page-path'); + const obj = { + hitType: 'pageview', + page: 'page-path', + tid: '1234' + }; + expect(global.window.ga).toHaveBeenCalledWith('send', obj); + }); + test('handleAdvanceStep', () => { const joinFlowInstance = getJoinFlowWrapper().instance(); joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2}); diff --git a/test/unit/components/registration-error-step.test.jsx b/test/unit/components/registration-error-step.test.jsx index 97876db95..42848bf1b 100644 --- a/test/unit/components/registration-error-step.test.jsx +++ b/test/unit/components/registration-error-step.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import {shallowWithIntl} from '../../helpers/intl-helpers.jsx'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; import JoinFlowStep from '../../../src/components/join-flow/join-flow-step'; import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step'; @@ -9,6 +10,7 @@ describe('RegistrationErrorStep', () => { const getRegistrationErrorStepWrapper = props => { const wrapper = shallowWithIntl( ); @@ -61,6 +63,14 @@ describe('RegistrationErrorStep', () => { expect(errMsgElement).toHaveLength(0); }); + test('logs to analytics', () => { + const analyticsFn = jest.fn(); + mountWithIntl( + ); + expect(analyticsFn).toHaveBeenCalledWith('join-error'); + }); test('when canTryAgain is true, show tryAgain message', () => { const props = { canTryAgain: true, diff --git a/test/unit/components/username-step.test.jsx b/test/unit/components/username-step.test.jsx index f08b8b682..873b015de 100644 --- a/test/unit/components/username-step.test.jsx +++ b/test/unit/components/username-step.test.jsx @@ -1,5 +1,6 @@ const React = require('react'); const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); +const {mountWithIntl} = require('../../helpers/intl-helpers.jsx'); const requestSuccessResponse = { requestSucceeded: true, @@ -22,6 +23,7 @@ let mockedValidateUsernameRemotely = jest.fn(() => ( /* eslint-enable no-undef */ )); + jest.mock('../../../src/lib/validate.js', () => ( { ...(jest.requireActual('../../../src/lib/validate.js')), @@ -32,10 +34,19 @@ jest.mock('../../../src/lib/validate.js', () => ( // must come after validation mocks, so validate.js will be mocked before it is required const UsernameStep = require('../../../src/components/join-flow/username-step.jsx'); + describe('UsernameStep tests', () => { + const defaultProps = () => ({ + sendAnalytics: jest.fn() + }); + afterEach(() => { + jest.clearAllMocks(); + }); test('send correct props to formik', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const formikWrapper = wrapper.dive(); expect(formikWrapper.props().initialValues.username).toBe(''); expect(formikWrapper.props().initialValues.password).toBe(''); @@ -47,6 +58,28 @@ describe('UsernameStep tests', () => { expect(formikWrapper.props().onSubmit).toBe(formikWrapper.instance().handleValidSubmit); }); + test('Component does not log if path is /join', () => { + const sendAnalyticsFn = jest.fn(); + + global.window.history.pushState({}, '', '/join'); + mountWithIntl( + ); + expect(sendAnalyticsFn).not.toHaveBeenCalled(); + }); + + test('Component logs analytics', () => { + // Make sure '/join' is NOT in the path + global.window.history.pushState({}, '', '/'); + const sendAnalyticsFn = jest.fn(); + mountWithIntl( + ); + expect(sendAnalyticsFn).toHaveBeenCalledWith('join-username-modal'); + }); + test('handleValidSubmit passes formData to next step', () => { const formikBag = { setSubmitting: jest.fn() @@ -55,6 +88,7 @@ describe('UsernameStep tests', () => { const mockedOnNextStep = jest.fn(); const wrapper = shallowWithIntl( );