@financial-times/cp-content-pipeline-ui 7.6.0 → 7.8.0
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 +14 -0
- package/lib/components/LiveBlogPost/index.js +1 -1
- package/lib/components/LiveBlogPost/index.js.map +1 -1
- package/lib/components/content-tree/Clip/client/index.d.ts +2 -0
- package/lib/components/content-tree/Clip/client/index.js +22 -2
- package/lib/components/content-tree/Clip/client/index.js.map +1 -1
- package/lib/components/content-tree/Clip/test/client/index.spec.d.ts +1 -0
- package/lib/components/content-tree/Clip/test/client/index.spec.js +178 -0
- package/lib/components/content-tree/Clip/test/client/index.spec.js.map +1 -0
- package/package.json +1 -1
- package/src/components/LiveBlogPost/index.tsx +1 -1
- package/src/components/content-tree/Clip/client/index.ts +28 -4
- package/src/components/content-tree/Clip/test/client/index.spec.ts +212 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import Clip from '../../client/index'
|
|
2
|
+
|
|
3
|
+
class IntersectionObserverMock implements IntersectionObserver {
|
|
4
|
+
root: Element | Document | null = null
|
|
5
|
+
rootMargin: string = ''
|
|
6
|
+
thresholds: ReadonlyArray<number> = []
|
|
7
|
+
callback: IntersectionObserverCallback
|
|
8
|
+
options?: IntersectionObserverInit
|
|
9
|
+
observe: jest.Mock
|
|
10
|
+
unobserve: jest.Mock
|
|
11
|
+
disconnect: jest.Mock
|
|
12
|
+
takeRecords: jest.Mock
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
callback: IntersectionObserverCallback,
|
|
16
|
+
options?: IntersectionObserverInit
|
|
17
|
+
) {
|
|
18
|
+
this.callback = callback
|
|
19
|
+
this.options = options
|
|
20
|
+
this.observe = jest.fn()
|
|
21
|
+
this.unobserve = jest.fn()
|
|
22
|
+
this.disconnect = jest.fn()
|
|
23
|
+
this.takeRecords = jest.fn().mockReturnValue([])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
trigger(
|
|
27
|
+
entries: IntersectionObserverEntry[],
|
|
28
|
+
observer: IntersectionObserverMock
|
|
29
|
+
) {
|
|
30
|
+
this.callback(entries, observer)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
global.IntersectionObserver =
|
|
35
|
+
IntersectionObserverMock as unknown as typeof IntersectionObserver
|
|
36
|
+
|
|
37
|
+
describe('Clip', () => {
|
|
38
|
+
let clipComponent: HTMLElement
|
|
39
|
+
let videoElement: HTMLVideoElement
|
|
40
|
+
let videoContainer: HTMLElement
|
|
41
|
+
let captionContainer: HTMLElement
|
|
42
|
+
let captionElement: HTMLElement
|
|
43
|
+
let clipInstance: Clip
|
|
44
|
+
let dispatchEventSpy: jest.SpyInstance
|
|
45
|
+
const mockReadySate = 4
|
|
46
|
+
let isPaused = true
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
videoElement = document.createElement('video')
|
|
50
|
+
videoElement.classList.add('cp-clip__video')
|
|
51
|
+
isPaused = true
|
|
52
|
+
|
|
53
|
+
const trackElement = document.createElement('track')
|
|
54
|
+
trackElement.kind = 'captions'
|
|
55
|
+
trackElement.srclang = 'en'
|
|
56
|
+
trackElement.src = 'https://next-media-api.ft.com/clips/captions/35441370'
|
|
57
|
+
videoElement.appendChild(trackElement)
|
|
58
|
+
|
|
59
|
+
Object.defineProperty(videoElement, 'readyState', {
|
|
60
|
+
get() {
|
|
61
|
+
return mockReadySate
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
Object.defineProperty(videoElement, 'paused', {
|
|
65
|
+
get() {
|
|
66
|
+
return isPaused
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
Object.defineProperty(videoElement, 'canPlayType', {
|
|
70
|
+
value: jest.fn().mockImplementation(() => 'probably'),
|
|
71
|
+
writable: true,
|
|
72
|
+
})
|
|
73
|
+
videoElement.play = jest.fn().mockImplementation(() => {
|
|
74
|
+
isPaused = false
|
|
75
|
+
return Promise.resolve()
|
|
76
|
+
})
|
|
77
|
+
videoElement.pause = jest.fn().mockImplementation(() => {
|
|
78
|
+
isPaused = true
|
|
79
|
+
})
|
|
80
|
+
const mockTextTracks = [
|
|
81
|
+
{
|
|
82
|
+
mode: 'disabled',
|
|
83
|
+
kind: 'captions',
|
|
84
|
+
language: 'en',
|
|
85
|
+
label: 'English',
|
|
86
|
+
addEventListener: jest.fn(),
|
|
87
|
+
removeEventListener: jest.fn(),
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
Object.defineProperty(videoElement, 'textTracks', {
|
|
91
|
+
value: mockTextTracks,
|
|
92
|
+
writable: true,
|
|
93
|
+
})
|
|
94
|
+
clipComponent = document.createElement('div')
|
|
95
|
+
|
|
96
|
+
videoContainer = document.createElement('div')
|
|
97
|
+
videoContainer.classList.add('cp-clip__video-container')
|
|
98
|
+
videoContainer.appendChild(videoElement)
|
|
99
|
+
|
|
100
|
+
captionElement = document.createElement('div')
|
|
101
|
+
captionElement.classList.add('cp-clip__caption')
|
|
102
|
+
captionElement.setAttribute('data-cp-clip-caption', 'true')
|
|
103
|
+
|
|
104
|
+
captionContainer = document.createElement('div')
|
|
105
|
+
captionContainer.classList.add('cp-clip__video-meta-info')
|
|
106
|
+
captionContainer.appendChild(captionElement)
|
|
107
|
+
|
|
108
|
+
clipComponent.appendChild(videoContainer)
|
|
109
|
+
clipComponent.appendChild(captionContainer)
|
|
110
|
+
document.body.appendChild(clipComponent)
|
|
111
|
+
|
|
112
|
+
clipInstance = new Clip(clipComponent, {
|
|
113
|
+
closedCaption: true,
|
|
114
|
+
autoShowClosedCaptions: true,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
dispatchEventSpy = jest.spyOn(videoElement, 'dispatchEvent')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
document.body.removeChild(clipComponent)
|
|
122
|
+
jest.clearAllMocks()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should initialise with default options', () => {
|
|
126
|
+
expect(clipInstance.opts.autorender).toBe(true)
|
|
127
|
+
expect(clipInstance.opts.fadeOutDelay).toBe(2000)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should update amount watched on pause', async () => {
|
|
131
|
+
isPaused = false
|
|
132
|
+
clipInstance.togglePlay()
|
|
133
|
+
clipInstance.markPlayStart()
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
clipInstance.updateAmountWatched()
|
|
136
|
+
expect(clipInstance.amountWatched).toBeGreaterThan(0)
|
|
137
|
+
}, 1)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should toggle play/pause state', () => {
|
|
141
|
+
clipInstance.togglePlay()
|
|
142
|
+
expect(videoElement.paused).toBe(false)
|
|
143
|
+
clipInstance.togglePlay()
|
|
144
|
+
expect(videoElement.paused).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should fire watched event on unload', () => {
|
|
148
|
+
const fireEventSpy = jest.spyOn(clipInstance, 'fireEvent')
|
|
149
|
+
clipInstance.unload()
|
|
150
|
+
expect(fireEventSpy).toHaveBeenCalledWith('watched')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should create custom player controls', () => {
|
|
154
|
+
clipInstance.createCustomPlayer()
|
|
155
|
+
const playerCreatedEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent
|
|
156
|
+
expect(playerCreatedEvent.type).toBe(
|
|
157
|
+
'cpContentPipeline.clipComponent.customPlayerCreated'
|
|
158
|
+
)
|
|
159
|
+
expect(playerCreatedEvent.detail).toEqual({
|
|
160
|
+
clipId: clipInstance.opts.id,
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should handle visibility changes', () => {
|
|
165
|
+
const visibilityListenerSpy = jest.spyOn(clipInstance, 'visibilityListener')
|
|
166
|
+
clipInstance.visibilityListener([
|
|
167
|
+
{ isIntersecting: true } as IntersectionObserverEntry,
|
|
168
|
+
])
|
|
169
|
+
expect(visibilityListenerSpy).toHaveBeenCalled()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should play closed captions by default when available', () => {
|
|
173
|
+
clipInstance.videoEl.dispatchEvent(new Event('playing'))
|
|
174
|
+
expect(clipInstance.videoEl.textTracks[0]?.mode).toBe('showing')
|
|
175
|
+
expect(clipInstance.videoEl.getAttribute('crossorigin')).toBe('true')
|
|
176
|
+
const closedCaptionIcon = document.querySelector('.cp-clip__closed-caption')
|
|
177
|
+
expect(
|
|
178
|
+
closedCaptionIcon?.getAttribute('data-display-closed-captions')
|
|
179
|
+
).toBeDefined()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should toggle closed captions', () => {
|
|
183
|
+
clipInstance.videoEl.dispatchEvent(new Event('playing'))
|
|
184
|
+
const fireEventSpy = jest.spyOn(clipInstance, 'fireEvent')
|
|
185
|
+
clipInstance.containerEl
|
|
186
|
+
.querySelector('.cp-clip__closed-caption')
|
|
187
|
+
?.dispatchEvent(new Event('click'))
|
|
188
|
+
expect(clipInstance.videoEl.textTracks[0]?.mode).toBe('hidden')
|
|
189
|
+
const closedCaptionIcon = clipInstance.containerEl.querySelector(
|
|
190
|
+
'.cp-clip__closed-caption'
|
|
191
|
+
)
|
|
192
|
+
expect(
|
|
193
|
+
closedCaptionIcon?.getAttribute('data-display-closed-captions')
|
|
194
|
+
).toBeNull()
|
|
195
|
+
expect(fireEventSpy.mock.calls[0]).toEqual([
|
|
196
|
+
'cta:click',
|
|
197
|
+
{ trigger_action: 'turn captions off' },
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
clipInstance.containerEl
|
|
201
|
+
.querySelector('.cp-clip__closed-caption')
|
|
202
|
+
?.dispatchEvent(new Event('click'))
|
|
203
|
+
expect(clipInstance.videoEl.textTracks[0]?.mode).toBe('showing')
|
|
204
|
+
expect(
|
|
205
|
+
closedCaptionIcon?.getAttribute('data-display-closed-captions')
|
|
206
|
+
).toBeDefined()
|
|
207
|
+
expect(fireEventSpy.mock.calls[1]).toEqual([
|
|
208
|
+
'cta:click',
|
|
209
|
+
{ trigger_action: 'turn captions on' },
|
|
210
|
+
])
|
|
211
|
+
})
|
|
212
|
+
})
|