@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +6 -0
  4. package/dist/index.d.ts +101 -96
  5. package/dist/index.js +1483 -782
  6. package/dist/squisq-player.full.global.js +4165 -0
  7. package/dist/squisq-player.global.js +561 -168
  8. package/dist/standalone-source.d.ts +1 -0
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +64 -0
  11. package/package.json +8 -4
  12. package/dist/index.js.map +0 -1
  13. package/dist/squisq-player.css +0 -2
  14. package/dist/squisq-player.css.map +0 -1
  15. package/dist/squisq-player.global.js.map +0 -1
  16. package/src/BlockRenderer.tsx +0 -194
  17. package/src/CaptionOverlay.tsx +0 -111
  18. package/src/DocControlsBottom.tsx +0 -109
  19. package/src/DocControlsOverlay.tsx +0 -190
  20. package/src/DocControlsSidebar.tsx +0 -113
  21. package/src/DocControlsSlideshow.tsx +0 -360
  22. package/src/DocPlayer.tsx +0 -1528
  23. package/src/DocPlayerWithSidebar.tsx +0 -155
  24. package/src/DocProgressBar.tsx +0 -257
  25. package/src/InlineAudioPlayer.tsx +0 -46
  26. package/src/InlineVideoPlayer.tsx +0 -70
  27. package/src/LinearDocView.tsx +0 -569
  28. package/src/MarkdownRenderer.tsx +0 -624
  29. package/src/MediaClipLayer.tsx +0 -145
  30. package/src/SocialCaptionOverlay.tsx +0 -255
  31. package/src/__tests__/BlockRenderer.test.tsx +0 -235
  32. package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
  33. package/src/__tests__/DocPlayer.test.tsx +0 -635
  34. package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
  35. package/src/__tests__/DocProgressBar.test.tsx +0 -102
  36. package/src/__tests__/JsonView.test.tsx +0 -111
  37. package/src/__tests__/LinearDocView.test.tsx +0 -426
  38. package/src/__tests__/MapLayer.test.tsx +0 -63
  39. package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
  40. package/src/__tests__/MediaClipLayer.test.tsx +0 -70
  41. package/src/__tests__/MediaContext.test.tsx +0 -51
  42. package/src/__tests__/PathLayer.test.tsx +0 -84
  43. package/src/__tests__/TableLayer.test.tsx +0 -142
  44. package/src/__tests__/VideoLayer.test.tsx +0 -94
  45. package/src/__tests__/exports.test.ts +0 -55
  46. package/src/__tests__/fillStyle.test.tsx +0 -160
  47. package/src/__tests__/standaloneEntry.test.tsx +0 -103
  48. package/src/__tests__/transitionStyles.test.ts +0 -125
  49. package/src/__tests__/useAudioSync.test.ts +0 -49
  50. package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
  51. package/src/__tests__/useJsonViewTokens.test.ts +0 -41
  52. package/src/__tests__/useSlideSwipe.test.ts +0 -81
  53. package/src/__tests__/useViewportOrientation.test.ts +0 -22
  54. package/src/hooks/AudioController.ts +0 -114
  55. package/src/hooks/MediaContext.tsx +0 -97
  56. package/src/hooks/index.ts +0 -11
  57. package/src/hooks/useAudioSync.ts +0 -456
  58. package/src/hooks/useAutoSurface.ts +0 -33
  59. package/src/hooks/useDocPlayback.ts +0 -265
  60. package/src/hooks/useMediaSchedule.ts +0 -39
  61. package/src/hooks/useSlideSwipe.ts +0 -265
  62. package/src/hooks/useViewportOrientation.ts +0 -115
  63. package/src/index.ts +0 -70
  64. package/src/jsonView/JsonView.tsx +0 -51
  65. package/src/jsonView/RenderNode.tsx +0 -51
  66. package/src/jsonView/index.ts +0 -2
  67. package/src/jsonView/json-view.css +0 -206
  68. package/src/jsonView/useJsonViewTokens.ts +0 -32
  69. package/src/jsonView/viewers.tsx +0 -343
  70. package/src/layers/ImageLayer.tsx +0 -297
  71. package/src/layers/MapLayer.tsx +0 -185
  72. package/src/layers/PathLayer.tsx +0 -155
  73. package/src/layers/ShapeLayer.tsx +0 -158
  74. package/src/layers/TableLayer.tsx +0 -129
  75. package/src/layers/TextLayer.tsx +0 -571
  76. package/src/layers/TreeLayer.tsx +0 -167
  77. package/src/layers/VideoLayer.tsx +0 -171
  78. package/src/layers/index.ts +0 -6
  79. package/src/standalone-entry.tsx +0 -337
  80. package/src/standalone-source.d.ts +0 -10
  81. package/src/styles/doc-animations.css +0 -2431
  82. package/src/styles/index.css +0 -7
  83. package/src/types.ts +0 -183
  84. package/src/utils/animationUtils.ts +0 -13
  85. package/src/utils/fillStyle.tsx +0 -148
  86. package/src/utils/layerUtils.ts +0 -42
  87. package/src/utils/mapTileUtils.ts +0 -375
