import * as counts from '../../../src/counts'; import { createModel } from '../../../src/preview/model'; import eventLogging from '../../../src/reducers/eventLogging'; import actionTypes from '../../../src/actionTypes'; QUnit.module( 'ext.popups/reducers#eventLogging', { beforeEach() { this.initialState = eventLogging( undefined, { type: '@@INIT' } ); } } ); QUnit.test( '@@INIT', function ( assert ) { assert.deepEqual( this.initialState, { previewCount: undefined, baseData: {}, event: undefined, interaction: undefined }, 'The initial state is correct.' ); } ); QUnit.test( 'BOOT', function ( assert ) { const action = { type: actionTypes.BOOT, isEnabled: true, isNavPopupsEnabled: false, sessionToken: '0123456789', pageToken: '9876543210', page: { title: 'Foo', namespaceId: 1, id: 2 }, user: { isAnon: false, editCount: 11, previewCount: 22 } }; const expectedEditCountBucket = counts.getEditCountBucket( action.user.editCount ); const expectedPreviewCountBucket = counts.getPreviewCountBucket( action.user.previewCount ); let state = eventLogging( this.initialState, action ); assert.deepEqual( state, { previewCount: action.user.previewCount, baseData: { pageTitleSource: action.page.title, namespaceIdSource: action.page.namespaceId, pageIdSource: action.page.id, isAnon: action.user.isAnon, popupEnabled: action.isEnabled, pageToken: action.pageToken, sessionToken: action.sessionToken, editCountBucket: expectedEditCountBucket, previewCountBucket: expectedPreviewCountBucket, hovercardsSuppressedByGadget: action.isNavPopupsEnabled }, event: { action: 'pageLoaded' }, interaction: undefined }, 'The boot state is correct.' ); // --- // And when the user is logged out... action.user.isAnon = true; state = eventLogging( this.initialState, action ); assert.strictEqual( state.baseData.isAnon, true, 'The user is anonymous and not logged in.' ); assert.strictEqual( state.baseData.editCountBucket, undefined, 'It shouldn\'t add the editCountBucket property when the user is logged out.' ); } ); QUnit.test( 'EVENT_LOGGED', ( assert ) => { let state = { event: {} }; let action = { type: actionTypes.EVENT_LOGGED, event: {} }; assert.deepEqual( eventLogging( state, action ), { event: undefined }, 'It dequeues any event queued for logging.' ); // --- state = { interaction: { token: 'asdf' }, event: { linkInteractionToken: 'asdf' } }; action = { type: actionTypes.EVENT_LOGGED, event: state.event }; assert.deepEqual( eventLogging( state, action ), { event: undefined, interaction: undefined }, 'It destroys current interaction if an event for it was logged.' ); } ); QUnit.test( 'PREVIEW_SHOW', ( assert ) => { const count = 22, expectedCount = count + 1, token = '1234567890'; let state = { previewCount: count, baseData: { previewCountBucket: counts.getPreviewCountBucket( count ) }, event: undefined, // state.interaction.started is used in this part of the reducer. interaction: { token } }; state = eventLogging( state, { type: actionTypes.PREVIEW_SHOW, token } ); assert.strictEqual( state.previewCount, expectedCount, 'It updates the user\'s preview count.' ); assert.deepEqual( state.baseData, { previewCountBucket: counts.getPreviewCountBucket( expectedCount ) }, 'It re-buckets the user\'s preview count.' ); } ); QUnit.module( 'ext.popups/reducers#eventLogging @integration', { beforeEach() { this.link = $( '' ); } } ); QUnit.test( 'LINK_DWELL starts an interaction', function ( assert ) { const state = { interaction: undefined }; const action = { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token: '0987654321', timestamp: Date.now() }; assert.deepEqual( eventLogging( state, action ), { interaction: { link: action.el, title: 'Foo', namespaceId: 1, token: action.token, started: action.timestamp, isUserDwelling: true }, event: undefined }, 'The link dwell state is correct.' ); } ); QUnit.test( 'LINK_DWELL doesn\'t start a new interaction under certain conditions', function ( assert ) { const now = Date.now(); let state = { interaction: undefined }; const action = { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token: '0987654321', timestamp: now }; state = eventLogging( state, action ); action.token = '1234567890'; action.timestamp = now + 200; state = eventLogging( state, action ); assert.deepEqual( state.interaction, { link: action.el, title: 'Foo', namespaceId: 1, token: '0987654321', started: now, isUserDwelling: true }, 'The link dwell state is correct.' ); } ); QUnit.test( 'LINK_DWELL should enqueue a "dismissed" or "dwelledButAbandoned" event under certain conditions', function ( assert ) { const token = '0987654321', now = Date.now(); // Read: The user dwells on link A, abandons it, and dwells on link B fewer // than 300 ms after (before the ABANDON_END action is reduced). let state = eventLogging( undefined, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); state = eventLogging( state, { type: actionTypes.ABANDON_START, timestamp: now + 250 } ); state = eventLogging( state, { type: actionTypes.LINK_DWELL, el: $( '' ), title: 'Bar', namespaceId: 1, token: '1234567890', timestamp: now + 500 } ); assert.deepEqual( state.event, { pageTitleHover: 'Foo', namespaceIdHover: 1, linkInteractionToken: '0987654321', totalInteractionTime: 250, // 250 - 0 action: 'dwelledButAbandoned' }, 'The link dwell state is correct.' ); // --- state = eventLogging( undefined, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); state = eventLogging( state, { type: actionTypes.LINK_CLICK, el: this.link } ); state = eventLogging( state, { type: actionTypes.LINK_DWELL, el: $( '' ), title: 'Bar', namespaceId: 1, token: 'banana', timestamp: now + 500 } ); assert.strictEqual( state.event, undefined, 'It shouldn\'t enqueue either event if the interaction is finalized.' ); } ); QUnit.test( 'LINK_CLICK should enqueue an "opened" event', function ( assert ) { const token = '0987654321', now = Date.now(); let state = { interaction: undefined }; const expectedState = state = eventLogging( state, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); state = eventLogging( state, { type: actionTypes.LINK_CLICK, el: this.link, timestamp: now + 250 } ); assert.deepEqual( state.event, { action: 'opened', pageTitleHover: 'Foo', namespaceIdHover: 1, linkInteractionToken: token, totalInteractionTime: 250 }, 'The event is enqueued and the totalInteractionTime property is an integer.' ); expectedState.interaction.finalized = true; assert.deepEqual( state.interaction, expectedState.interaction, 'It should finalize the interaction.' ); } ); QUnit.test( 'PREVIEW_SHOW should update the perceived wait time of the interaction', function ( assert ) { const now = Date.now(), token = '1234567890'; let state = { interaction: undefined }; state = eventLogging( state, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); state = eventLogging( state, { type: actionTypes.PREVIEW_SHOW, token, timestamp: now + 500 } ); assert.deepEqual( state.interaction, { link: this.link, title: 'Foo', namespaceId: 1, token, started: now, isUserDwelling: true, timeToPreviewShow: 500 }, 'The preview show state is correct.' ); } ); QUnit.test( 'LINK_CLICK should include perceivedWait if the preview has been shown', function ( assert ) { const token = '0987654321', now = Date.now(); let state = { interaction: undefined }; state = eventLogging( state, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); state = eventLogging( state, { type: actionTypes.PREVIEW_SHOW, token, timestamp: now + 750 } ); state = eventLogging( state, { type: actionTypes.LINK_CLICK, el: this.link, timestamp: now + 1050 } ); assert.deepEqual( state.event, { action: 'opened', pageTitleHover: 'Foo', namespaceIdHover: 1, linkInteractionToken: token, totalInteractionTime: 1050, // N.B. that the FETCH_* actions have been skipped. previewType: undefined, perceivedWait: 750 }, 'The previewType and perceivedWait properties are set if the preview has been shown.' ); } ); QUnit.test( 'FETCH_COMPLETE', ( assert ) => { const token = '1234567890', initialState = { interaction: { token } }, model = createModel( 'Foo', 'https://en.wikipedia.org/wiki/Foo', 'en', 'ltr', '', {} ); let state = eventLogging( initialState, { type: actionTypes.FETCH_COMPLETE, result: model, token } ); assert.strictEqual( state.interaction.previewType, model.type, 'It mixes in the preview type to the interaction state.' ); // --- state = eventLogging( initialState, { type: actionTypes.FETCH_COMPLETE, result: model, token: 'banana' } ); assert.strictEqual( initialState, state, 'It should NOOP if there\'s a new interaction.' ); // --- delete initialState.interaction; state = eventLogging( initialState, { type: actionTypes.FETCH_COMPLETE, result: model, token: '0123456789' } ); assert.strictEqual( initialState, state, 'It should NOOP if the interaction has been finalised.' ); } ); QUnit.test( 'ABANDON_START', function ( assert ) { let state = { interaction: {} }; state = eventLogging( state, { type: actionTypes.ABANDON_START, timestamp: Date.now() } ); assert.notOk( state.interaction.isUserDwelling, 'It should mark the link or preview as having been abandoned.' ); } ); QUnit.test( 'ABANDON_END', function ( assert ) { let state = { interaction: {} }; let action = { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token: '1234567890', timestamp: Date.now() }; state = eventLogging( state, action ); action = { type: actionTypes.ABANDON_END, token: '1234567890' }; assert.deepEqual( eventLogging( state, action ), state, 'ABANDON_END should NOOP if the user is dwelling on the preview or the link.' ); // --- action.token = '0987654321'; assert.deepEqual( eventLogging( state, action ), state, 'ABANDON_END should NOOP if the current interaction has changed.' ); } ); QUnit.test( 'PREVIEW_DWELL', ( assert ) => { let state = { interaction: {} }; state = eventLogging( state, { type: actionTypes.PREVIEW_DWELL } ); assert.ok( state.interaction.isUserDwelling, 'It should mark the link or preview as being dwelled on.' ); } ); QUnit.test( 'SETTINGS_SHOW should enqueue a "tapped settings cog" event', function ( assert ) { const initialState = { interaction: {} }, token = '0123456789'; let state = eventLogging( initialState, { type: actionTypes.SETTINGS_SHOW } ); // Note well that this is a valid event. The "tapped settings cog" event is // also logged as a result of clicking the footer link. assert.deepEqual( state.event, { action: 'tapped settings cog', linkInteractionToken: undefined, namespaceIdHover: undefined, pageTitleHover: undefined }, 'It shouldn\'t fail if there\'s no interaction.' ); // --- state = eventLogging( initialState, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: Date.now() } ); state = eventLogging( state, { type: actionTypes.SETTINGS_SHOW } ); assert.deepEqual( state.event, { action: 'tapped settings cog', linkInteractionToken: token, namespaceIdHover: 1, pageTitleHover: 'Foo' }, 'It should include the interaction information if there\'s an interaction.' ); } ); QUnit.test( 'SETTINGS_CHANGE should enqueue disabled event', ( assert ) => { let state = eventLogging( undefined, { type: actionTypes.SETTINGS_CHANGE, wasEnabled: false, enabled: false } ); assert.strictEqual( state.event, undefined, 'It shouldn\'t enqueue a "disabled" event when there is no change' ); state = eventLogging( state, { type: actionTypes.SETTINGS_CHANGE, wasEnabled: true, enabled: false } ); assert.deepEqual( state.event, { action: 'disabled', popupEnabled: false }, 'It should enqueue a "disabled" event when the previews has been disabled' ); delete state.event; state = eventLogging( state, { type: actionTypes.SETTINGS_CHANGE, wasEnabled: false, enabled: true } ); assert.strictEqual( state.event, undefined, 'It shouldn\'t enqueue a "disabled" event when page previews has been enabled' ); } ); QUnit.test( 'ABANDON_END should enqueue an event', function ( assert ) { const token = '0987654321', now = Date.now(); const dwelledState = eventLogging( undefined, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); let state = eventLogging( dwelledState, { type: actionTypes.ABANDON_START, token, timestamp: now + 500 } ); state = eventLogging( state, { type: actionTypes.ABANDON_END, token } ); assert.deepEqual( state.event, { pageTitleHover: 'Foo', namespaceIdHover: 1, linkInteractionToken: token, totalInteractionTime: 500, action: 'dwelledButAbandoned' }, 'It should enqueue a "dwelledButAbandoned" event when the preview hasn\'t been shown.' ); assert.strictEqual( state.interaction, undefined, 'It should close the interaction.' ); // --- state = eventLogging( dwelledState, { type: actionTypes.PREVIEW_SHOW, token, timestamp: now + 700 } ); state = eventLogging( state, { type: actionTypes.ABANDON_START, token, timestamp: now + 850 } ); state = eventLogging( state, { type: actionTypes.ABANDON_END, token } ); assert.deepEqual( state.event, { pageTitleHover: 'Foo', namespaceIdHover: 1, linkInteractionToken: token, totalInteractionTime: 850, action: 'dismissed', // N.B. that the FETCH_* actions have been skipped. previewType: undefined, perceivedWait: 700 }, 'It should enqueue a "dismissed" event when the preview has been shown.' ); } ); QUnit.test( 'ABANDON_END doesn\'t enqueue an event under certain conditions', function ( assert ) { const token = '0987654321', now = Date.now(); const dwelledState = eventLogging( undefined, { type: actionTypes.LINK_DWELL, el: this.link, title: 'Foo', namespaceId: 1, token, timestamp: now } ); let state = eventLogging( dwelledState, { type: actionTypes.ABANDON_END, token: '1234567890' } ); assert.strictEqual( state.event, undefined, 'It shouldn\'t enqueue an event if there\'s a new interaction.' ); // --- state = eventLogging( dwelledState, { type: actionTypes.ABANDON_END, token } ); assert.strictEqual( state.event, undefined, 'It shouldn\'t enqueue an event if the user is dwelling on the preview or the link.' ); // --- state = eventLogging( dwelledState, { type: actionTypes.LINK_CLICK, timestamp: now + 500 } ); state = eventLogging( state, { type: actionTypes.EVENT_LOGGED, event: {} } ); state = eventLogging( state, { type: actionTypes.ABANDON_START, token, timestamp: now + 700 } ); state = eventLogging( state, { type: actionTypes.ABANDON_END, token, timestamp: now + 1000 // ABANDON_END_DELAY is 300 ms. } ); assert.strictEqual( state.event, undefined, 'It shouldn\'t enqueue an event if the interaction is finalized.' ); } );