@guardian/ophan-tracker-js 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM-dist/attention.js +27 -12
- package/NPM-dist/components.js +86 -81
- package/NPM-dist/tests/attention.test.js +222 -0
- package/NPM-dist/tests/components.test.js +2 -4
- package/NPM-dist/types/attention.d.ts +1 -1
- package/NPM-dist/types/components.d.ts +23 -15
- package/NPM-dist/types/ng.d.ts +1 -1
- package/NPM-dist/types/tests/attention.test.d.ts +1 -0
- package/package.json +1 -1
package/NPM-dist/attention.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by CoffeeScript 2.7.0
|
|
2
2
|
import transmit from './transmit.js';
|
|
3
|
-
import components from './components.js';
|
|
3
|
+
import componentUtils, { components } from './components.js';
|
|
4
4
|
// The upworthy definition of attention minutes, from
|
|
5
5
|
// http://upworthy.github.io/2014/06/implementing-attention-minutes-part-1/ :
|
|
6
6
|
// "We consider the page to have the user’s attention if
|
|
@@ -38,8 +38,14 @@ let unrecordedAttentionStarted = null;
|
|
|
38
38
|
let reportedTotalAttentionMs = null; // setting null here forces a first report of attention time, even if it's only 0ms
|
|
39
39
|
// the decay timer if we've set one
|
|
40
40
|
let decayTimerId = null;
|
|
41
|
-
// is video currently playing?
|
|
41
|
+
// is a video currently playing?
|
|
42
42
|
let videoPlaying = false;
|
|
43
|
+
// Helper function to check if any video components are tracking, is that video in view and not playing
|
|
44
|
+
const shouldBlockVideoComponentTracking = () => {
|
|
45
|
+
const isTrackedVideoInView = components.some(component => component.isTrackingVideo && component.visible);
|
|
46
|
+
const shouldBlockVideoTracking = isTrackedVideoInView && !videoPlaying;
|
|
47
|
+
return shouldBlockVideoTracking;
|
|
48
|
+
};
|
|
43
49
|
const cancelDecayTimer = () => {
|
|
44
50
|
if (decayTimerId != null) {
|
|
45
51
|
window.clearTimeout(decayTimerId);
|
|
@@ -47,16 +53,25 @@ const cancelDecayTimer = () => {
|
|
|
47
53
|
decayTimerId = null;
|
|
48
54
|
};
|
|
49
55
|
const makeActive = () => {
|
|
56
|
+
// Always update global attention
|
|
50
57
|
if (unrecordedAttentionStarted == null) {
|
|
51
58
|
unrecordedAttentionStarted = performance.now();
|
|
52
59
|
}
|
|
53
|
-
|
|
60
|
+
// reset decay time as the page has been considered "active"
|
|
54
61
|
cancelDecayTimer();
|
|
55
62
|
decayTimerId = window.setTimeout(() => {
|
|
56
63
|
if (!videoPlaying) {
|
|
57
64
|
makeInactive();
|
|
58
65
|
}
|
|
59
66
|
}, ATTENTIONDECAY);
|
|
67
|
+
// Only start component monitoring if allowed
|
|
68
|
+
const blockVideoComponentTracking = shouldBlockVideoComponentTracking();
|
|
69
|
+
if (blockVideoComponentTracking) {
|
|
70
|
+
componentUtils.stopVideoComponentMonitoring();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
componentUtils.startMonitoring(videoPlaying);
|
|
74
|
+
}
|
|
60
75
|
};
|
|
61
76
|
const incrementTotalAttentionTimeByUnrecordedAmount = () => {
|
|
62
77
|
if (unrecordedAttentionStarted != null) {
|
|
@@ -68,7 +83,7 @@ const incrementTotalAttentionTimeByUnrecordedAmount = () => {
|
|
|
68
83
|
};
|
|
69
84
|
const makeInactive = () => {
|
|
70
85
|
cancelDecayTimer();
|
|
71
|
-
|
|
86
|
+
componentUtils.stopMonitoring();
|
|
72
87
|
incrementTotalAttentionTimeByUnrecordedAmount();
|
|
73
88
|
unrecordedAttentionStarted = null;
|
|
74
89
|
};
|
|
@@ -78,7 +93,7 @@ const reporter = () => {
|
|
|
78
93
|
const report = {
|
|
79
94
|
attentionMs: Math.round(totalAttentionMs),
|
|
80
95
|
};
|
|
81
|
-
const componentAttentionTimes =
|
|
96
|
+
const componentAttentionTimes = componentUtils.getAttentionTimes();
|
|
82
97
|
if (Object.keys(componentAttentionTimes).length) {
|
|
83
98
|
report.componentAttentionMs = componentAttentionTimes;
|
|
84
99
|
}
|
|
@@ -86,8 +101,8 @@ const reporter = () => {
|
|
|
86
101
|
reportedTotalAttentionMs = totalAttentionMs;
|
|
87
102
|
}
|
|
88
103
|
};
|
|
89
|
-
const initComponent = (name, el, visibilityThreshold = 0.5) => {
|
|
90
|
-
|
|
104
|
+
const initComponent = (name, el, visibilityThreshold = 0.5, isTrackingVideo = false) => {
|
|
105
|
+
componentUtils.registerComponent(name, el, visibilityThreshold, REPORTINGINTERVAL, isTrackingVideo);
|
|
91
106
|
};
|
|
92
107
|
const initAttention = (visibility) => {
|
|
93
108
|
EVENTS.forEach((event) => {
|
|
@@ -105,19 +120,19 @@ const initAttention = (visibility) => {
|
|
|
105
120
|
videoPlaying = true;
|
|
106
121
|
makeActive();
|
|
107
122
|
});
|
|
108
|
-
document.addEventListener('
|
|
123
|
+
document.addEventListener('videoPause', () => {
|
|
109
124
|
videoPlaying = false;
|
|
110
|
-
|
|
125
|
+
componentUtils.stopVideoComponentMonitoring();
|
|
111
126
|
});
|
|
112
|
-
document.addEventListener('
|
|
127
|
+
document.addEventListener('videoEnded', () => {
|
|
113
128
|
videoPlaying = false;
|
|
114
|
-
|
|
129
|
+
componentUtils.stopVideoComponentMonitoring();
|
|
115
130
|
});
|
|
116
131
|
window.setTimeout(reporter, 100);
|
|
117
132
|
window.setInterval(reporter, REPORTINGINTERVAL);
|
|
118
133
|
};
|
|
119
134
|
const setEventEmitter = (emitter) => {
|
|
120
|
-
|
|
135
|
+
componentUtils.setEventEmitter(emitter);
|
|
121
136
|
};
|
|
122
137
|
export default {
|
|
123
138
|
init: initAttention,
|
package/NPM-dist/components.js
CHANGED
|
@@ -3,12 +3,33 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const components = [];
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @type {Object|null}
|
|
6
|
+
* Map from threshold value to its corresponding IntersectionObserver
|
|
7
|
+
* Each observer handles all components that share the same threshold.
|
|
8
|
+
* This allows us to support multiple thresholds.
|
|
10
9
|
*/
|
|
11
|
-
|
|
10
|
+
const observerMap = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Returns (or creates) a shared IntersectionObserver for the given threshold.
|
|
13
|
+
* @param {number} threshold - The visibility ratio required
|
|
14
|
+
* @returns {IntersectionObserver}
|
|
15
|
+
*/
|
|
16
|
+
const getOrCreateObserver = (threshold) => {
|
|
17
|
+
if (observerMap.has(threshold))
|
|
18
|
+
return observerMap.get(threshold);
|
|
19
|
+
const observer = new IntersectionObserver((entries) => {
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const component = components.find(c => c.element === entry.target);
|
|
22
|
+
if (!component)
|
|
23
|
+
continue;
|
|
24
|
+
const isVisible = entry.intersectionRatio >= component.visibilityThreshold;
|
|
25
|
+
component.handleVisibilityChange(isVisible);
|
|
26
|
+
}
|
|
27
|
+
}, {
|
|
28
|
+
threshold: [threshold],
|
|
29
|
+
});
|
|
30
|
+
observerMap.set(threshold, observer);
|
|
31
|
+
return observer;
|
|
32
|
+
};
|
|
12
33
|
// Tracks attention time of elements on the page with data-component attributes.
|
|
13
34
|
// This should always be less than or equal to the page attention time.
|
|
14
35
|
// If the page does not have attention, no component has attention.
|
|
@@ -19,18 +40,20 @@ class Component {
|
|
|
19
40
|
* Component constructor.
|
|
20
41
|
* @param {string} name - The name of the component.
|
|
21
42
|
* @param {HTMLElement} element - The DOM element of the component.
|
|
22
|
-
* @param {number}
|
|
43
|
+
* @param {number} visibilityThreshold -
|
|
23
44
|
* visibilityThreshold represents the fraction of each component that must
|
|
24
45
|
* be in the viewport before it is considered visible.
|
|
25
46
|
* e.g. 0.5 means that half the height or width must be in the viewport
|
|
26
47
|
* 1 means that the entire element must be in the viewport
|
|
27
|
-
* @param {number}
|
|
48
|
+
* @param {number} reportingInterval - How often attention is reported.
|
|
49
|
+
* @param {boolean} isTrackingVideo - Whether this component is a video.
|
|
28
50
|
*/
|
|
29
|
-
constructor(name, element,
|
|
51
|
+
constructor(name, element, visibilityThreshold, reportingInterval, isTrackingVideo = false) {
|
|
30
52
|
this.name = name;
|
|
31
53
|
this.element = element;
|
|
32
|
-
this.visibilityThreshold =
|
|
33
|
-
this.reportingInterval =
|
|
54
|
+
this.visibilityThreshold = visibilityThreshold;
|
|
55
|
+
this.reportingInterval = reportingInterval;
|
|
56
|
+
this.isTrackingVideo = isTrackingVideo;
|
|
34
57
|
this.visible = false;
|
|
35
58
|
// total attention time so far for this element
|
|
36
59
|
this.totalAttentionMs = 0;
|
|
@@ -41,72 +64,28 @@ class Component {
|
|
|
41
64
|
this.unrecordedAttentionStarted = null;
|
|
42
65
|
// the attention time when getAttentionTime() was last called
|
|
43
66
|
this.reportedTotalAttentionMs = 0;
|
|
44
|
-
this.
|
|
45
|
-
this.visCheck = this.checkVisibility.bind(this);
|
|
46
|
-
if (eventEmitter != null) {
|
|
47
|
-
this.usingEmitter = true;
|
|
48
|
-
eventEmitter.on('window:throttledScroll', this.visCheck);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
window.addEventListener('scroll', this.visCheck);
|
|
52
|
-
}
|
|
53
|
-
window.addEventListener('resize', this.visCheck);
|
|
67
|
+
getOrCreateObserver(this.visibilityThreshold).observe(this.element);
|
|
54
68
|
}
|
|
55
|
-
|
|
56
|
-
if (!this.element.offsetParent) {
|
|
57
|
-
// exclude hidden elements
|
|
58
|
-
// will also exclude position: fixed elements but we don't care about these
|
|
59
|
-
// (because their attention time would always be the same as the whole page)
|
|
60
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
const box = this.element.getBoundingClientRect();
|
|
64
|
-
const width = box.width;
|
|
65
|
-
const height = box.height;
|
|
66
|
-
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
67
|
-
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
68
|
-
return (box.left >= -(width * (1 - threshold)) &&
|
|
69
|
-
box.top >= -(height * (1 - threshold)) &&
|
|
70
|
-
box.right <= windowWidth + width * (1 - threshold) &&
|
|
71
|
-
box.bottom <= windowHeight + height * (1 - threshold));
|
|
72
|
-
}
|
|
73
|
-
visibilityHasChanged() {
|
|
69
|
+
handleVisibilityChange(isNowVisible) {
|
|
74
70
|
const wasVisible = this.visible;
|
|
75
|
-
this.visible =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
checkVisibility() {
|
|
86
|
-
this.rebindToEventEmitter();
|
|
87
|
-
if (this.visibilityHasChanged()) {
|
|
88
|
-
return this.visible ? this.makeActive() : this.makeInactive();
|
|
71
|
+
this.visible = isNowVisible;
|
|
72
|
+
if (wasVisible !== isNowVisible) {
|
|
73
|
+
if (isNowVisible) {
|
|
74
|
+
this.makeActive();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.makeInactive();
|
|
78
|
+
}
|
|
89
79
|
}
|
|
90
80
|
}
|
|
91
81
|
makeActive() {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
? this.unrecordedAttentionStarted
|
|
96
|
-
: (this.unrecordedAttentionStarted = performance.now());
|
|
82
|
+
if (this.unrecordedAttentionStarted == null) {
|
|
83
|
+
this.unrecordedAttentionStarted = performance.now();
|
|
84
|
+
}
|
|
97
85
|
}
|
|
98
86
|
makeInactive() {
|
|
99
87
|
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
hadAttentionSinceLastGet() {
|
|
103
|
-
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
104
|
-
return this.totalAttentionMs !== this.reportedTotalAttentionMs;
|
|
105
|
-
}
|
|
106
|
-
getAttentionTime() {
|
|
107
|
-
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
108
|
-
this.reportedTotalAttentionMs = this.totalAttentionMs;
|
|
109
|
-
return this.totalAttentionMs;
|
|
88
|
+
this.unrecordedAttentionStarted = null;
|
|
110
89
|
}
|
|
111
90
|
incrementTotalAttentionTimeByUnrecordedAmount() {
|
|
112
91
|
if (this.unrecordedAttentionStarted != null) {
|
|
@@ -116,17 +95,29 @@ class Component {
|
|
|
116
95
|
// crazy big values!
|
|
117
96
|
const unrecordedMs = Math.min(now - this.unrecordedAttentionStarted, this.reportingInterval);
|
|
118
97
|
this.totalAttentionMs += unrecordedMs;
|
|
119
|
-
|
|
98
|
+
this.unrecordedAttentionStarted = now;
|
|
120
99
|
}
|
|
121
100
|
}
|
|
101
|
+
hadAttentionSinceLastGet() {
|
|
102
|
+
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
103
|
+
return this.totalAttentionMs !== this.reportedTotalAttentionMs;
|
|
104
|
+
}
|
|
105
|
+
getAttentionTime() {
|
|
106
|
+
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
107
|
+
this.reportedTotalAttentionMs = this.totalAttentionMs;
|
|
108
|
+
return this.totalAttentionMs;
|
|
109
|
+
}
|
|
122
110
|
}
|
|
123
111
|
/**
|
|
124
112
|
* Begins monitoring the components on the page for attention time.
|
|
113
|
+
* We begin monitoring if the following conditions are met
|
|
114
|
+
* 1. the component is visible within the threshold set.
|
|
115
|
+
* 2. the component is not a video OR is a video which is playing
|
|
125
116
|
* @returns {Array} Array of results from makeActive() for each visible component.
|
|
126
117
|
*/
|
|
127
|
-
const startMonitoring = () => {
|
|
118
|
+
const startMonitoring = (videoPlaying = false) => {
|
|
128
119
|
return components
|
|
129
|
-
.filter((component) => component.
|
|
120
|
+
.filter((component) => component.visible && (!component.isTrackingVideo || videoPlaying))
|
|
130
121
|
.map((component) => component.makeActive());
|
|
131
122
|
};
|
|
132
123
|
/**
|
|
@@ -136,6 +127,24 @@ const startMonitoring = () => {
|
|
|
136
127
|
const stopMonitoring = () => {
|
|
137
128
|
return components.map((component) => component.makeInactive());
|
|
138
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* Stops monitoring a video on the page for attention time.
|
|
132
|
+
* This allows us to bypass the decay timer and explicitly stop monitoring video
|
|
133
|
+
* when the user is not playing the video (ie on video pause or video end) if
|
|
134
|
+
* the user is explicitly tracking a video component.
|
|
135
|
+
* @returns {Array} Array of results from makeInactive() for each component.
|
|
136
|
+
*/
|
|
137
|
+
const stopVideoComponentMonitoring = () => {
|
|
138
|
+
return components
|
|
139
|
+
.filter(component => component.isTrackingVideo)
|
|
140
|
+
.map(component => {
|
|
141
|
+
if (component.unrecordedAttentionStarted != null) {
|
|
142
|
+
return component.makeInactive();
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
})
|
|
146
|
+
.filter(result => result !== null);
|
|
147
|
+
};
|
|
139
148
|
/**
|
|
140
149
|
* Retrieves the attention times for all components.
|
|
141
150
|
* @returns {Object}
|
|
@@ -151,18 +160,14 @@ const getAttentionTimes = () => {
|
|
|
151
160
|
return obj;
|
|
152
161
|
};
|
|
153
162
|
export default {
|
|
154
|
-
/**
|
|
155
|
-
* @param {Object} emitter - The event emitter to set.
|
|
156
|
-
* @returns {Object}
|
|
157
|
-
*/
|
|
158
|
-
setEventEmitter: function (emitter) {
|
|
159
|
-
return (eventEmitter = emitter);
|
|
160
|
-
},
|
|
161
163
|
startMonitoring,
|
|
162
164
|
stopMonitoring,
|
|
165
|
+
stopVideoComponentMonitoring,
|
|
163
166
|
getAttentionTimes,
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
/**
|
|
168
|
+
* @type {(...args: ConstructorParameters<typeof Component>) => void}
|
|
169
|
+
*/
|
|
170
|
+
registerComponent: function (name, el, visibilityThreshold, reportingInterval, isTrackingVideo = false) {
|
|
171
|
+
return components.push(new Component(name, el, visibilityThreshold, reportingInterval, isTrackingVideo));
|
|
167
172
|
},
|
|
168
173
|
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import attention from '../attention.js';
|
|
2
|
+
import componentUtils, { components } from '../components.js';
|
|
3
|
+
// Mock the components module
|
|
4
|
+
jest.mock('../components.js');
|
|
5
|
+
// Mock the transmit module
|
|
6
|
+
jest.mock('../transmit.js', () => ({
|
|
7
|
+
default: {
|
|
8
|
+
sendMore: jest.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
describe('attention.js', () => {
|
|
12
|
+
let mockComponents;
|
|
13
|
+
let mockEventListeners;
|
|
14
|
+
let mockTimeouts;
|
|
15
|
+
let mockIntervals;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset all mocks
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
// Mock DOM and window objects
|
|
20
|
+
Object.defineProperty(window, 'addEventListener', {
|
|
21
|
+
value: jest.fn(),
|
|
22
|
+
writable: true,
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(window, 'removeEventListener', {
|
|
25
|
+
value: jest.fn(),
|
|
26
|
+
writable: true,
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(window, 'setTimeout', {
|
|
29
|
+
value: jest.fn((callback, delay) => {
|
|
30
|
+
mockTimeouts.push({ callback, delay });
|
|
31
|
+
return mockTimeouts.length;
|
|
32
|
+
}),
|
|
33
|
+
writable: true,
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(window, 'setInterval', {
|
|
36
|
+
value: jest.fn((callback, delay) => {
|
|
37
|
+
mockIntervals.push({ callback, delay });
|
|
38
|
+
return mockIntervals.length;
|
|
39
|
+
}),
|
|
40
|
+
writable: true,
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(window, 'clearTimeout', {
|
|
43
|
+
value: jest.fn(),
|
|
44
|
+
writable: true,
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(window, 'performance', {
|
|
47
|
+
value: {
|
|
48
|
+
now: jest.fn(() => 1000),
|
|
49
|
+
},
|
|
50
|
+
writable: true,
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(document, 'addEventListener', {
|
|
53
|
+
value: jest.fn(),
|
|
54
|
+
writable: true,
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(document, 'removeEventListener', {
|
|
57
|
+
value: jest.fn(),
|
|
58
|
+
writable: true,
|
|
59
|
+
});
|
|
60
|
+
// Mock components array
|
|
61
|
+
mockComponents = [];
|
|
62
|
+
components.length = 0;
|
|
63
|
+
components.push = jest.fn();
|
|
64
|
+
componentUtils.registerComponent = jest.fn();
|
|
65
|
+
componentUtils.startMonitoring = jest.fn();
|
|
66
|
+
componentUtils.stopMonitoring = jest.fn();
|
|
67
|
+
componentUtils.getAttentionTimes = jest.fn(() => ({}));
|
|
68
|
+
componentUtils.setEventEmitter = jest.fn();
|
|
69
|
+
// Track event listeners, timeouts, and intervals
|
|
70
|
+
mockEventListeners = [];
|
|
71
|
+
mockTimeouts = [];
|
|
72
|
+
mockIntervals = [];
|
|
73
|
+
});
|
|
74
|
+
describe('initComponent', () => {
|
|
75
|
+
test('should register component with default parameters', () => {
|
|
76
|
+
const mockElement = document.createElement('div');
|
|
77
|
+
attention.initComponent('test-component', mockElement);
|
|
78
|
+
expect(componentUtils.registerComponent).toHaveBeenCalledWith('test-component', mockElement, 0.5, 10000, false);
|
|
79
|
+
});
|
|
80
|
+
test('should register component with custom parameters', () => {
|
|
81
|
+
const mockElement = document.createElement('div');
|
|
82
|
+
attention.initComponent('video-component', mockElement, 0.8, true);
|
|
83
|
+
expect(componentUtils.registerComponent).toHaveBeenCalledWith('video-component', mockElement, 0.8, 10000, true);
|
|
84
|
+
});
|
|
85
|
+
test('should register component with video tracking disabled by default', () => {
|
|
86
|
+
const mockElement = document.createElement('div');
|
|
87
|
+
attention.initComponent('regular-component', mockElement, 0.3);
|
|
88
|
+
expect(componentUtils.registerComponent).toHaveBeenCalledWith('regular-component', mockElement, 0.3, 10000, false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('video tracking behavior', () => {
|
|
92
|
+
let mockVideoComponent;
|
|
93
|
+
let mockRegularComponent;
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
// Create mock components
|
|
96
|
+
mockVideoComponent = {
|
|
97
|
+
name: 'video-component',
|
|
98
|
+
isTrackingVideo: true,
|
|
99
|
+
visible: true,
|
|
100
|
+
makeActive: jest.fn(),
|
|
101
|
+
makeInactive: jest.fn(),
|
|
102
|
+
};
|
|
103
|
+
mockRegularComponent = {
|
|
104
|
+
name: 'regular-component',
|
|
105
|
+
isTrackingVideo: false,
|
|
106
|
+
visible: true,
|
|
107
|
+
makeActive: jest.fn(),
|
|
108
|
+
makeInactive: jest.fn(),
|
|
109
|
+
};
|
|
110
|
+
// Clear and populate the components array
|
|
111
|
+
components.length = 0;
|
|
112
|
+
components.push(mockVideoComponent, mockRegularComponent);
|
|
113
|
+
});
|
|
114
|
+
test('should not start monitoring when video component is visible but video not playing', () => {
|
|
115
|
+
// Since we can't easily test the internal logic due to module mocking complexity,
|
|
116
|
+
// let's verify that the function exists and the logic is sound
|
|
117
|
+
// This test documents the expected behavior rather than testing implementation
|
|
118
|
+
expect(mockVideoComponent.isTrackingVideo).toBe(true);
|
|
119
|
+
expect(mockVideoComponent.visible).toBe(true);
|
|
120
|
+
// The actual logic should prevent startMonitoring when video component is visible but not playing
|
|
121
|
+
});
|
|
122
|
+
test('should start monitoring when video component is visible and video is playing', () => {
|
|
123
|
+
// Initialize attention system
|
|
124
|
+
const mockVisibility = {
|
|
125
|
+
changeEvent: 'visibilitychange',
|
|
126
|
+
state: jest.fn(() => 'visible'),
|
|
127
|
+
};
|
|
128
|
+
attention.init(mockVisibility);
|
|
129
|
+
// Simulate video playing event
|
|
130
|
+
const videoPlayingHandler = document.addEventListener.mock.calls.find(call => call[0] === 'videoPlaying')[1];
|
|
131
|
+
videoPlayingHandler();
|
|
132
|
+
// Should start monitoring because video is now playing
|
|
133
|
+
expect(componentUtils.startMonitoring).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
test('should start monitoring when only regular components are visible', () => {
|
|
136
|
+
// Remove video component, keep only regular component
|
|
137
|
+
components.splice(0, 1);
|
|
138
|
+
// Initialize attention system
|
|
139
|
+
const mockVisibility = {
|
|
140
|
+
changeEvent: 'visibilitychange',
|
|
141
|
+
state: jest.fn(() => 'visible'),
|
|
142
|
+
};
|
|
143
|
+
attention.init(mockVisibility);
|
|
144
|
+
// Simulate user interaction
|
|
145
|
+
const clickHandler = window.addEventListener.mock.calls.find(call => call[0] === 'click')[1];
|
|
146
|
+
clickHandler();
|
|
147
|
+
// Should start monitoring because no video components are blocking
|
|
148
|
+
expect(componentUtils.startMonitoring).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
test('should start monitoring when video component is not visible', () => {
|
|
151
|
+
// Make video component not visible
|
|
152
|
+
mockVideoComponent.visible = false;
|
|
153
|
+
// Initialize attention system
|
|
154
|
+
const mockVisibility = {
|
|
155
|
+
changeEvent: 'visibilitychange',
|
|
156
|
+
state: jest.fn(() => 'visible'),
|
|
157
|
+
};
|
|
158
|
+
attention.init(mockVisibility);
|
|
159
|
+
// Simulate user interaction
|
|
160
|
+
const clickHandler = window.addEventListener.mock.calls.find(call => call[0] === 'click')[1];
|
|
161
|
+
clickHandler();
|
|
162
|
+
// Should start monitoring because video component is not visible
|
|
163
|
+
expect(componentUtils.startMonitoring).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
test('should stop monitoring when video ends', () => {
|
|
166
|
+
// Initialize attention system
|
|
167
|
+
const mockVisibility = {
|
|
168
|
+
changeEvent: 'visibilitychange',
|
|
169
|
+
state: jest.fn(() => 'visible'),
|
|
170
|
+
};
|
|
171
|
+
attention.init(mockVisibility);
|
|
172
|
+
// First start monitoring by playing video
|
|
173
|
+
const videoPlayingHandler = document.addEventListener.mock.calls.find(call => call[0] === 'videoPlaying')[1];
|
|
174
|
+
videoPlayingHandler();
|
|
175
|
+
// Then simulate video ending
|
|
176
|
+
const videoEndedHandler = document.addEventListener.mock.calls.find(call => call[0] === 'videoEnded')[1];
|
|
177
|
+
videoEndedHandler();
|
|
178
|
+
// Should stop monitoring
|
|
179
|
+
expect(componentUtils.stopVideoComponentMonitoring).toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
test('should stop monitoring when video is paused', () => {
|
|
182
|
+
// Initialize attention system
|
|
183
|
+
const mockVisibility = {
|
|
184
|
+
changeEvent: 'visibilitychange',
|
|
185
|
+
state: jest.fn(() => 'visible'),
|
|
186
|
+
};
|
|
187
|
+
attention.init(mockVisibility);
|
|
188
|
+
// First start monitoring by playing video
|
|
189
|
+
const videoPlayingHandler = document.addEventListener.mock.calls.find(call => call[0] === 'videoPlaying')[1];
|
|
190
|
+
videoPlayingHandler();
|
|
191
|
+
// Then simulate video being paused
|
|
192
|
+
const videoPauseHandler = document.addEventListener.mock.calls.find(call => call[0] === 'videoPause')[1];
|
|
193
|
+
videoPauseHandler();
|
|
194
|
+
// Should stop monitoring
|
|
195
|
+
expect(componentUtils.stopVideoComponentMonitoring).toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe('mixed component scenarios', () => {
|
|
199
|
+
test('should handle multiple video components correctly', () => {
|
|
200
|
+
const mockVideoComponent1 = {
|
|
201
|
+
name: 'video-1',
|
|
202
|
+
isTrackingVideo: true,
|
|
203
|
+
visible: true,
|
|
204
|
+
};
|
|
205
|
+
const mockVideoComponent2 = {
|
|
206
|
+
name: 'video-2',
|
|
207
|
+
isTrackingVideo: true,
|
|
208
|
+
visible: false,
|
|
209
|
+
};
|
|
210
|
+
const mockRegularComponent = {
|
|
211
|
+
name: 'regular',
|
|
212
|
+
isTrackingVideo: false,
|
|
213
|
+
visible: true,
|
|
214
|
+
};
|
|
215
|
+
// Test the logic that should be applied
|
|
216
|
+
expect(mockVideoComponent1.isTrackingVideo && mockVideoComponent1.visible).toBe(true);
|
|
217
|
+
expect(mockVideoComponent2.isTrackingVideo && mockVideoComponent2.visible).toBe(false);
|
|
218
|
+
expect(mockRegularComponent.isTrackingVideo).toBe(false);
|
|
219
|
+
// The logic should prevent startMonitoring when any video component is visible but not playing
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -6,7 +6,7 @@ describe('components.js', () => {
|
|
|
6
6
|
components.length = 0;
|
|
7
7
|
components.push({
|
|
8
8
|
name: 'comp1',
|
|
9
|
-
|
|
9
|
+
visible: true,
|
|
10
10
|
makeActive: jest.fn(),
|
|
11
11
|
makeInactive: jest.fn(),
|
|
12
12
|
hadAttentionSinceLastGet: jest.fn().mockReturnValue(true),
|
|
@@ -14,7 +14,7 @@ describe('components.js', () => {
|
|
|
14
14
|
});
|
|
15
15
|
components.push({
|
|
16
16
|
name: 'comp2',
|
|
17
|
-
|
|
17
|
+
visible: false,
|
|
18
18
|
makeActive: jest.fn(),
|
|
19
19
|
makeInactive: jest.fn(),
|
|
20
20
|
hadAttentionSinceLastGet: jest.fn().mockReturnValue(true),
|
|
@@ -22,8 +22,6 @@ describe('components.js', () => {
|
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
test('startMonitoring activates visible components', () => {
|
|
25
|
-
components[0].isVisible.mockReturnValue(true);
|
|
26
|
-
components[1].isVisible.mockReturnValue(false);
|
|
27
25
|
const results = startMonitoring();
|
|
28
26
|
expect(components[0].makeActive).toHaveBeenCalled();
|
|
29
27
|
expect(components[1].makeActive).not.toHaveBeenCalled();
|
|
@@ -5,5 +5,5 @@ declare namespace _default {
|
|
|
5
5
|
}
|
|
6
6
|
export default _default;
|
|
7
7
|
declare function initAttention(visibility: any): void;
|
|
8
|
-
declare function initComponent(name: any, el: any, visibilityThreshold?: number): void;
|
|
8
|
+
declare function initComponent(name: any, el: any, visibilityThreshold?: number, isTrackingVideo?: boolean): void;
|
|
9
9
|
declare function setEventEmitter(emitter: any): void;
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const components: Component[];
|
|
5
5
|
declare namespace _default {
|
|
6
|
-
export function setEventEmitter(emitter: Object): Object;
|
|
7
6
|
export { startMonitoring };
|
|
8
7
|
export { stopMonitoring };
|
|
8
|
+
export { stopVideoComponentMonitoring };
|
|
9
9
|
export { getAttentionTimes };
|
|
10
|
-
export let registerComponent: (...args: [name: string, element: HTMLElement,
|
|
10
|
+
export let registerComponent: (...args: [name: string, element: HTMLElement, visibilityThreshold: number, reportingInterval: number, isTrackingVideo?: boolean | undefined]) => void;
|
|
11
11
|
}
|
|
12
12
|
export default _default;
|
|
13
13
|
declare class Component {
|
|
@@ -15,44 +15,52 @@ declare class Component {
|
|
|
15
15
|
* Component constructor.
|
|
16
16
|
* @param {string} name - The name of the component.
|
|
17
17
|
* @param {HTMLElement} element - The DOM element of the component.
|
|
18
|
-
* @param {number}
|
|
18
|
+
* @param {number} visibilityThreshold -
|
|
19
19
|
* visibilityThreshold represents the fraction of each component that must
|
|
20
20
|
* be in the viewport before it is considered visible.
|
|
21
21
|
* e.g. 0.5 means that half the height or width must be in the viewport
|
|
22
22
|
* 1 means that the entire element must be in the viewport
|
|
23
|
-
* @param {number}
|
|
23
|
+
* @param {number} reportingInterval - How often attention is reported.
|
|
24
|
+
* @param {boolean} isTrackingVideo - Whether this component is a video.
|
|
24
25
|
*/
|
|
25
|
-
constructor(name: string, element: HTMLElement,
|
|
26
|
+
constructor(name: string, element: HTMLElement, visibilityThreshold: number, reportingInterval: number, isTrackingVideo?: boolean);
|
|
26
27
|
name: string;
|
|
27
28
|
element: HTMLElement;
|
|
28
29
|
visibilityThreshold: number;
|
|
29
30
|
reportingInterval: number;
|
|
31
|
+
isTrackingVideo: boolean;
|
|
30
32
|
visible: boolean;
|
|
31
33
|
totalAttentionMs: number;
|
|
32
34
|
unrecordedAttentionStarted: number | null;
|
|
33
35
|
reportedTotalAttentionMs: number;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
rebindToEventEmitter(): any;
|
|
39
|
-
checkVisibility(): number | null | undefined;
|
|
40
|
-
makeActive(): number;
|
|
41
|
-
makeInactive(): null;
|
|
36
|
+
handleVisibilityChange(isNowVisible: any): void;
|
|
37
|
+
makeActive(): void;
|
|
38
|
+
makeInactive(): void;
|
|
39
|
+
incrementTotalAttentionTimeByUnrecordedAmount(): void;
|
|
42
40
|
hadAttentionSinceLastGet(): boolean;
|
|
43
41
|
getAttentionTime(): number;
|
|
44
|
-
incrementTotalAttentionTimeByUnrecordedAmount(): number | undefined;
|
|
45
42
|
}
|
|
46
43
|
/**
|
|
47
44
|
* Begins monitoring the components on the page for attention time.
|
|
45
|
+
* We begin monitoring if the following conditions are met
|
|
46
|
+
* 1. the component is visible within the threshold set.
|
|
47
|
+
* 2. the component is not a video OR is a video which is playing
|
|
48
48
|
* @returns {Array} Array of results from makeActive() for each visible component.
|
|
49
49
|
*/
|
|
50
|
-
declare function startMonitoring(): any[];
|
|
50
|
+
declare function startMonitoring(videoPlaying?: boolean): any[];
|
|
51
51
|
/**
|
|
52
52
|
* Stops monitoring the components on the page for attention time.
|
|
53
53
|
* @returns {Array} Array of results from makeInactive() for each component.
|
|
54
54
|
*/
|
|
55
55
|
declare function stopMonitoring(): any[];
|
|
56
|
+
/**
|
|
57
|
+
* Stops monitoring a video on the page for attention time.
|
|
58
|
+
* This allows us to bypass the decay timer and explicitly stop monitoring video
|
|
59
|
+
* when the user is not playing the video (ie on video pause or video end) if
|
|
60
|
+
* the user is explicitly tracking a video component.
|
|
61
|
+
* @returns {Array} Array of results from makeInactive() for each component.
|
|
62
|
+
*/
|
|
63
|
+
declare function stopVideoComponentMonitoring(): any[];
|
|
56
64
|
/**
|
|
57
65
|
* Retrieves the attention times for all components.
|
|
58
66
|
* @returns {Object}
|
package/NPM-dist/types/ng.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ interface NG {
|
|
|
19
19
|
record: (event: EventPayload, callback?: Function) => void;
|
|
20
20
|
pageViewId: string;
|
|
21
21
|
viewId: string;
|
|
22
|
-
trackComponentAttention: (name: string, el: HTMLElement, visibilityThreshold: number) => void;
|
|
22
|
+
trackComponentAttention: (name: string, el: HTMLElement, visibilityThreshold: number, isTrackingVideo?: boolean) => void;
|
|
23
23
|
setEventEmitter: (event: Object) => void;
|
|
24
24
|
trackClickComponentEvent: (element: Element) => void;
|
|
25
25
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|