@financial-times/cp-content-pipeline-ui 6.13.0 → 6.14.1
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/CHANGELOG.md +23 -0
- package/lib/client.d.ts +1 -0
- package/lib/client.js +3 -1
- package/lib/client.js.map +1 -1
- package/lib/components/Flourish/index.js +3 -3
- package/lib/components/Flourish/index.js.map +1 -1
- package/lib/components/RichText/index.d.ts +1 -1
- package/lib/components/RichText/index.test.js +6 -6
- package/lib/components/RichText/index.test.js.map +1 -1
- package/lib/components/Topper/client/index.d.ts +2 -0
- package/lib/components/Topper/client/index.js +6 -0
- package/lib/components/Topper/client/index.js.map +1 -0
- package/lib/components/Topper/client/tracking.d.ts +17 -0
- package/lib/components/Topper/client/tracking.js +68 -0
- package/lib/components/Topper/client/tracking.js.map +1 -0
- package/lib/components/Topper/client/tracking.spec.d.ts +1 -0
- package/lib/components/Topper/client/tracking.spec.js +115 -0
- package/lib/components/Topper/client/tracking.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/client.ts +1 -0
- package/src/components/Flourish/index.tsx +5 -2
- package/src/components/Flourish/test/__snapshots__/snapshot.spec.tsx.snap +12 -0
- package/src/components/RichText/index.test.tsx +14 -13
- package/src/components/Topper/client/index.ts +3 -0
- package/src/components/Topper/client/tracking.spec.ts +132 -0
- package/src/components/Topper/client/tracking.ts +88 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { TopperTracker } from './index'
|
|
2
|
+
|
|
3
|
+
describe('TopperTracker', () => {
|
|
4
|
+
let mockIntersectionObserver: jest.Mock
|
|
5
|
+
let mockObserve: jest.Mock
|
|
6
|
+
let mockUnobserve: jest.Mock
|
|
7
|
+
let mockDisconnect: jest.Mock
|
|
8
|
+
let mockComponent: HTMLElement
|
|
9
|
+
let dispatchEventSpy: jest.SpyInstance
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockObserve = jest.fn()
|
|
13
|
+
mockUnobserve = jest.fn()
|
|
14
|
+
mockDisconnect = jest.fn()
|
|
15
|
+
|
|
16
|
+
mockIntersectionObserver = jest.fn(() => {
|
|
17
|
+
return {
|
|
18
|
+
observe: mockObserve,
|
|
19
|
+
unobserve: mockUnobserve,
|
|
20
|
+
disconnect: mockDisconnect,
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
window.IntersectionObserver = mockIntersectionObserver
|
|
25
|
+
|
|
26
|
+
mockComponent = document.createElement('div')
|
|
27
|
+
mockComponent.setAttribute('data-component-type', 'flourish-topper')
|
|
28
|
+
mockComponent.setAttribute('data-component-id', 'test-id')
|
|
29
|
+
document.body.appendChild(mockComponent)
|
|
30
|
+
|
|
31
|
+
jest.spyOn(document, 'querySelector').mockReturnValue(mockComponent)
|
|
32
|
+
jest
|
|
33
|
+
.spyOn(performance, 'now')
|
|
34
|
+
.mockImplementationOnce(() => 1)
|
|
35
|
+
.mockImplementationOnce(() => 12344)
|
|
36
|
+
dispatchEventSpy = jest.spyOn(document.body, 'dispatchEvent')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
jest.clearAllMocks()
|
|
41
|
+
document.body.removeChild(mockComponent)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should initialise and observe the component', () => {
|
|
45
|
+
const tracker = new TopperTracker()
|
|
46
|
+
tracker.init()
|
|
47
|
+
|
|
48
|
+
expect(mockIntersectionObserver).toHaveBeenCalled()
|
|
49
|
+
expect(mockObserve).toHaveBeenCalledWith(mockComponent)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should dispatch a mount event on intialisation', () => {
|
|
53
|
+
const tracker = new TopperTracker()
|
|
54
|
+
tracker.init()
|
|
55
|
+
|
|
56
|
+
const mountEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent
|
|
57
|
+
expect(mountEvent.type).toBe('oTracking.event')
|
|
58
|
+
expect(mountEvent.detail).toEqual({
|
|
59
|
+
category: 'component',
|
|
60
|
+
action: 'mount',
|
|
61
|
+
component: {
|
|
62
|
+
name: 'flourish-topper',
|
|
63
|
+
id: 'test-id',
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should handle component visibility changes', () => {
|
|
69
|
+
const tracker = new TopperTracker()
|
|
70
|
+
tracker.init()
|
|
71
|
+
|
|
72
|
+
const mockChanges: IntersectionObserverEntry[] = [
|
|
73
|
+
{
|
|
74
|
+
target: mockComponent,
|
|
75
|
+
isIntersecting: true,
|
|
76
|
+
intersectionRatio: 1.0,
|
|
77
|
+
boundingClientRect: {} as DOMRectReadOnly,
|
|
78
|
+
intersectionRect: {} as DOMRectReadOnly,
|
|
79
|
+
rootBounds: null,
|
|
80
|
+
time: 1,
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
tracker['onChange'](mockChanges)
|
|
85
|
+
|
|
86
|
+
const viewEvent = dispatchEventSpy.mock.calls[1][0] as CustomEvent
|
|
87
|
+
expect(viewEvent.type).toBe('oTracking.event')
|
|
88
|
+
expect(viewEvent.detail).toEqual({
|
|
89
|
+
category: 'component',
|
|
90
|
+
action: 'view',
|
|
91
|
+
component: {
|
|
92
|
+
name: 'flourish-topper',
|
|
93
|
+
id: 'test-id',
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const mockChangesAfter: IntersectionObserverEntry[] = [
|
|
98
|
+
{
|
|
99
|
+
target: mockComponent,
|
|
100
|
+
isIntersecting: false,
|
|
101
|
+
intersectionRatio: 0.0,
|
|
102
|
+
boundingClientRect: {} as DOMRectReadOnly,
|
|
103
|
+
intersectionRect: {} as DOMRectReadOnly,
|
|
104
|
+
rootBounds: null,
|
|
105
|
+
time: 12.3444,
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
tracker['onChange'](mockChangesAfter)
|
|
110
|
+
|
|
111
|
+
const stopViewEvent = dispatchEventSpy.mock.calls[2][0] as CustomEvent
|
|
112
|
+
expect(stopViewEvent.type).toBe('oTracking.event')
|
|
113
|
+
expect(stopViewEvent.detail).toEqual({
|
|
114
|
+
category: 'component',
|
|
115
|
+
action: 'stop-view',
|
|
116
|
+
component: {
|
|
117
|
+
name: 'flourish-topper',
|
|
118
|
+
id: 'test-id',
|
|
119
|
+
timeElapsedSeconds: 12.34,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should disconnect the observer', () => {
|
|
125
|
+
const tracker = new TopperTracker()
|
|
126
|
+
tracker.init()
|
|
127
|
+
tracker.disconnect()
|
|
128
|
+
|
|
129
|
+
expect(mockUnobserve).toHaveBeenCalledWith(mockComponent)
|
|
130
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
interface Component {
|
|
2
|
+
name: string
|
|
3
|
+
id: string | null
|
|
4
|
+
timeElapsedSeconds?: number
|
|
5
|
+
}
|
|
6
|
+
class TopperTracker {
|
|
7
|
+
private startTime: number
|
|
8
|
+
private totalVisibleTime: number
|
|
9
|
+
private timeElapsedSeconds: number | null
|
|
10
|
+
private type: string
|
|
11
|
+
private component: Element | null
|
|
12
|
+
private id: string | null
|
|
13
|
+
private observer: IntersectionObserver | null
|
|
14
|
+
|
|
15
|
+
constructor({ type = 'flourish-topper' } = {}) {
|
|
16
|
+
this.startTime = 0
|
|
17
|
+
this.totalVisibleTime = 0
|
|
18
|
+
this.timeElapsedSeconds = null
|
|
19
|
+
this.type = type
|
|
20
|
+
this.component = document.querySelector(
|
|
21
|
+
`[data-component-type="${this.type}"]`
|
|
22
|
+
)
|
|
23
|
+
this.id = this.component?.getAttribute('data-component-id') || null
|
|
24
|
+
this.observer = null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
init(): void {
|
|
28
|
+
if (!window.IntersectionObserver || !this.component) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.dispatchEvent('mount')
|
|
33
|
+
|
|
34
|
+
this.observer = new IntersectionObserver(this.onChange.bind(this), {
|
|
35
|
+
threshold: [0.75],
|
|
36
|
+
})
|
|
37
|
+
this.observer.observe(this.component)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private dispatchEvent(action: string): void {
|
|
41
|
+
const component: Component = {
|
|
42
|
+
name: this.type,
|
|
43
|
+
id: this.id,
|
|
44
|
+
}
|
|
45
|
+
if (this.timeElapsedSeconds) {
|
|
46
|
+
component.timeElapsedSeconds = this.timeElapsedSeconds
|
|
47
|
+
}
|
|
48
|
+
const event = new CustomEvent('oTracking.event', {
|
|
49
|
+
detail: {
|
|
50
|
+
category: 'component',
|
|
51
|
+
action: action,
|
|
52
|
+
component,
|
|
53
|
+
},
|
|
54
|
+
bubbles: true,
|
|
55
|
+
})
|
|
56
|
+
document.body.dispatchEvent(event)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private onChange(changes: IntersectionObserverEntry[]): void {
|
|
60
|
+
changes.forEach((change) => {
|
|
61
|
+
if (change.target !== this.component) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if (change.isIntersecting || change.intersectionRatio >= 1) {
|
|
65
|
+
this.dispatchEvent('view')
|
|
66
|
+
this.startTime = performance.now()
|
|
67
|
+
}
|
|
68
|
+
if (!change.isIntersecting || change.intersectionRatio === 0) {
|
|
69
|
+
this.totalVisibleTime = performance.now() - this.startTime
|
|
70
|
+
this.timeElapsedSeconds = parseFloat(
|
|
71
|
+
(this.totalVisibleTime / 1000).toFixed(2)
|
|
72
|
+
)
|
|
73
|
+
this.dispatchEvent('stop-view')
|
|
74
|
+
this.totalVisibleTime = 0
|
|
75
|
+
this.timeElapsedSeconds = null
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
disconnect(): void {
|
|
81
|
+
if (this.observer && this.component) {
|
|
82
|
+
this.observer.unobserve(this.component)
|
|
83
|
+
this.observer.disconnect()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { TopperTracker }
|