@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,142 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import { TableLayer } from '../layers/TableLayer';
|
|
4
|
-
import type { TableLayer as TableLayerType } from '@bendyline/squisq/schemas';
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Helpers
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
function makeTableLayer(overrides: Partial<TableLayerType> = {}): TableLayerType {
|
|
11
|
-
return {
|
|
12
|
-
type: 'table',
|
|
13
|
-
id: 'test-table',
|
|
14
|
-
content: {
|
|
15
|
-
headers: ['Name', 'Age', 'City'],
|
|
16
|
-
rows: [
|
|
17
|
-
['Alice', '30', 'Seattle'],
|
|
18
|
-
['Bob', '25', 'Portland'],
|
|
19
|
-
],
|
|
20
|
-
style: {
|
|
21
|
-
headerBackground: '#1e3a5f',
|
|
22
|
-
headerColor: '#ffffff',
|
|
23
|
-
cellBackground: 'rgba(255,255,255,0.05)',
|
|
24
|
-
cellColor: '#e0e0e0',
|
|
25
|
-
borderColor: 'rgba(255,255,255,0.12)',
|
|
26
|
-
fontSize: 28,
|
|
27
|
-
fontFamily: 'system-ui',
|
|
28
|
-
headerFontFamily: 'Georgia',
|
|
29
|
-
borderRadius: 8,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
position: { x: '10%', y: '10%', width: '80%', height: '80%' },
|
|
33
|
-
...overrides,
|
|
34
|
-
} as TableLayerType;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const viewport = { width: 1920, height: 1080 };
|
|
38
|
-
|
|
39
|
-
function renderTableLayer(layer?: TableLayerType) {
|
|
40
|
-
return render(
|
|
41
|
-
<svg>
|
|
42
|
-
<TableLayer layer={layer ?? makeTableLayer()} viewport={viewport} blockTime={0} />
|
|
43
|
-
</svg>,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Tests
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
describe('TableLayer', () => {
|
|
52
|
-
it('renders a foreignObject element', () => {
|
|
53
|
-
const { container } = renderTableLayer();
|
|
54
|
-
const fo = container.querySelector('foreignObject');
|
|
55
|
-
expect(fo).toBeTruthy();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('renders a table element', () => {
|
|
59
|
-
const { container } = renderTableLayer();
|
|
60
|
-
const table = container.querySelector('table');
|
|
61
|
-
expect(table).toBeTruthy();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('renders the correct number of header cells', () => {
|
|
65
|
-
const { container } = renderTableLayer();
|
|
66
|
-
const headers = container.querySelectorAll('th');
|
|
67
|
-
expect(headers.length).toBe(3);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('renders header text content', () => {
|
|
71
|
-
const { container } = renderTableLayer();
|
|
72
|
-
const headers = container.querySelectorAll('th');
|
|
73
|
-
expect(headers[0].textContent).toBe('Name');
|
|
74
|
-
expect(headers[1].textContent).toBe('Age');
|
|
75
|
-
expect(headers[2].textContent).toBe('City');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('renders the correct number of data rows', () => {
|
|
79
|
-
const { container } = renderTableLayer();
|
|
80
|
-
const rows = container.querySelectorAll('tbody tr');
|
|
81
|
-
expect(rows.length).toBe(2);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('renders cell text content', () => {
|
|
85
|
-
const { container } = renderTableLayer();
|
|
86
|
-
const cells = container.querySelectorAll('td');
|
|
87
|
-
expect(cells[0].textContent).toBe('Alice');
|
|
88
|
-
expect(cells[1].textContent).toBe('30');
|
|
89
|
-
expect(cells[2].textContent).toBe('Seattle');
|
|
90
|
-
expect(cells[3].textContent).toBe('Bob');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('applies header background color', () => {
|
|
94
|
-
const { container } = renderTableLayer();
|
|
95
|
-
const th = container.querySelector('th');
|
|
96
|
-
expect(th?.style.background).toBe('rgb(30, 58, 95)');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('applies font size from style', () => {
|
|
100
|
-
const { container } = renderTableLayer();
|
|
101
|
-
const table = container.querySelector('table');
|
|
102
|
-
expect(table?.style.fontSize).toBe('28px');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('applies column alignment when provided', () => {
|
|
106
|
-
const layer = makeTableLayer();
|
|
107
|
-
layer.content.align = ['left', 'center', 'right'];
|
|
108
|
-
const { container } = renderTableLayer(layer);
|
|
109
|
-
|
|
110
|
-
const headers = container.querySelectorAll('th');
|
|
111
|
-
expect(headers[0].style.textAlign).toBe('left');
|
|
112
|
-
expect(headers[1].style.textAlign).toBe('center');
|
|
113
|
-
expect(headers[2].style.textAlign).toBe('right');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('handles empty headers gracefully', () => {
|
|
117
|
-
const layer = makeTableLayer();
|
|
118
|
-
layer.content.headers = [];
|
|
119
|
-
const { container } = renderTableLayer(layer);
|
|
120
|
-
const thead = container.querySelector('thead');
|
|
121
|
-
// No thead or empty thead
|
|
122
|
-
expect(thead).toBeFalsy();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('handles empty rows gracefully', () => {
|
|
126
|
-
const layer = makeTableLayer();
|
|
127
|
-
layer.content.rows = [];
|
|
128
|
-
const { container } = renderTableLayer(layer);
|
|
129
|
-
const tbody = container.querySelector('tbody');
|
|
130
|
-
// No tbody or empty tbody
|
|
131
|
-
expect(tbody).toBeFalsy();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('sets foreignObject dimensions based on viewport percentage', () => {
|
|
135
|
-
const { container } = renderTableLayer();
|
|
136
|
-
const fo = container.querySelector('foreignObject');
|
|
137
|
-
// 80% of 1920 = 1536
|
|
138
|
-
expect(fo?.getAttribute('width')).toBe('1536');
|
|
139
|
-
// 80% of 1080 = 864
|
|
140
|
-
expect(fo?.getAttribute('height')).toBe('864');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/** @vitest-environment jsdom */
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { cleanup, render, waitFor } from '@testing-library/react';
|
|
5
|
-
import type { VideoLayer as VideoLayerType } from '@bendyline/squisq/schemas';
|
|
6
|
-
import { VideoLayer } from '../layers/VideoLayer';
|
|
7
|
-
|
|
8
|
-
const layer: VideoLayerType = {
|
|
9
|
-
id: 'video',
|
|
10
|
-
type: 'video',
|
|
11
|
-
position: { x: 0, y: 0, width: 640, height: 360 },
|
|
12
|
-
content: {
|
|
13
|
-
src: 'clip.mp4',
|
|
14
|
-
alt: 'Demo clip',
|
|
15
|
-
clipStart: 2,
|
|
16
|
-
clipEnd: 8,
|
|
17
|
-
startAt: 1,
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined);
|
|
23
|
-
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => undefined);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
cleanup();
|
|
28
|
-
vi.restoreAllMocks();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('VideoLayer playback synchronization', () => {
|
|
32
|
-
it('joins the document clock when mounted partway through a block', async () => {
|
|
33
|
-
const { container, rerender } = render(
|
|
34
|
-
<svg>
|
|
35
|
-
<VideoLayer
|
|
36
|
-
layer={layer}
|
|
37
|
-
basePath="/media"
|
|
38
|
-
viewport={{ width: 640, height: 360 }}
|
|
39
|
-
blockTime={4}
|
|
40
|
-
isPlaying
|
|
41
|
-
/>
|
|
42
|
-
</svg>,
|
|
43
|
-
);
|
|
44
|
-
const video = container.querySelector('video')!;
|
|
45
|
-
|
|
46
|
-
// clipStart 2 + (blockTime 4 - startAt 1) = source time 5.
|
|
47
|
-
await waitFor(() => expect(video.currentTime).toBe(5));
|
|
48
|
-
|
|
49
|
-
rerender(
|
|
50
|
-
<svg>
|
|
51
|
-
<VideoLayer
|
|
52
|
-
layer={layer}
|
|
53
|
-
basePath="/media"
|
|
54
|
-
viewport={{ width: 640, height: 360 }}
|
|
55
|
-
blockTime={6}
|
|
56
|
-
isPlaying
|
|
57
|
-
/>
|
|
58
|
-
</svg>,
|
|
59
|
-
);
|
|
60
|
-
expect(video.currentTime).toBe(7);
|
|
61
|
-
|
|
62
|
-
rerender(
|
|
63
|
-
<svg>
|
|
64
|
-
<VideoLayer
|
|
65
|
-
layer={layer}
|
|
66
|
-
basePath="/media"
|
|
67
|
-
viewport={{ width: 640, height: 360 }}
|
|
68
|
-
blockTime={20}
|
|
69
|
-
isPlaying
|
|
70
|
-
/>
|
|
71
|
-
</svg>,
|
|
72
|
-
);
|
|
73
|
-
expect(video.currentTime).toBe(8);
|
|
74
|
-
expect(video.pause).toHaveBeenCalled();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('holds at the in-point until startAt', async () => {
|
|
78
|
-
const { container } = render(
|
|
79
|
-
<svg>
|
|
80
|
-
<VideoLayer
|
|
81
|
-
layer={layer}
|
|
82
|
-
basePath="/media"
|
|
83
|
-
viewport={{ width: 640, height: 360 }}
|
|
84
|
-
blockTime={0.5}
|
|
85
|
-
isPlaying
|
|
86
|
-
/>
|
|
87
|
-
</svg>,
|
|
88
|
-
);
|
|
89
|
-
const video = container.querySelector('video')!;
|
|
90
|
-
|
|
91
|
-
await waitFor(() => expect(video.currentTime).toBe(2));
|
|
92
|
-
expect(video.pause).toHaveBeenCalled();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import * as squisqReact from '../index';
|
|
3
|
-
|
|
4
|
-
describe('@bendyline/squisq-react exports', () => {
|
|
5
|
-
it('exports DocPlayer component', () => {
|
|
6
|
-
expect(squisqReact.DocPlayer).toBeDefined();
|
|
7
|
-
expect(typeof squisqReact.DocPlayer).toBe('function');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('exports BlockRenderer component', () => {
|
|
11
|
-
expect(squisqReact.BlockRenderer).toBeDefined();
|
|
12
|
-
expect(typeof squisqReact.BlockRenderer).toBe('function');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('exports all layer components', () => {
|
|
16
|
-
expect(typeof squisqReact.ImageLayer).toBe('function');
|
|
17
|
-
expect(typeof squisqReact.TextLayer).toBe('function');
|
|
18
|
-
expect(typeof squisqReact.ShapeLayer).toBe('function');
|
|
19
|
-
expect(typeof squisqReact.VideoLayer).toBe('function');
|
|
20
|
-
expect(typeof squisqReact.MapLayer).toBe('function');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('exports control components', () => {
|
|
24
|
-
expect(typeof squisqReact.CaptionOverlay).toBe('function');
|
|
25
|
-
expect(typeof squisqReact.DocControlsOverlay).toBe('function');
|
|
26
|
-
expect(typeof squisqReact.DocControlsBottom).toBe('function');
|
|
27
|
-
expect(typeof squisqReact.DocControlsSidebar).toBe('function');
|
|
28
|
-
expect(typeof squisqReact.DocControlsSlideshow).toBe('function');
|
|
29
|
-
expect(typeof squisqReact.DocPlayerWithSidebar).toBe('function');
|
|
30
|
-
expect(typeof squisqReact.DocProgressBar).toBe('function');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('exports MarkdownRenderer and LinearDocView', () => {
|
|
34
|
-
expect(typeof squisqReact.MarkdownRenderer).toBe('function');
|
|
35
|
-
expect(typeof squisqReact.LinearDocView).toBe('function');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('exports hooks', () => {
|
|
39
|
-
expect(typeof squisqReact.useAudioSync).toBe('function');
|
|
40
|
-
expect(typeof squisqReact.useDocPlayback).toBe('function');
|
|
41
|
-
expect(typeof squisqReact.useViewportOrientation).toBe('function');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('exports utility functions', () => {
|
|
45
|
-
expect(typeof squisqReact.getAnimationStyle).toBe('function');
|
|
46
|
-
expect(typeof squisqReact.getTransitionClass).toBe('function');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('exports formatTime utility', () => {
|
|
50
|
-
expect(typeof squisqReact.formatTime).toBe('function');
|
|
51
|
-
expect(squisqReact.formatTime(65)).toBe('1:05');
|
|
52
|
-
expect(squisqReact.formatTime(0)).toBe('0:00');
|
|
53
|
-
expect(squisqReact.formatTime(3661)).toBe('61:01');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import { borderDashArray, resolveFill } from '../utils/fillStyle';
|
|
4
|
-
import { ShapeLayer } from '../layers/ShapeLayer';
|
|
5
|
-
import { TextLayer } from '../layers/TextLayer';
|
|
6
|
-
import type {
|
|
7
|
-
ShapeLayer as ShapeLayerType,
|
|
8
|
-
TextLayer as TextLayerType,
|
|
9
|
-
} from '@bendyline/squisq/schemas';
|
|
10
|
-
|
|
11
|
-
const viewport = { width: 1000, height: 1000 };
|
|
12
|
-
|
|
13
|
-
describe('borderDashArray', () => {
|
|
14
|
-
it('returns undefined for solid / unset', () => {
|
|
15
|
-
expect(borderDashArray(undefined, 2)).toBeUndefined();
|
|
16
|
-
expect(borderDashArray('solid', 2)).toBeUndefined();
|
|
17
|
-
});
|
|
18
|
-
it('scales the pattern by stroke width', () => {
|
|
19
|
-
expect(borderDashArray('dashed', 2)).toBe('6 4');
|
|
20
|
-
expect(borderDashArray('dotted', 2)).toBe('2 4');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('resolveFill', () => {
|
|
25
|
-
it('returns the solid color with no def when there is no gradient', () => {
|
|
26
|
-
const { fill, def } = resolveFill('x', '#abcdef', undefined);
|
|
27
|
-
expect(fill).toBe('#abcdef');
|
|
28
|
-
expect(def).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
it('returns a url() fill and a gradient def when a gradient is set', () => {
|
|
31
|
-
const { fill, def } = resolveFill('x', '#abcdef', { from: '#000', to: '#fff', angle: 90 });
|
|
32
|
-
expect(fill).toBe('url(#squisq-grad-x)');
|
|
33
|
-
expect(def).not.toBeNull();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('ShapeLayer fill/border', () => {
|
|
38
|
-
function makeShape(content: Partial<ShapeLayerType['content']>): ShapeLayerType {
|
|
39
|
-
return {
|
|
40
|
-
id: 's1',
|
|
41
|
-
type: 'shape',
|
|
42
|
-
position: { x: 0, y: 0, width: 100, height: 100 },
|
|
43
|
-
content: { shape: 'rect', ...content },
|
|
44
|
-
} as ShapeLayerType;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function makeFullBleedShape(content: Partial<ShapeLayerType['content']>): ShapeLayerType {
|
|
48
|
-
return {
|
|
49
|
-
...makeShape(content),
|
|
50
|
-
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
it('applies fill opacity and a dashed border', () => {
|
|
55
|
-
const { container } = render(
|
|
56
|
-
<svg>
|
|
57
|
-
<ShapeLayer
|
|
58
|
-
layer={makeShape({
|
|
59
|
-
fill: '#3b82f6',
|
|
60
|
-
fillOpacity: 0.5,
|
|
61
|
-
stroke: '#000',
|
|
62
|
-
strokeWidth: 4,
|
|
63
|
-
borderStyle: 'dashed',
|
|
64
|
-
})}
|
|
65
|
-
viewport={viewport}
|
|
66
|
-
blockTime={0}
|
|
67
|
-
/>
|
|
68
|
-
</svg>,
|
|
69
|
-
);
|
|
70
|
-
const rect = container.querySelector('rect')!;
|
|
71
|
-
expect(rect.getAttribute('fill-opacity')).toBe('0.5');
|
|
72
|
-
expect(rect.getAttribute('stroke-dasharray')).toBe('12 8');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('renders a gradient via an SVG linearGradient', () => {
|
|
76
|
-
const { container } = render(
|
|
77
|
-
<svg>
|
|
78
|
-
<ShapeLayer
|
|
79
|
-
layer={makeShape({ gradient: { from: '#111', to: '#eee', angle: 0 } })}
|
|
80
|
-
viewport={viewport}
|
|
81
|
-
blockTime={0}
|
|
82
|
-
/>
|
|
83
|
-
</svg>,
|
|
84
|
-
);
|
|
85
|
-
const gradient = container.querySelector('linearGradient');
|
|
86
|
-
expect(gradient).not.toBeNull();
|
|
87
|
-
expect(container.querySelector('rect')!.getAttribute('fill')).toBe(`url(#${gradient!.id})`);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('overscans a full-bleed solid shade to prevent an image edge seam', () => {
|
|
91
|
-
const { container } = render(
|
|
92
|
-
<svg>
|
|
93
|
-
<ShapeLayer
|
|
94
|
-
layer={makeFullBleedShape({ fill: 'rgba(0, 0, 0, 0.5)' })}
|
|
95
|
-
viewport={viewport}
|
|
96
|
-
blockTime={0}
|
|
97
|
-
/>
|
|
98
|
-
</svg>,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const rect = container.querySelector('rect')!;
|
|
102
|
-
expect(rect.getAttribute('x')).toBe('-1');
|
|
103
|
-
expect(rect.getAttribute('y')).toBe('-1');
|
|
104
|
-
expect(rect.getAttribute('width')).toBe('1002');
|
|
105
|
-
expect(rect.getAttribute('height')).toBe('1002');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('overscans a full-bleed CSS gradient shade and its HTML fill', () => {
|
|
109
|
-
const { container } = render(
|
|
110
|
-
<svg>
|
|
111
|
-
<ShapeLayer
|
|
112
|
-
layer={makeFullBleedShape({
|
|
113
|
-
fill: 'linear-gradient(0deg, rgba(0,0,0,0.8), transparent)',
|
|
114
|
-
})}
|
|
115
|
-
viewport={viewport}
|
|
116
|
-
blockTime={0}
|
|
117
|
-
/>
|
|
118
|
-
</svg>,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const foreignObject = container.querySelector('foreignObject')!;
|
|
122
|
-
expect(foreignObject.getAttribute('x')).toBe('-1');
|
|
123
|
-
expect(foreignObject.getAttribute('y')).toBe('-1');
|
|
124
|
-
expect(foreignObject.getAttribute('width')).toBe('1002');
|
|
125
|
-
expect(foreignObject.getAttribute('height')).toBe('1002');
|
|
126
|
-
expect(foreignObject.querySelector('div')!.style.width).toBe('1002px');
|
|
127
|
-
expect(foreignObject.querySelector('div')!.style.height).toBe('1002px');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('TextLayer box border', () => {
|
|
132
|
-
it('draws a border rect on the position box', () => {
|
|
133
|
-
const layer: TextLayerType = {
|
|
134
|
-
id: 't1',
|
|
135
|
-
type: 'text',
|
|
136
|
-
position: { x: 0, y: 0, width: 400, height: 200 },
|
|
137
|
-
content: {
|
|
138
|
-
text: 'Hi',
|
|
139
|
-
style: {
|
|
140
|
-
fontSize: 40,
|
|
141
|
-
color: '#000',
|
|
142
|
-
borderColor: '#f00',
|
|
143
|
-
borderWidth: 3,
|
|
144
|
-
borderStyle: 'dotted',
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
const { container } = render(
|
|
149
|
-
<svg>
|
|
150
|
-
<TextLayer layer={layer} viewport={viewport} blockTime={0} />
|
|
151
|
-
</svg>,
|
|
152
|
-
);
|
|
153
|
-
const rect = container.querySelector('rect')!;
|
|
154
|
-
expect(rect).not.toBeNull();
|
|
155
|
-
expect(rect.getAttribute('stroke')).toBe('#f00');
|
|
156
|
-
expect(rect.getAttribute('stroke-width')).toBe('3');
|
|
157
|
-
expect(rect.getAttribute('width')).toBe('400');
|
|
158
|
-
expect(rect.getAttribute('height')).toBe('200');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import type { Doc } from '@bendyline/squisq/schemas';
|
|
3
|
-
import { getHandle, mount, unmount } from '../standalone-entry';
|
|
4
|
-
import * as standalone from '../standalone-entry';
|
|
5
|
-
|
|
6
|
-
function doc(id: string): Doc {
|
|
7
|
-
return {
|
|
8
|
-
articleId: id,
|
|
9
|
-
duration: 2,
|
|
10
|
-
blocks: [{ id: `${id}-block`, startTime: 0, duration: 2, audioSegment: 0, layers: [] }],
|
|
11
|
-
audio: { segments: [] },
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function animatedDoc(id: string): Doc {
|
|
16
|
-
const result = doc(id);
|
|
17
|
-
result.blocks[0].layers = [
|
|
18
|
-
{
|
|
19
|
-
type: 'text',
|
|
20
|
-
id: `${id}-title`,
|
|
21
|
-
content: { text: 'Standalone motion', style: { fontSize: 48, color: '#fff' } },
|
|
22
|
-
position: { x: 100, y: 100 },
|
|
23
|
-
animation: { type: 'fadeIn', duration: 1 },
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const mountedElements: Element[] = [];
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
for (const element of mountedElements.splice(0)) unmount(element);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('standalone player instance handles', () => {
|
|
36
|
-
it('does not expose the removed mountStatic compatibility alias', () => {
|
|
37
|
-
expect('mountStatic' in standalone).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('returns the render API for exactly the mounted player instance', async () => {
|
|
41
|
-
const firstRoot = document.createElement('div');
|
|
42
|
-
const secondRoot = document.createElement('div');
|
|
43
|
-
document.body.append(firstRoot, secondRoot);
|
|
44
|
-
mountedElements.push(firstRoot, secondRoot);
|
|
45
|
-
|
|
46
|
-
const first = mount(firstRoot, doc('first'), { renderMode: true });
|
|
47
|
-
const second = mount(secondRoot, doc('second'), { renderMode: true });
|
|
48
|
-
const [firstAPI, secondAPI] = await Promise.all([first.renderAPI, second.renderAPI]);
|
|
49
|
-
|
|
50
|
-
expect(firstAPI?.getBlocks()[0].id).toBe('first-block');
|
|
51
|
-
expect(secondAPI?.getBlocks()[0].id).toBe('second-block');
|
|
52
|
-
expect(getHandle(firstRoot)).toBe(first);
|
|
53
|
-
expect(getHandle(secondRoot)).toBe(second);
|
|
54
|
-
expect('seekTo' in window).toBe(false);
|
|
55
|
-
expect('getDuration' in window).toBe(false);
|
|
56
|
-
expect('squisqActivePlayerId' in window).toBe(false);
|
|
57
|
-
expect('squisqPlayers' in window).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('resolves a null render API outside render mode and owns unmounting', async () => {
|
|
61
|
-
const root = document.createElement('div');
|
|
62
|
-
document.body.append(root);
|
|
63
|
-
mountedElements.push(root);
|
|
64
|
-
|
|
65
|
-
const handle = mount(root, doc('static'), { mode: 'static' });
|
|
66
|
-
expect(await handle.renderAPI).toBeNull();
|
|
67
|
-
expect(getHandle(root)).toBe(handle);
|
|
68
|
-
|
|
69
|
-
handle.unmount();
|
|
70
|
-
expect(getHandle(root)).toBeUndefined();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('prevents a stale handle from unmounting a newer player in the same element', async () => {
|
|
74
|
-
const root = document.createElement('div');
|
|
75
|
-
document.body.append(root);
|
|
76
|
-
mountedElements.push(root);
|
|
77
|
-
|
|
78
|
-
const first = mount(root, doc('first'), { renderMode: true });
|
|
79
|
-
expect((await first.renderAPI)?.getBlocks()[0].id).toBe('first-block');
|
|
80
|
-
|
|
81
|
-
const second = mount(root, doc('second'), { renderMode: true });
|
|
82
|
-
expect((await second.renderAPI)?.getBlocks()[0].id).toBe('second-block');
|
|
83
|
-
expect(first.getRenderAPI()).toBeNull();
|
|
84
|
-
|
|
85
|
-
first.unmount();
|
|
86
|
-
expect(getHandle(root)).toBe(second);
|
|
87
|
-
expect(second.getRenderAPI()?.getBlocks()[0].id).toBe('second-block');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('forwards the animationsEnabled render policy to the mounted player', async () => {
|
|
91
|
-
const root = document.createElement('div');
|
|
92
|
-
document.body.append(root);
|
|
93
|
-
mountedElements.push(root);
|
|
94
|
-
|
|
95
|
-
const handle = mount(root, animatedDoc('motionless'), {
|
|
96
|
-
renderMode: true,
|
|
97
|
-
animationsEnabled: false,
|
|
98
|
-
});
|
|
99
|
-
await handle.renderAPI;
|
|
100
|
-
|
|
101
|
-
expect(root.querySelector('[class*="anim-"]')).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import type { TransitionDirection, TransitionType } from '@bendyline/squisq/schemas';
|
|
5
|
-
import { TRANSITION_TYPES } from '@bendyline/squisq/schemas';
|
|
6
|
-
import { getTransitionClass } from '../utils/animationUtils';
|
|
7
|
-
|
|
8
|
-
const transitionCss = readFileSync(
|
|
9
|
-
resolve(process.cwd(), 'packages/react/src/styles/doc-animations.css'),
|
|
10
|
-
'utf8',
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const directionalTypes: TransitionType[] = [
|
|
14
|
-
'cover',
|
|
15
|
-
'pan',
|
|
16
|
-
'pull',
|
|
17
|
-
'push',
|
|
18
|
-
'reveal',
|
|
19
|
-
'strips',
|
|
20
|
-
'uncover',
|
|
21
|
-
'wipe',
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const axisTypes: TransitionType[] = ['blinds', 'comb', 'randomBar', 'randomBars', 'split'];
|
|
25
|
-
const cardinalDirections = [
|
|
26
|
-
'left',
|
|
27
|
-
'right',
|
|
28
|
-
'up',
|
|
29
|
-
'down',
|
|
30
|
-
] as const satisfies readonly TransitionDirection[];
|
|
31
|
-
const axisDirections = ['horizontal', 'vertical'] as const satisfies readonly TransitionDirection[];
|
|
32
|
-
|
|
33
|
-
function collectGeneratedTransitionClasses(): string[] {
|
|
34
|
-
const classNames = new Set<string>();
|
|
35
|
-
|
|
36
|
-
for (const type of TRANSITION_TYPES) {
|
|
37
|
-
if (type === 'cut') continue;
|
|
38
|
-
classNames.add(getTransitionClass(type, true));
|
|
39
|
-
classNames.add(getTransitionClass(type, false));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const type of directionalTypes) {
|
|
43
|
-
for (const direction of cardinalDirections) {
|
|
44
|
-
classNames.add(getTransitionClass(type, true, direction));
|
|
45
|
-
classNames.add(getTransitionClass(type, false, direction));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (const type of axisTypes) {
|
|
50
|
-
for (const direction of axisDirections) {
|
|
51
|
-
classNames.add(getTransitionClass(type, true, direction));
|
|
52
|
-
classNames.add(getTransitionClass(type, false, direction));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return [...classNames].sort();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe('transition CSS coverage', () => {
|
|
60
|
-
it('contains enter and exit selectors for every generated transition class', () => {
|
|
61
|
-
for (const className of collectGeneratedTransitionClasses()) {
|
|
62
|
-
expect(transitionCss, className).toContain(`.${className}`);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
/** All `@keyframes <name>` defined in the stylesheet. */
|
|
68
|
-
function definedKeyframes(): Set<string> {
|
|
69
|
-
const names = new Set<string>();
|
|
70
|
-
for (const m of transitionCss.matchAll(/@keyframes\s+([A-Za-z0-9_-]+)/g)) {
|
|
71
|
-
names.add(m[1]);
|
|
72
|
-
}
|
|
73
|
-
return names;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Map each `.transition-<x>-(enter|exit)` selector to the keyframe its rule
|
|
78
|
-
* animates. Parses top-level `selectors { body }` blocks and reads the first
|
|
79
|
-
* identifier of the `animation:` shorthand. Returns null for a transition rule
|
|
80
|
-
* with no `animation` property (itself a defect).
|
|
81
|
-
*/
|
|
82
|
-
function transitionRuleKeyframes(): Map<string, string | null> {
|
|
83
|
-
const map = new Map<string, string | null>();
|
|
84
|
-
const ruleRe = /([^{}]+)\{([^{}]*)\}/g;
|
|
85
|
-
let rule: RegExpExecArray | null;
|
|
86
|
-
while ((rule = ruleRe.exec(transitionCss)) !== null) {
|
|
87
|
-
const selectors = rule[1].split(',').map((s) => s.trim());
|
|
88
|
-
const transitionSelectors = selectors.filter((s) =>
|
|
89
|
-
/\.transition-[A-Za-z0-9-]+-(?:enter|exit)\b/.test(s),
|
|
90
|
-
);
|
|
91
|
-
if (transitionSelectors.length === 0) continue;
|
|
92
|
-
const animMatch = rule[2].match(/animation:\s*([A-Za-z0-9_-]+)/);
|
|
93
|
-
const keyframe = animMatch ? animMatch[1] : null;
|
|
94
|
-
for (const selector of transitionSelectors) {
|
|
95
|
-
const classMatch = selector.match(/\.(transition-[A-Za-z0-9-]+-(?:enter|exit))/);
|
|
96
|
-
if (classMatch && !map.has(classMatch[1])) map.set(classMatch[1], keyframe);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return map;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
describe('transition keyframe integrity', () => {
|
|
103
|
-
it('every transition rule animates a defined @keyframes (catches typos)', () => {
|
|
104
|
-
const keyframes = definedKeyframes();
|
|
105
|
-
const broken: string[] = [];
|
|
106
|
-
for (const [selector, keyframe] of transitionRuleKeyframes()) {
|
|
107
|
-
if (keyframe === null) broken.push(`${selector}: no animation property`);
|
|
108
|
-
else if (!keyframes.has(keyframe)) broken.push(`${selector}: missing @keyframes ${keyframe}`);
|
|
109
|
-
}
|
|
110
|
-
expect(broken, broken.join('\n')).toEqual([]);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('every generated transition class resolves to a defined keyframe', () => {
|
|
114
|
-
// Ties the function output to the CSS end-to-end: a class the player can
|
|
115
|
-
// emit must have a rule, and that rule must animate a real keyframe.
|
|
116
|
-
const keyframes = definedKeyframes();
|
|
117
|
-
const rules = transitionRuleKeyframes();
|
|
118
|
-
for (const className of collectGeneratedTransitionClasses()) {
|
|
119
|
-
const keyframe = rules.get(className);
|
|
120
|
-
expect(keyframe, `${className} has no transition rule`).toBeDefined();
|
|
121
|
-
expect(keyframe, `${className} animates undefined keyframe ${keyframe}`).not.toBeNull();
|
|
122
|
-
if (keyframe) expect(keyframes.has(keyframe), `${className} -> ${keyframe}`).toBe(true);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|