@financial-times/cp-content-pipeline-ui 7.7.0 → 7.8.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.
@@ -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
+ })