@@ -1,63 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { render, waitFor } from '@testing-library/react';
3
- import type { MapLayer as MapLayerSchema } from '@bendyline/squisq/schemas';
4
-
5
- const { composeMapImage } = vi.hoisted(() => ({
6
- composeMapImage: vi.fn(async () => 'data:image/png;base64,map'),
7
- }));
8
- vi.mock('../utils/mapTileUtils', () => ({ composeMapImage }));
9
-
10
- import { MapLayer } from '../layers/MapLayer';
11
-
12
- function layer(
13
- markers: MapLayerSchema['content']['markers'],
14
- showAttribution = true,
15
- ): MapLayerSchema {
16
- return {
17
- id: 'map',
18
- type: 'map',
19
- position: { x: 0, y: 0, width: 100, height: 100 },
20
- content: {
21
- center: { lat: 1, lng: 2 },
22
- zoom: 4,
23
- style: 'road',
24
- markers,
25
- showAttribution,
26
- },
27
- };
28
- }
29
-
30
- describe('MapLayer dependencies', () => {
31
- beforeEach(() => composeMapImage.mockClear());
32
-
33
- it('recomposes when markers or attribution change', async () => {
34
- const firstMarkers = [{ lat: 1, lng: 2, label: 'A' }];
35
- const { rerender } = render(
36
- <svg>
37
- <MapLayer
38
- layer={layer(firstMarkers)}
39
- basePath="."
40
- viewport={{ width: 100, height: 100 }}
41
- blockTime={0}
42
- />
43
- </svg>,
44
- );
45
- await waitFor(() => expect(composeMapImage).toHaveBeenCalledTimes(1));
46
-
47
- const secondMarkers = [{ lat: 3, lng: 4, label: 'B' }];
48
- rerender(
49
- <svg>
50
- <MapLayer
51
- layer={layer(secondMarkers, false)}
52
- basePath="."
53
- viewport={{ width: 100, height: 100 }}
54
- blockTime={0}
55
- />
56
- </svg>,
57
- );
58
- await waitFor(() => expect(composeMapImage).toHaveBeenCalledTimes(2));
59
- expect(composeMapImage).toHaveBeenLastCalledWith(
60
- expect.objectContaining({ markers: secondMarkers, showAttribution: false }),
61
- );
62
- });
63
- });
@@ -1,357 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from '@testing-library/react';
3
- import { MarkdownRenderer } from '../MarkdownRenderer';
4
- import {
5
- parseMarkdown,
6
- type MarkdownBlockNode,
7
- type MarkdownInlineNode,
8
- } from '@bendyline/squisq/markdown';
9
-
10
- // ── Helpers ────────────────────────────────────────────────────────
11
-
12
- function text(value: string): MarkdownInlineNode {
13
- return { type: 'text', value };
14
- }
15
-
16
- function paragraph(...children: MarkdownInlineNode[]): MarkdownBlockNode {
17
- return { type: 'paragraph', children };
18
- }
19
-
20
- function heading(
21
- depth: 1 | 2 | 3 | 4 | 5 | 6,
22
- ...children: MarkdownInlineNode[]
23
- ): MarkdownBlockNode {
24
- return { type: 'heading', depth, children };
25
- }
26
-
27
- function parseNodes(markdown: string): MarkdownBlockNode[] {
28
- return parseMarkdown(markdown).children;
29
- }
30
-
31
- // ── Tests ──────────────────────────────────────────────────────────
32
-
33
- describe('MarkdownRenderer', () => {
34
- it('renders null for empty nodes', () => {
35
- const { container } = render(<MarkdownRenderer nodes={[]} />);
36
- expect(container.innerHTML).toBe('');
37
- });
38
-
39
- it('renders a paragraph', () => {
40
- const { container } = render(<MarkdownRenderer nodes={[paragraph(text('Hello world'))]} />);
41
- const p = container.querySelector('p.squisq-md-p');
42
- expect(p).toBeTruthy();
43
- expect(p?.textContent).toBe('Hello world');
44
- });
45
-
46
- it('renders headings at correct depth', () => {
47
- const nodes: MarkdownBlockNode[] = [
48
- heading(1, text('Title')),
49
- heading(2, text('Subtitle')),
50
- heading(3, text('Section')),
51
- ];
52
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
53
- expect(container.querySelector('h1')?.textContent).toBe('Title');
54
- expect(container.querySelector('h2')?.textContent).toBe('Subtitle');
55
- expect(container.querySelector('h3')?.textContent).toBe('Section');
56
- });
57
-
58
- it('renders emphasis and strong inline', () => {
59
- const nodes: MarkdownBlockNode[] = [
60
- paragraph(text('normal '), { type: 'emphasis', children: [text('italic')] }, text(' and '), {
61
- type: 'strong',
62
- children: [text('bold')],
63
- }),
64
- ];
65
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
66
- expect(container.querySelector('em')?.textContent).toBe('italic');
67
- expect(container.querySelector('strong')?.textContent).toBe('bold');
68
- });
69
-
70
- it('renders inline code', () => {
71
- const nodes: MarkdownBlockNode[] = [
72
- paragraph(text('run '), { type: 'inlineCode', value: 'npm install' }),
73
- ];
74
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
75
- const code = container.querySelector('code.squisq-md-inline-code');
76
- expect(code?.textContent).toBe('npm install');
77
- });
78
-
79
- it('renders a link with target _blank', () => {
80
- const nodes: MarkdownBlockNode[] = [
81
- paragraph({
82
- type: 'link',
83
- url: 'https://example.com',
84
- title: 'Example',
85
- children: [text('click')],
86
- }),
87
- ];
88
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
89
- const a = container.querySelector('a.squisq-md-link') as HTMLAnchorElement;
90
- expect(a).toBeTruthy();
91
- expect(a.href).toContain('example.com');
92
- expect(a.target).toBe('_blank');
93
- expect(a.textContent).toBe('click');
94
- });
95
-
96
- it('renders unsafe links as inert text by default', () => {
97
- const { container } = render(
98
- <MarkdownRenderer nodes={parseNodes('[x](javascript:alert(1))')} />,
99
- );
100
- expect(container.querySelector('a.squisq-md-link')).toBeNull();
101
- expect(container.querySelector('.squisq-md-link--blocked')?.textContent).toBe('x');
102
- expect(container.innerHTML).not.toContain('javascript:');
103
- });
104
-
105
- it('linkSchemes allows a host scheme as a real anchor, never executable ones', () => {
106
- const nodes = parseNodes('[a](workspace-nav:src%2Fa.ts) [b](javascript:alert(1))');
107
- const blocked = render(<MarkdownRenderer nodes={nodes} />);
108
- expect(blocked.container.querySelector('a.squisq-md-link')).toBeNull();
109
-
110
- const allowed = render(<MarkdownRenderer nodes={nodes} linkSchemes={['workspace-nav']} />);
111
- const a = allowed.container.querySelector('a.squisq-md-link') as HTMLAnchorElement;
112
- expect(a?.getAttribute('href')).toBe('workspace-nav:src%2Fa.ts');
113
- // javascript: stays blocked even when a host lists it
114
- const evil = render(
115
- <MarkdownRenderer
116
- nodes={parseNodes('[b](javascript:alert(1))')}
117
- linkSchemes={['javascript']}
118
- />,
119
- );
120
- expect(evil.container.querySelector('a.squisq-md-link')).toBeNull();
121
- });
122
-
123
- it('renders an image', () => {
124
- const nodes: MarkdownBlockNode[] = [
125
- paragraph({
126
- type: 'image',
127
- url: '/cat.jpg',
128
- alt: 'A cat',
129
- }),
130
- ];
131
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
132
- const img = container.querySelector('img.squisq-md-image') as HTMLImageElement;
133
- expect(img).toBeTruthy();
134
- expect(img.alt).toBe('A cat');
135
- });
136
-
137
- it('renders an unordered list', () => {
138
- const nodes: MarkdownBlockNode[] = [
139
- {
140
- type: 'list',
141
- ordered: false,
142
- children: [
143
- { type: 'listItem', children: [paragraph(text('Item A'))] },
144
- { type: 'listItem', children: [paragraph(text('Item B'))] },
145
- ],
146
- },
147
- ];
148
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
149
- const ul = container.querySelector('ul.squisq-md-ul');
150
- expect(ul).toBeTruthy();
151
- const items = ul?.querySelectorAll('li');
152
- expect(items?.length).toBe(2);
153
- expect(items?.[0]?.textContent).toBe('Item A');
154
- });
155
-
156
- it('renders an ordered list with start number', () => {
157
- const nodes: MarkdownBlockNode[] = [
158
- {
159
- type: 'list',
160
- ordered: true,
161
- start: 3,
162
- children: [{ type: 'listItem', children: [paragraph(text('Third'))] }],
163
- },
164
- ];
165
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
166
- const ol = container.querySelector('ol.squisq-md-ol') as HTMLOListElement;
167
- expect(ol).toBeTruthy();
168
- expect(ol.start).toBe(3);
169
- });
170
-
171
- it('renders a task list item with checkbox', () => {
172
- const nodes: MarkdownBlockNode[] = [
173
- {
174
- type: 'list',
175
- ordered: false,
176
- children: [
177
- { type: 'listItem', checked: true, children: [paragraph(text('Done'))] },
178
- { type: 'listItem', checked: false, children: [paragraph(text('Todo'))] },
179
- ],
180
- },
181
- ];
182
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
183
- const checkboxes = container.querySelectorAll('input[type="checkbox"]');
184
- expect(checkboxes.length).toBe(2);
185
- expect((checkboxes[0] as HTMLInputElement).checked).toBe(true);
186
- expect((checkboxes[1] as HTMLInputElement).checked).toBe(false);
187
- });
188
-
189
- it('renders a code block', () => {
190
- const nodes: MarkdownBlockNode[] = [
191
- { type: 'code', lang: 'typescript', value: 'const x = 1;' },
192
- ];
193
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
194
- const pre = container.querySelector('pre.squisq-md-code-block');
195
- expect(pre).toBeTruthy();
196
- const code = pre?.querySelector('code.language-typescript');
197
- expect(code?.textContent).toBe('const x = 1;');
198
- });
199
-
200
- it('renders a blockquote', () => {
201
- const nodes: MarkdownBlockNode[] = [
202
- { type: 'blockquote', children: [paragraph(text('Quoted text'))] },
203
- ];
204
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
205
- const bq = container.querySelector('blockquote.squisq-md-blockquote');
206
- expect(bq).toBeTruthy();
207
- expect(bq?.textContent).toBe('Quoted text');
208
- });
209
-
210
- it('renders a thematic break', () => {
211
- const nodes: MarkdownBlockNode[] = [
212
- paragraph(text('Before')),
213
- { type: 'thematicBreak' },
214
- paragraph(text('After')),
215
- ];
216
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
217
- expect(container.querySelector('hr.squisq-md-hr')).toBeTruthy();
218
- });
219
-
220
- it('renders a table', () => {
221
- const nodes: MarkdownBlockNode[] = [
222
- {
223
- type: 'table',
224
- align: ['left', 'right'],
225
- children: [
226
- {
227
- type: 'tableRow',
228
- children: [
229
- { type: 'tableCell', isHeader: true, children: [text('Name')] },
230
- { type: 'tableCell', isHeader: true, children: [text('Value')] },
231
- ],
232
- },
233
- {
234
- type: 'tableRow',
235
- children: [
236
- { type: 'tableCell', children: [text('A')] },
237
- { type: 'tableCell', children: [text('1')] },
238
- ],
239
- },
240
- ],
241
- },
242
- ];
243
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
244
- const table = container.querySelector('table.squisq-md-table');
245
- expect(table).toBeTruthy();
246
- expect(table?.querySelectorAll('th').length).toBe(2);
247
- expect(table?.querySelectorAll('td').length).toBe(2);
248
- });
249
-
250
- it('renders strikethrough', () => {
251
- const nodes: MarkdownBlockNode[] = [paragraph({ type: 'delete', children: [text('removed')] })];
252
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
253
- const del = container.querySelector('del.squisq-md-del');
254
- expect(del?.textContent).toBe('removed');
255
- });
256
-
257
- it('renders a hard break', () => {
258
- const nodes: MarkdownBlockNode[] = [paragraph(text('line1'), { type: 'break' }, text('line2'))];
259
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
260
- expect(container.querySelector('br')).toBeTruthy();
261
- });
262
-
263
- it('applies custom className', () => {
264
- const { container } = render(
265
- <MarkdownRenderer nodes={[paragraph(text('test'))]} className="custom" />,
266
- );
267
- expect(container.querySelector('.squisq-md.custom')).toBeTruthy();
268
- });
269
-
270
- it('sanitizes raw HTML by default', () => {
271
- const nodes = parseNodes(
272
- '<div><img src="x.jpg" onerror="alert(1)"><script>alert(1)</script><span onclick="alert(1)">ok</span></div>',
273
- );
274
- const { container } = render(<MarkdownRenderer nodes={nodes} />);
275
-
276
- expect(container.querySelector('script')).toBeNull();
277
- expect(container.querySelector('img')?.getAttribute('src')).toBe('x.jpg');
278
- expect(container.querySelector('img')?.getAttribute('onerror')).toBeNull();
279
- expect(container.querySelector('span')?.getAttribute('onclick')).toBeNull();
280
- expect(container.textContent).toContain('ok');
281
- expect(container.textContent).not.toContain('alert(1)');
282
- });
283
-
284
- it('strips raw HTML when requested', () => {
285
- const { container } = render(
286
- <MarkdownRenderer nodes={parseNodes('<div><span>hidden</span></div>')} htmlPolicy="strip" />,
287
- );
288
- expect(container.textContent).not.toContain('hidden');
289
- expect(container.querySelector('span')).toBeNull();
290
- });
291
-
292
- it('preserves trusted HTML structure without executable attributes', () => {
293
- const { container } = render(
294
- <MarkdownRenderer
295
- nodes={parseNodes('<div><span class="trusted" onclick="alert(1)">ok</span></div>')}
296
- htmlPolicy="trusted"
297
- />,
298
- );
299
- const span = container.querySelector('span.trusted');
300
- expect(span?.getAttribute('onclick')).toBeNull();
301
- });
302
-
303
- it('blocks executable URLs under the trusted policy', () => {
304
- const { container } = render(
305
- <MarkdownRenderer
306
- nodes={parseNodes('<a href="javascript:alert(1)">bad</a><img src="javascript:x">')}
307
- htmlPolicy="trusted"
308
- />,
309
- );
310
- expect(container.querySelector('a')?.getAttribute('href')).toBeNull();
311
- expect(container.querySelector('img')?.getAttribute('src')).toBeNull();
312
- });
313
-
314
- // Host-affecting tags (style/script/…) must never reach the document.
315
- // A bare <style> applies page-wide (CSS isn't scoped outside shadow DOM
316
- // / iframes), so an embedded game's <style> in a chat message used to
317
- // restyle the whole app chrome. Guard it across every policy.
318
- it('drops a raw <style> by default so it cannot leak onto the host', () => {
319
- const { container } = render(
320
- <MarkdownRenderer
321
- nodes={parseNodes('<div><style>body{font-family:monospace}</style><p>hi</p></div>')}
322
- />,
323
- );
324
- expect(container.querySelector('style')).toBeNull();
325
- expect(container.textContent).toContain('hi');
326
- expect(container.textContent).not.toContain('font-family');
327
- });
328
-
329
- it('drops <style>/<script> even under the trusted policy, keeping safe content', () => {
330
- const { container } = render(
331
- <MarkdownRenderer
332
- nodes={parseNodes(
333
- '<div class="game"><style>body{font-family:monospace}</style><script>alert(1)</script><p>play</p></div>',
334
- )}
335
- htmlPolicy="trusted"
336
- />,
337
- );
338
- expect(container.querySelector('style')).toBeNull();
339
- expect(container.querySelector('script')).toBeNull();
340
- expect(container.querySelector('div.game')).toBeTruthy();
341
- expect(container.textContent).toContain('play');
342
- expect(container.textContent).not.toContain('font-family');
343
- });
344
-
345
- it('drops a raw <style> on the verbatim path when htmlChildren was not parsed', () => {
346
- // parseHtml:false leaves htmlChildren empty while rawHtml keeps the
347
- // markup — the raw-string backstop must still refuse the fast path.
348
- const node = {
349
- type: 'htmlBlock',
350
- rawHtml: '<style>body{font-family:monospace}</style><p>play</p>',
351
- htmlChildren: [],
352
- } as unknown as MarkdownBlockNode;
353
- const { container } = render(<MarkdownRenderer nodes={[node]} htmlPolicy="trusted" />);
354
- expect(container.querySelector('style')).toBeNull();
355
- expect(container.textContent).not.toContain('font-family');
356
- });
357
- });
@@ -1,70 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { act, render, waitFor } from '@testing-library/react';
3
- import type { MediaProvider, ScheduledClip } from '@bendyline/squisq/schemas';
4
- import { MediaClipLayer } from '../MediaClipLayer';
5
- import { MediaContext } from '../hooks/MediaContext';
6
-
7
- afterEach(() => vi.restoreAllMocks());
8
-
9
- describe('MediaClipLayer', () => {
10
- it('mutes scheduled audio when the player muted contract is enabled', () => {
11
- vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
12
- const clip: ScheduledClip = {
13
- id: 'narration',
14
- kind: 'audio',
15
- src: 'narration.mp3',
16
- absoluteStart: 0,
17
- absoluteEnd: 5,
18
- sourceIn: 0,
19
- anchor: 'document',
20
- };
21
- const { container } = render(
22
- <MediaClipLayer schedule={[clip]} currentTime={0} isPlaying={false} basePath="." muted />,
23
- );
24
- expect(container.querySelector('audio')?.muted).toBe(true);
25
- });
26
-
27
- it('restores a paused clip position after an async media URL resolves', async () => {
28
- vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
29
- let resolveUrl!: (url: string) => void;
30
- const provider = {
31
- resolveUrl: vi.fn(
32
- () =>
33
- new Promise<string>((resolve) => {
34
- resolveUrl = resolve;
35
- }),
36
- ),
37
- } as unknown as MediaProvider;
38
- const clip: ScheduledClip = {
39
- id: 'background-video',
40
- kind: 'video',
41
- src: 'background.mp4',
42
- absoluteStart: 0,
43
- absoluteEnd: 10,
44
- sourceIn: 2,
45
- anchor: 'document',
46
- };
47
-
48
- const { container } = render(
49
- <MediaContext.Provider value={provider}>
50
- <MediaClipLayer
51
- schedule={[clip]}
52
- currentTime={5}
53
- isPlaying={false}
54
- basePath="/media"
55
- muted
56
- />
57
- </MediaContext.Provider>,
58
- );
59
- const video = container.querySelector('video')!;
60
- await waitFor(() => expect(video.currentTime).toBe(7));
61
-
62
- // Replacing an HTMLMediaElement source resets its playback position in a
63
- // browser. Simulate that reset before the provider's blob URL arrives.
64
- video.currentTime = 0;
65
- await act(async () => resolveUrl('blob:resolved-background'));
66
-
67
- await waitFor(() => expect(video.getAttribute('src')).toBe('blob:resolved-background'));
68
- expect(video.currentTime).toBe(7);
69
- });
70
- });
@@ -1,51 +0,0 @@
1
- import { act, render, screen } from '@testing-library/react';
2
- import { describe, expect, it, vi } from 'vitest';
3
- import type { MediaProvider } from '@bendyline/squisq/schemas';
4
- import { MediaContext, useMediaUrl } from '../hooks/MediaContext';
5
-
6
- function deferred<T>() {
7
- let resolve!: (value: T) => void;
8
- let reject!: (reason?: unknown) => void;
9
- const promise = new Promise<T>((res, rej) => {
10
- resolve = res;
11
- reject = rej;
12
- });
13
- return { promise, resolve, reject };
14
- }
15
-
16
- function Probe({ path }: { path: string }) {
17
- return <span data-testid="url">{useMediaUrl(path, '.')}</span>;
18
- }
19
-
20
- describe('useMediaUrl', () => {
21
- it('clears a stale URL and consumes provider rejection', async () => {
22
- const first = deferred<string>();
23
- const second = deferred<string>();
24
- const resolveUrl = vi
25
- .fn()
26
- .mockReturnValueOnce(first.promise)
27
- .mockReturnValueOnce(second.promise);
28
- const provider = { resolveUrl } as unknown as MediaProvider;
29
- const { rerender } = render(
30
- <MediaContext.Provider value={provider}>
31
- <Probe path="first.png" />
32
- </MediaContext.Provider>,
33
- );
34
- expect(screen.getByTestId('url').textContent).toBe('./first.png');
35
-
36
- rerender(
37
- <MediaContext.Provider value={provider}>
38
- <Probe path="second.png" />
39
- </MediaContext.Provider>,
40
- );
41
- expect(screen.getByTestId('url').textContent).toBe('./second.png');
42
-
43
- await act(async () => {
44
- first.resolve('blob:first');
45
- await first.promise;
46
- second.reject(new Error('missing'));
47
- await second.promise.catch(() => undefined);
48
- });
49
- expect(screen.getByTestId('url').textContent).toBe('./second.png');
50
- });
51
- });
@@ -1,84 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from '@testing-library/react';
3
- import { PathLayer } from '../layers/PathLayer';
4
- import type { PathLayer as PathLayerType } from '@bendyline/squisq/schemas';
5
-
6
- const viewport = { width: 1000, height: 1000 };
7
-
8
- function renderPath(layer: PathLayerType) {
9
- const { container } = render(
10
- <svg>
11
- <PathLayer layer={layer} viewport={viewport} blockTime={0} />
12
- </svg>,
13
- );
14
- return container.querySelector('.block-layer--path > path')!;
15
- }
16
-
17
- describe('PathLayer', () => {
18
- it('uses the stored absolute `d` for plain paths (no shapeKind)', () => {
19
- const path = renderPath({
20
- id: 'p1',
21
- type: 'path',
22
- position: { x: 0, y: 0, width: 100, height: 100 },
23
- content: { d: 'M 10 10 L 90 90' },
24
- });
25
- expect(path.getAttribute('d')).toBe('M 10 10 L 90 90');
26
- });
27
-
28
- it('derives `d` from the position box for a named shape', () => {
29
- // A diamond's first vertex is the top-center of its box.
30
- const path = renderPath({
31
- id: 'p2',
32
- type: 'path',
33
- position: { x: 100, y: 200, width: 400, height: 200 },
34
- content: { d: 'STALE', shapeKind: 'diamond' },
35
- });
36
- const d = path.getAttribute('d')!;
37
- expect(d).not.toBe('STALE');
38
- // Top vertex at (x + w/2, y) = (300, 200).
39
- expect(d.startsWith('M 300 200')).toBe(true);
40
- });
41
-
42
- it('moving the position moves a named shape (regenerates `d`)', () => {
43
- const at = (x: number) =>
44
- renderPath({
45
- id: 'p3',
46
- type: 'path',
47
- position: { x, y: 0, width: 200, height: 200 },
48
- content: { d: 'M 0 0', shapeKind: 'diamond' },
49
- }).getAttribute('d')!;
50
- expect(at(0)).not.toBe(at(300));
51
- });
52
-
53
- it('resolves `%` positions against the viewport', () => {
54
- const path = renderPath({
55
- id: 'p4',
56
- type: 'path',
57
- position: { x: '0%', y: '0%', width: '50%', height: '50%' },
58
- content: { d: '', shapeKind: 'diamond' },
59
- });
60
- // 50% of a 1000px viewport → 500px box; diamond top vertex at (250, 0).
61
- expect(path.getAttribute('d')!.startsWith('M 250 0')).toBe(true);
62
- });
63
-
64
- it('falls back to the stored `d` when shapeKind is unknown', () => {
65
- const path = renderPath({
66
- id: 'p5',
67
- type: 'path',
68
- position: { x: 0, y: 0, width: 100, height: 100 },
69
- content: { d: 'M 1 2 L 3 4', shapeKind: 'not-a-real-shape' },
70
- });
71
- expect(path.getAttribute('d')).toBe('M 1 2 L 3 4');
72
- });
73
-
74
- it('reads the old serialized arrow field without exposing it in the PathLayer type', () => {
75
- const legacyLayer = {
76
- id: 'legacy-arrow',
77
- type: 'path',
78
- position: { x: 0, y: 0, width: 100, height: 100 },
79
- content: { d: 'M 0 0 L 100 100', arrow: 'end' },
80
- } as unknown as PathLayerType;
81
-
82
- expect(renderPath(legacyLayer).getAttribute('marker-end')).toMatch(/^url\(#marker-end-/);
83
- });
84
- });