@bendyline/squisq-react 2.0.1 → 2.2.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/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +6 -0
- package/dist/index.d.ts +101 -96
- package/dist/index.js +1483 -782
- package/dist/squisq-player.full.global.js +4165 -0
- package/dist/squisq-player.global.js +561 -168
- package/dist/standalone-source.d.ts +1 -0
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +64 -0
- package/package.json +8 -4
- package/dist/index.js.map +0 -1
- package/dist/squisq-player.css +0 -2
- package/dist/squisq-player.css.map +0 -1
- package/dist/squisq-player.global.js.map +0 -1
- package/src/BlockRenderer.tsx +0 -194
- package/src/CaptionOverlay.tsx +0 -111
- package/src/DocControlsBottom.tsx +0 -109
- package/src/DocControlsOverlay.tsx +0 -190
- package/src/DocControlsSidebar.tsx +0 -113
- package/src/DocControlsSlideshow.tsx +0 -360
- package/src/DocPlayer.tsx +0 -1528
- package/src/DocPlayerWithSidebar.tsx +0 -155
- package/src/DocProgressBar.tsx +0 -257
- package/src/InlineAudioPlayer.tsx +0 -46
- package/src/InlineVideoPlayer.tsx +0 -70
- package/src/LinearDocView.tsx +0 -569
- package/src/MarkdownRenderer.tsx +0 -624
- package/src/MediaClipLayer.tsx +0 -145
- package/src/SocialCaptionOverlay.tsx +0 -255
- package/src/__tests__/BlockRenderer.test.tsx +0 -235
- package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
- package/src/__tests__/DocPlayer.test.tsx +0 -635
- package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
- package/src/__tests__/DocProgressBar.test.tsx +0 -102
- package/src/__tests__/JsonView.test.tsx +0 -111
- package/src/__tests__/LinearDocView.test.tsx +0 -426
- package/src/__tests__/MapLayer.test.tsx +0 -63
- package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
- package/src/__tests__/MediaClipLayer.test.tsx +0 -70
- package/src/__tests__/MediaContext.test.tsx +0 -51
- package/src/__tests__/PathLayer.test.tsx +0 -84
- package/src/__tests__/TableLayer.test.tsx +0 -142
- package/src/__tests__/VideoLayer.test.tsx +0 -94
- package/src/__tests__/exports.test.ts +0 -55
- package/src/__tests__/fillStyle.test.tsx +0 -160
- package/src/__tests__/standaloneEntry.test.tsx +0 -103
- package/src/__tests__/transitionStyles.test.ts +0 -125
- package/src/__tests__/useAudioSync.test.ts +0 -49
- package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
- package/src/__tests__/useJsonViewTokens.test.ts +0 -41
- package/src/__tests__/useSlideSwipe.test.ts +0 -81
- package/src/__tests__/useViewportOrientation.test.ts +0 -22
- package/src/hooks/AudioController.ts +0 -114
- package/src/hooks/MediaContext.tsx +0 -97
- package/src/hooks/index.ts +0 -11
- package/src/hooks/useAudioSync.ts +0 -456
- package/src/hooks/useAutoSurface.ts +0 -33
- package/src/hooks/useDocPlayback.ts +0 -265
- package/src/hooks/useMediaSchedule.ts +0 -39
- package/src/hooks/useSlideSwipe.ts +0 -265
- package/src/hooks/useViewportOrientation.ts +0 -115
- package/src/index.ts +0 -70
- package/src/jsonView/JsonView.tsx +0 -51
- package/src/jsonView/RenderNode.tsx +0 -51
- package/src/jsonView/index.ts +0 -2
- package/src/jsonView/json-view.css +0 -206
- package/src/jsonView/useJsonViewTokens.ts +0 -32
- package/src/jsonView/viewers.tsx +0 -343
- package/src/layers/ImageLayer.tsx +0 -297
- package/src/layers/MapLayer.tsx +0 -185
- package/src/layers/PathLayer.tsx +0 -155
- package/src/layers/ShapeLayer.tsx +0 -158
- package/src/layers/TableLayer.tsx +0 -129
- package/src/layers/TextLayer.tsx +0 -571
- package/src/layers/TreeLayer.tsx +0 -167
- package/src/layers/VideoLayer.tsx +0 -171
- package/src/layers/index.ts +0 -6
- package/src/standalone-entry.tsx +0 -337
- package/src/standalone-source.d.ts +0 -10
- package/src/styles/doc-animations.css +0 -2431
- package/src/styles/index.css +0 -7
- package/src/types.ts +0 -183
- package/src/utils/animationUtils.ts +0 -13
- package/src/utils/fillStyle.tsx +0 -148
- package/src/utils/layerUtils.ts +0 -42
- package/src/utils/mapTileUtils.ts +0 -375
|
@@ -1,635 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import { DocPlayer } from '../DocPlayer';
|
|
4
|
-
import type { Doc } from '@bendyline/squisq/schemas';
|
|
5
|
-
import type { AudioController } from '../hooks/AudioController';
|
|
6
|
-
import type { SquisqRenderAPI } from '../types';
|
|
7
|
-
|
|
8
|
-
function minimalDoc(): Doc {
|
|
9
|
-
return {
|
|
10
|
-
articleId: 'smoke',
|
|
11
|
-
duration: 5,
|
|
12
|
-
blocks: [{ id: 'b1', startTime: 0, duration: 5, audioSegment: 0, layers: [] }],
|
|
13
|
-
audio: { segments: [] },
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function docWithCover(): Doc {
|
|
18
|
-
return {
|
|
19
|
-
...minimalDoc(),
|
|
20
|
-
startBlock: {
|
|
21
|
-
title: 'Managed Cover',
|
|
22
|
-
subtitle: 'Generated by Squisq',
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function docWithTransition(): Doc {
|
|
28
|
-
return {
|
|
29
|
-
articleId: 'transition-doc',
|
|
30
|
-
duration: 10,
|
|
31
|
-
blocks: [
|
|
32
|
-
{ id: 'first', startTime: 0, duration: 5, audioSegment: 0, layers: [] },
|
|
33
|
-
{
|
|
34
|
-
id: 'second',
|
|
35
|
-
startTime: 5,
|
|
36
|
-
duration: 5,
|
|
37
|
-
audioSegment: 0,
|
|
38
|
-
transition: { type: 'fade', duration: 1 },
|
|
39
|
-
layers: [
|
|
40
|
-
{
|
|
41
|
-
type: 'text',
|
|
42
|
-
id: 'animated-title',
|
|
43
|
-
content: { text: 'Second slide', style: { fontSize: 48, color: '#fff' } },
|
|
44
|
-
position: { x: 100, y: 100 },
|
|
45
|
-
animation: { type: 'fadeIn', duration: 1 },
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
audio: { segments: [] },
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function docWithThreeSlides(): Doc {
|
|
55
|
-
return {
|
|
56
|
-
articleId: 'three-slide-transition-doc',
|
|
57
|
-
duration: 15,
|
|
58
|
-
blocks: [
|
|
59
|
-
{ id: 'first', startTime: 0, duration: 5, audioSegment: 0, layers: [] },
|
|
60
|
-
{ id: 'second', startTime: 5, duration: 5, audioSegment: 0, layers: [] },
|
|
61
|
-
{
|
|
62
|
-
id: 'third',
|
|
63
|
-
startTime: 10,
|
|
64
|
-
duration: 5,
|
|
65
|
-
audioSegment: 0,
|
|
66
|
-
transition: { type: 'fade', duration: 1 },
|
|
67
|
-
layers: [],
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
audio: { segments: [] },
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function controller(overrides: Partial<AudioController> = {}): AudioController {
|
|
75
|
-
return {
|
|
76
|
-
currentTime: 0,
|
|
77
|
-
isPlaying: false,
|
|
78
|
-
currentSegment: 0,
|
|
79
|
-
totalDuration: 5,
|
|
80
|
-
isEnded: false,
|
|
81
|
-
isReady: true,
|
|
82
|
-
isAvailable: true,
|
|
83
|
-
play: vi.fn(async () => {}),
|
|
84
|
-
pause: vi.fn(async () => {}),
|
|
85
|
-
toggle: vi.fn(async () => {}),
|
|
86
|
-
seekTo: vi.fn(async () => {}),
|
|
87
|
-
skipToSegment: vi.fn(async () => {}),
|
|
88
|
-
restart: vi.fn(async () => {}),
|
|
89
|
-
...overrides,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function pointerEvent(type: string, clientX: number): MouseEvent {
|
|
94
|
-
const event = new MouseEvent(type, { bubbles: true, cancelable: true, button: 0, clientX });
|
|
95
|
-
Object.defineProperties(event, {
|
|
96
|
-
pointerId: { value: 1 },
|
|
97
|
-
pointerType: { value: 'touch' },
|
|
98
|
-
});
|
|
99
|
-
return event;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
describe('DocPlayer smoke test', () => {
|
|
103
|
-
beforeAll(() => {
|
|
104
|
-
vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
afterAll(() => {
|
|
108
|
-
vi.restoreAllMocks();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('renders without crashing in video mode (default)', () => {
|
|
112
|
-
const { container } = render(<DocPlayer doc={minimalDoc()} basePath="/test" />);
|
|
113
|
-
expect(container.firstChild).toBeTruthy();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('renders without crashing in slideshow mode', () => {
|
|
117
|
-
const { container } = render(
|
|
118
|
-
<DocPlayer doc={minimalDoc()} basePath="/test" displayMode="slideshow" />,
|
|
119
|
-
);
|
|
120
|
-
expect(container.firstChild).toBeTruthy();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('lists block summaries and jumps to a selected slideshow block', () => {
|
|
124
|
-
const audioController = controller({ totalDuration: 15 });
|
|
125
|
-
render(
|
|
126
|
-
<DocPlayer
|
|
127
|
-
doc={docWithThreeSlides()}
|
|
128
|
-
audioController={audioController}
|
|
129
|
-
displayMode="slideshow"
|
|
130
|
-
/>,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
fireEvent.click(screen.getByTestId('slide-counter'));
|
|
134
|
-
expect(screen.getByTestId('slide-picker-item-0').textContent).toContain('1First');
|
|
135
|
-
expect(screen.getByTestId('slide-picker-item-2').textContent).toContain('3Third');
|
|
136
|
-
|
|
137
|
-
fireEvent.click(screen.getByTestId('slide-picker-item-2'));
|
|
138
|
-
expect(audioController.seekTo).toHaveBeenCalledWith(10);
|
|
139
|
-
expect(screen.queryByTestId('slide-picker')).toBeNull();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('uses global left and right arrows to change slides without focusing the player', () => {
|
|
143
|
-
const audioController = controller({ currentTime: 5, totalDuration: 15 });
|
|
144
|
-
render(
|
|
145
|
-
<DocPlayer
|
|
146
|
-
doc={docWithThreeSlides()}
|
|
147
|
-
audioController={audioController}
|
|
148
|
-
displayMode="slideshow"
|
|
149
|
-
globalKeyboardShortcuts
|
|
150
|
-
/>,
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
fireEvent.keyDown(document, { key: 'ArrowLeft' });
|
|
154
|
-
fireEvent.keyDown(document, { key: 'ArrowRight' });
|
|
155
|
-
|
|
156
|
-
expect(audioController.seekTo).toHaveBeenNthCalledWith(1, 0);
|
|
157
|
-
expect(audioController.seekTo).toHaveBeenNthCalledWith(2, 10);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('globally opens the slide picker with ArrowUp instead of changing slides', () => {
|
|
161
|
-
const audioController = controller({ currentTime: 5, totalDuration: 15 });
|
|
162
|
-
render(
|
|
163
|
-
<DocPlayer
|
|
164
|
-
doc={docWithThreeSlides()}
|
|
165
|
-
audioController={audioController}
|
|
166
|
-
displayMode="slideshow"
|
|
167
|
-
globalKeyboardShortcuts
|
|
168
|
-
/>,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
fireEvent.keyDown(document, { key: 'ArrowUp' });
|
|
172
|
-
|
|
173
|
-
expect(screen.getByTestId('slide-picker')).toBeTruthy();
|
|
174
|
-
expect(audioController.seekTo).not.toHaveBeenCalled();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('globally seeks video with left and right arrows', () => {
|
|
178
|
-
const audioController = controller({ currentTime: 20, totalDuration: 60 });
|
|
179
|
-
render(
|
|
180
|
-
<DocPlayer
|
|
181
|
-
doc={docWithThreeSlides()}
|
|
182
|
-
audioController={audioController}
|
|
183
|
-
globalKeyboardShortcuts
|
|
184
|
-
/>,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
fireEvent.keyDown(document, { key: 'ArrowRight' });
|
|
188
|
-
fireEvent.keyDown(document, { key: 'ArrowLeft' });
|
|
189
|
-
|
|
190
|
-
expect(audioController.seekTo).toHaveBeenNthCalledWith(1, 30);
|
|
191
|
-
expect(audioController.seekTo).toHaveBeenNthCalledWith(2, 10);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('handles a focused-player arrow only once when global shortcuts are enabled', () => {
|
|
195
|
-
const audioController = controller({ currentTime: 5, totalDuration: 15 });
|
|
196
|
-
const { container } = render(
|
|
197
|
-
<DocPlayer
|
|
198
|
-
doc={docWithThreeSlides()}
|
|
199
|
-
audioController={audioController}
|
|
200
|
-
displayMode="slideshow"
|
|
201
|
-
globalKeyboardShortcuts
|
|
202
|
-
/>,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
fireEvent.keyDown(container.querySelector<HTMLElement>('.doc-player')!, {
|
|
206
|
-
key: 'ArrowRight',
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
expect(audioController.seekTo).toHaveBeenCalledTimes(1);
|
|
210
|
-
expect(audioController.seekTo).toHaveBeenCalledWith(10);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('does not globally intercept arrows from editable fields', () => {
|
|
214
|
-
const audioController = controller({ currentTime: 20, totalDuration: 60 });
|
|
215
|
-
render(
|
|
216
|
-
<>
|
|
217
|
-
<input aria-label="Editable field" />
|
|
218
|
-
<DocPlayer
|
|
219
|
-
doc={docWithThreeSlides()}
|
|
220
|
-
audioController={audioController}
|
|
221
|
-
globalKeyboardShortcuts
|
|
222
|
-
/>
|
|
223
|
-
</>,
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
fireEvent.keyDown(screen.getByRole('textbox', { name: 'Editable field' }), {
|
|
227
|
-
key: 'ArrowRight',
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
expect(audioController.seekTo).not.toHaveBeenCalled();
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('shows the managed cover as the first slideshow entry by default', async () => {
|
|
234
|
-
const { container } = render(
|
|
235
|
-
<DocPlayer doc={docWithCover()} basePath="/test" displayMode="slideshow" />,
|
|
236
|
-
);
|
|
237
|
-
await waitFor(() => expect(container.textContent).toContain('Managed Cover'));
|
|
238
|
-
expect(screen.getByTestId('slide-counter').textContent).toBe('Cover');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('can suppress the managed cover slide', () => {
|
|
242
|
-
const { container } = render(
|
|
243
|
-
<DocPlayer
|
|
244
|
-
doc={docWithCover()}
|
|
245
|
-
basePath="/test"
|
|
246
|
-
displayMode="slideshow"
|
|
247
|
-
showCoverSlide={false}
|
|
248
|
-
/>,
|
|
249
|
-
);
|
|
250
|
-
expect(container.textContent).not.toContain('Managed Cover');
|
|
251
|
-
expect(screen.getByTestId('slide-counter').textContent).toBe('1 / 1');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('renders the managed cover without layer animation classes when animations are disabled', async () => {
|
|
255
|
-
const { container } = render(
|
|
256
|
-
<DocPlayer
|
|
257
|
-
doc={docWithCover()}
|
|
258
|
-
basePath="/test"
|
|
259
|
-
displayMode="slideshow"
|
|
260
|
-
animationsEnabled={false}
|
|
261
|
-
/>,
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
await waitFor(() => expect(container.textContent).toContain('Managed Cover'));
|
|
265
|
-
expect(container.querySelector('[class*="anim-"]')).toBeNull();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('uses static cuts without an outgoing slide while preserving timeline changes', () => {
|
|
269
|
-
const doc = docWithTransition();
|
|
270
|
-
const onRenderAPIReady = vi.fn();
|
|
271
|
-
const { container, rerender } = render(
|
|
272
|
-
<DocPlayer
|
|
273
|
-
doc={doc}
|
|
274
|
-
audioController={controller({ currentTime: 0, totalDuration: 10 })}
|
|
275
|
-
showControls={false}
|
|
276
|
-
renderMode
|
|
277
|
-
animationsEnabled={false}
|
|
278
|
-
onRenderAPIReady={onRenderAPIReady}
|
|
279
|
-
/>,
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
rerender(
|
|
283
|
-
<DocPlayer
|
|
284
|
-
doc={doc}
|
|
285
|
-
audioController={controller({ currentTime: 5.1, totalDuration: 10 })}
|
|
286
|
-
showControls={false}
|
|
287
|
-
renderMode
|
|
288
|
-
animationsEnabled={false}
|
|
289
|
-
onRenderAPIReady={onRenderAPIReady}
|
|
290
|
-
/>,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
expect(container.querySelector('.doc-player__block--previous')).toBeNull();
|
|
294
|
-
expect(
|
|
295
|
-
container.querySelector('.doc-player__block--active [data-block-id="second"]'),
|
|
296
|
-
).toBeTruthy();
|
|
297
|
-
expect(container.querySelector('.transition-fade-enter')).toBeNull();
|
|
298
|
-
expect(container.querySelector('[class*="anim-"]')).toBeNull();
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('keeps a swiped-to slide entrance transition without remounting the swiped-away slide', async () => {
|
|
302
|
-
const doc = docWithThreeSlides();
|
|
303
|
-
const secondSlideController = controller({ currentTime: 5, totalDuration: 15 });
|
|
304
|
-
const { container, rerender } = render(
|
|
305
|
-
<DocPlayer
|
|
306
|
-
doc={doc}
|
|
307
|
-
audioController={secondSlideController}
|
|
308
|
-
displayMode="slideshow"
|
|
309
|
-
showControls={false}
|
|
310
|
-
/>,
|
|
311
|
-
);
|
|
312
|
-
const player = container.querySelector<HTMLElement>('.doc-player');
|
|
313
|
-
expect(player).not.toBeNull();
|
|
314
|
-
vi.spyOn(player!, 'getBoundingClientRect').mockReturnValue({
|
|
315
|
-
width: 1000,
|
|
316
|
-
height: 600,
|
|
317
|
-
x: 0,
|
|
318
|
-
y: 0,
|
|
319
|
-
top: 0,
|
|
320
|
-
right: 1000,
|
|
321
|
-
bottom: 600,
|
|
322
|
-
left: 0,
|
|
323
|
-
toJSON: () => ({}),
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
fireEvent(player!, pointerEvent('pointerdown', 800));
|
|
327
|
-
expect(player!.classList.contains('doc-player--grabbing')).toBe(true);
|
|
328
|
-
fireEvent(window, pointerEvent('pointermove', 400));
|
|
329
|
-
expect(
|
|
330
|
-
container.querySelector<HTMLElement>('.doc-player__block--active')?.style.transform,
|
|
331
|
-
).toBe('translateX(-400px)');
|
|
332
|
-
fireEvent(window, pointerEvent('pointerup', 400));
|
|
333
|
-
|
|
334
|
-
await waitFor(() => expect(secondSlideController.seekTo).toHaveBeenCalledWith(10), {
|
|
335
|
-
timeout: 1000,
|
|
336
|
-
});
|
|
337
|
-
rerender(
|
|
338
|
-
<DocPlayer
|
|
339
|
-
doc={doc}
|
|
340
|
-
audioController={controller({ currentTime: 10, totalDuration: 15 })}
|
|
341
|
-
displayMode="slideshow"
|
|
342
|
-
showControls={false}
|
|
343
|
-
/>,
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
expect(
|
|
347
|
-
container.querySelector(
|
|
348
|
-
'.doc-player__block--active [data-block-id="third"].transition-fade-enter',
|
|
349
|
-
),
|
|
350
|
-
).toBeTruthy();
|
|
351
|
-
expect(container.querySelector('.doc-player__block--previous')).toBeNull();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('keeps outgoing slide context for ordinary slideshow button navigation', () => {
|
|
355
|
-
const doc = docWithThreeSlides();
|
|
356
|
-
const secondSlideController = controller({ currentTime: 5, totalDuration: 15 });
|
|
357
|
-
const { container, rerender } = render(
|
|
358
|
-
<DocPlayer doc={doc} audioController={secondSlideController} displayMode="slideshow" />,
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
fireEvent.click(screen.getByTestId('slide-next'));
|
|
362
|
-
expect(secondSlideController.seekTo).toHaveBeenCalledWith(10);
|
|
363
|
-
rerender(
|
|
364
|
-
<DocPlayer
|
|
365
|
-
doc={doc}
|
|
366
|
-
audioController={controller({ currentTime: 10, totalDuration: 15 })}
|
|
367
|
-
displayMode="slideshow"
|
|
368
|
-
/>,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
expect(
|
|
372
|
-
container.querySelector('.doc-player__block--previous [data-block-id="second"]'),
|
|
373
|
-
).toBeTruthy();
|
|
374
|
-
expect(
|
|
375
|
-
container.querySelector(
|
|
376
|
-
'.doc-player__block--active [data-block-id="third"].transition-fade-enter',
|
|
377
|
-
),
|
|
378
|
-
).toBeTruthy();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('advances from slideshow cover to slide 1', async () => {
|
|
382
|
-
const { container } = render(
|
|
383
|
-
<DocPlayer doc={docWithCover()} basePath="/test" displayMode="slideshow" />,
|
|
384
|
-
);
|
|
385
|
-
await waitFor(() => expect(screen.getByTestId('slide-counter').textContent).toBe('Cover'));
|
|
386
|
-
fireEvent.click(screen.getByTestId('slide-next'));
|
|
387
|
-
await waitFor(() => expect(screen.getByTestId('slide-counter').textContent).toBe('1 / 1'));
|
|
388
|
-
expect(container.textContent).not.toContain('Managed Cover');
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('accepts controlled cover visibility for a synchronized audience mirror', async () => {
|
|
392
|
-
const states: Array<{ isCoverVisible?: boolean }> = [];
|
|
393
|
-
const { container, rerender } = render(
|
|
394
|
-
<DocPlayer
|
|
395
|
-
doc={docWithCover()}
|
|
396
|
-
audioController={controller()}
|
|
397
|
-
displayMode="slideshow"
|
|
398
|
-
showControls={false}
|
|
399
|
-
coverVisible={false}
|
|
400
|
-
onPlaybackStateChange={(state) => states.push(state)}
|
|
401
|
-
/>,
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
await waitFor(() => expect(container.textContent).not.toContain('Managed Cover'));
|
|
405
|
-
expect(states[states.length - 1]?.isCoverVisible).toBe(false);
|
|
406
|
-
expect(screen.queryByTestId('slide-counter')).toBeNull();
|
|
407
|
-
|
|
408
|
-
rerender(
|
|
409
|
-
<DocPlayer
|
|
410
|
-
doc={docWithCover()}
|
|
411
|
-
audioController={controller()}
|
|
412
|
-
displayMode="slideshow"
|
|
413
|
-
showControls={false}
|
|
414
|
-
coverVisible
|
|
415
|
-
onPlaybackStateChange={(state) => states.push(state)}
|
|
416
|
-
/>,
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
await waitFor(() => expect(container.textContent).toContain('Managed Cover'));
|
|
420
|
-
expect(states[states.length - 1]?.isCoverVisible).toBe(true);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('reinitializes cover state when a new document replaces the old one', async () => {
|
|
424
|
-
const { container, rerender } = render(
|
|
425
|
-
<DocPlayer doc={docWithCover()} basePath="/test" displayMode="slideshow" />,
|
|
426
|
-
);
|
|
427
|
-
fireEvent.click(screen.getByTestId('slide-next'));
|
|
428
|
-
await waitFor(() => expect(screen.getByTestId('slide-counter').textContent).toBe('1 / 1'));
|
|
429
|
-
rerender(
|
|
430
|
-
<DocPlayer
|
|
431
|
-
doc={{
|
|
432
|
-
...docWithCover(),
|
|
433
|
-
articleId: 'replacement',
|
|
434
|
-
startBlock: { title: 'Replacement Cover' },
|
|
435
|
-
}}
|
|
436
|
-
basePath="/test"
|
|
437
|
-
displayMode="slideshow"
|
|
438
|
-
/>,
|
|
439
|
-
);
|
|
440
|
-
await waitFor(() => expect(container.textContent).toContain('Replacement Cover'));
|
|
441
|
-
expect(screen.getByTestId('slide-counter').textContent).toBe('Cover');
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it('renders without crashing in linear mode', () => {
|
|
445
|
-
const { container } = render(
|
|
446
|
-
<DocPlayer doc={minimalDoc()} basePath="/test" displayMode="linear" />,
|
|
447
|
-
);
|
|
448
|
-
expect(container.firstChild).toBeTruthy();
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it('exposes playback controls via onControlsReady', () => {
|
|
452
|
-
let controls: { play: () => void; pause: () => void } | null = null;
|
|
453
|
-
render(
|
|
454
|
-
<DocPlayer
|
|
455
|
-
doc={minimalDoc()}
|
|
456
|
-
basePath="/test"
|
|
457
|
-
showControls={false}
|
|
458
|
-
onControlsReady={(c) => {
|
|
459
|
-
controls = c;
|
|
460
|
-
}}
|
|
461
|
-
/>,
|
|
462
|
-
);
|
|
463
|
-
expect(controls).not.toBeNull();
|
|
464
|
-
expect(typeof controls!.play).toBe('function');
|
|
465
|
-
expect(typeof controls!.pause).toBe('function');
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('handles keyboard shortcuts only in the focused player instance', () => {
|
|
469
|
-
const first = controller();
|
|
470
|
-
const second = controller();
|
|
471
|
-
const { container } = render(
|
|
472
|
-
<>
|
|
473
|
-
<DocPlayer doc={minimalDoc()} audioController={first} showControls={false} />
|
|
474
|
-
<DocPlayer doc={minimalDoc()} audioController={second} showControls={false} />
|
|
475
|
-
</>,
|
|
476
|
-
);
|
|
477
|
-
const players = container.querySelectorAll<HTMLElement>('.doc-player');
|
|
478
|
-
players[1].focus();
|
|
479
|
-
fireEvent.keyDown(players[1], { key: ' ' });
|
|
480
|
-
expect(second.toggle).toHaveBeenCalledOnce();
|
|
481
|
-
expect(first.toggle).not.toHaveBeenCalled();
|
|
482
|
-
|
|
483
|
-
const editable = document.createElement('div');
|
|
484
|
-
editable.setAttribute('contenteditable', 'true');
|
|
485
|
-
players[1].append(editable);
|
|
486
|
-
fireEvent.keyDown(editable, { key: ' ' });
|
|
487
|
-
expect(second.toggle).toHaveBeenCalledOnce();
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it('keeps render APIs scoped to their player callbacks during sibling cleanup', () => {
|
|
491
|
-
const firstEvents: Array<SquisqRenderAPI | null> = [];
|
|
492
|
-
const secondEvents: Array<SquisqRenderAPI | null> = [];
|
|
493
|
-
const one = render(
|
|
494
|
-
<DocPlayer
|
|
495
|
-
doc={minimalDoc()}
|
|
496
|
-
renderMode
|
|
497
|
-
audioController={controller()}
|
|
498
|
-
onRenderAPIReady={(api) => firstEvents.push(api)}
|
|
499
|
-
/>,
|
|
500
|
-
);
|
|
501
|
-
render(
|
|
502
|
-
<DocPlayer
|
|
503
|
-
doc={minimalDoc()}
|
|
504
|
-
renderMode
|
|
505
|
-
audioController={controller()}
|
|
506
|
-
onRenderAPIReady={(api) => secondEvents.push(api)}
|
|
507
|
-
/>,
|
|
508
|
-
);
|
|
509
|
-
const secondAPI = secondEvents[secondEvents.length - 1];
|
|
510
|
-
expect(firstEvents[firstEvents.length - 1]).not.toBeNull();
|
|
511
|
-
expect(secondAPI).not.toBeNull();
|
|
512
|
-
|
|
513
|
-
one.unmount();
|
|
514
|
-
|
|
515
|
-
expect(firstEvents[firstEvents.length - 1]).toBeNull();
|
|
516
|
-
expect(secondEvents[secondEvents.length - 1]).toBe(secondAPI);
|
|
517
|
-
expect(typeof secondAPI?.seekTo).toBe('function');
|
|
518
|
-
expect('seekTo' in window).toBe(false);
|
|
519
|
-
expect('getDuration' in window).toBe(false);
|
|
520
|
-
expect('squisqActivePlayerId' in window).toBe(false);
|
|
521
|
-
expect('squisqPlayers' in window).toBe(false);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it('delivers and cleans up an instance render API through the callback', () => {
|
|
525
|
-
const observed: Array<SquisqRenderAPI | null> = [];
|
|
526
|
-
const { unmount } = render(
|
|
527
|
-
<DocPlayer
|
|
528
|
-
doc={minimalDoc()}
|
|
529
|
-
renderMode
|
|
530
|
-
audioController={controller()}
|
|
531
|
-
onRenderAPIReady={(api) => observed.push(api)}
|
|
532
|
-
/>,
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
expect(observed[observed.length - 1]?.getDuration()).toBeGreaterThan(0);
|
|
536
|
-
unmount();
|
|
537
|
-
expect(observed[observed.length - 1]).toBeNull();
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
it('keeps the render API identity stable while dispatching to the latest controller', async () => {
|
|
541
|
-
const firstController = controller({ currentSegment: 0 });
|
|
542
|
-
const secondController = controller({ currentSegment: 1 });
|
|
543
|
-
const observed: Array<SquisqRenderAPI | null> = [];
|
|
544
|
-
const onRenderAPIReady = (api: SquisqRenderAPI | null) => observed.push(api);
|
|
545
|
-
const { rerender } = render(
|
|
546
|
-
<DocPlayer
|
|
547
|
-
doc={minimalDoc()}
|
|
548
|
-
renderMode
|
|
549
|
-
audioController={firstController}
|
|
550
|
-
onRenderAPIReady={onRenderAPIReady}
|
|
551
|
-
/>,
|
|
552
|
-
);
|
|
553
|
-
const api = observed[observed.length - 1];
|
|
554
|
-
expect(api).not.toBeNull();
|
|
555
|
-
|
|
556
|
-
rerender(
|
|
557
|
-
<DocPlayer
|
|
558
|
-
doc={minimalDoc()}
|
|
559
|
-
renderMode
|
|
560
|
-
audioController={secondController}
|
|
561
|
-
onRenderAPIReady={onRenderAPIReady}
|
|
562
|
-
/>,
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
expect(observed[observed.length - 1]).toBe(api);
|
|
566
|
-
await api!.seekTo(3.25);
|
|
567
|
-
expect(secondController.seekTo).toHaveBeenCalledWith(3.25);
|
|
568
|
-
expect(firstController.seekTo).not.toHaveBeenCalled();
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it('refreshes instance render metadata when the doc prop changes', () => {
|
|
572
|
-
const firstDoc: Doc = { ...minimalDoc(), captions: { version: 1, phrases: [] } };
|
|
573
|
-
const observed: Array<SquisqRenderAPI | null> = [];
|
|
574
|
-
const onRenderAPIReady = (next: SquisqRenderAPI | null) => {
|
|
575
|
-
observed.push(next);
|
|
576
|
-
};
|
|
577
|
-
const { container, rerender } = render(
|
|
578
|
-
<DocPlayer
|
|
579
|
-
doc={firstDoc}
|
|
580
|
-
renderMode
|
|
581
|
-
audioController={controller()}
|
|
582
|
-
onRenderAPIReady={onRenderAPIReady}
|
|
583
|
-
/>,
|
|
584
|
-
);
|
|
585
|
-
expect(container.querySelector('.doc-player')).toBeTruthy();
|
|
586
|
-
const nextDoc: Doc = {
|
|
587
|
-
...minimalDoc(),
|
|
588
|
-
captions: {
|
|
589
|
-
version: 1,
|
|
590
|
-
phrases: [{ text: 'new caption', startTime: 0, endTime: 1, audioSegment: 0 }],
|
|
591
|
-
},
|
|
592
|
-
};
|
|
593
|
-
rerender(
|
|
594
|
-
<DocPlayer
|
|
595
|
-
doc={nextDoc}
|
|
596
|
-
renderMode
|
|
597
|
-
audioController={controller()}
|
|
598
|
-
onRenderAPIReady={onRenderAPIReady}
|
|
599
|
-
/>,
|
|
600
|
-
);
|
|
601
|
-
expect(observed[observed.length - 1]?.getCaptions()).toEqual([
|
|
602
|
-
{ text: 'new caption', startTime: 0, endTime: 1 },
|
|
603
|
-
]);
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
describe('DocPlayer front door (doc / markdown resolution)', () => {
|
|
608
|
-
it('renders a doc built from the markdown prop', () => {
|
|
609
|
-
const { container } = render(
|
|
610
|
-
<DocPlayer markdown={'# Hello From Markdown\n\nBody text here.'} displayMode="linear" />,
|
|
611
|
-
);
|
|
612
|
-
expect(container.querySelector('.doc-player')).toBeTruthy();
|
|
613
|
-
expect(container.textContent).toContain('Hello From Markdown');
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it('doc wins over markdown when both are provided', () => {
|
|
617
|
-
const { container } = render(
|
|
618
|
-
<DocPlayer doc={minimalDoc()} markdown="# Markdown Loses" displayMode="linear" />,
|
|
619
|
-
);
|
|
620
|
-
expect(container.querySelector('.doc-player')).toBeTruthy();
|
|
621
|
-
expect(container.textContent).not.toContain('Markdown Loses');
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it('renders an empty state without throwing when neither doc nor markdown is given', () => {
|
|
625
|
-
const { container } = render(<DocPlayer />);
|
|
626
|
-
const empty = container.querySelector('.doc-player--empty');
|
|
627
|
-
expect(empty).toBeTruthy();
|
|
628
|
-
expect(empty!.classList.contains('doc-player')).toBe(true);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it('defaults basePath when omitted', () => {
|
|
632
|
-
const { container } = render(<DocPlayer doc={minimalDoc()} />);
|
|
633
|
-
expect(container.querySelector('.doc-player')).toBeTruthy();
|
|
634
|
-
});
|
|
635
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Missing-stylesheet sentinel test.
|
|
3
|
-
*
|
|
4
|
-
* Lives in its own file so it owns a fresh module instance of DocPlayer
|
|
5
|
-
* (vitest isolates module registries per test file) — the sentinel warning
|
|
6
|
-
* is one-shot at module level, so this file's first mount deterministically
|
|
7
|
-
* observes it.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
11
|
-
import { render } from '@testing-library/react';
|
|
12
|
-
import { DocPlayer } from '../DocPlayer';
|
|
13
|
-
import type { Doc } from '@bendyline/squisq/schemas';
|
|
14
|
-
|
|
15
|
-
function minimalDoc(): Doc {
|
|
16
|
-
return {
|
|
17
|
-
articleId: 'sentinel',
|
|
18
|
-
duration: 5,
|
|
19
|
-
blocks: [{ id: 'b1', startTime: 0, duration: 5, audioSegment: 0, layers: [] }],
|
|
20
|
-
audio: { segments: [] },
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
vi.restoreAllMocks();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('DocPlayer missing-CSS sentinel', () => {
|
|
29
|
-
it('warns once (and only once) in dev when the stylesheet is not loaded', () => {
|
|
30
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
31
|
-
|
|
32
|
-
render(<DocPlayer doc={minimalDoc()} />);
|
|
33
|
-
const sentinelCalls = () =>
|
|
34
|
-
warnSpy.mock.calls.filter((c) => String(c[0]).includes('squisq-react/styles'));
|
|
35
|
-
expect(sentinelCalls().length).toBe(1);
|
|
36
|
-
|
|
37
|
-
// Second mount must not warn again — module-level one-shot.
|
|
38
|
-
render(<DocPlayer doc={minimalDoc()} />);
|
|
39
|
-
expect(sentinelCalls().length).toBe(1);
|
|
40
|
-
});
|
|
41
|
-
});
|