Skip to content

Commit 4c1f787

Browse files
committed
Add SVG tests too
1 parent f724931 commit 4c1f787

File tree

4 files changed

+152
-44
lines changed

4 files changed

+152
-44
lines changed

stories/svg.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ export const CanPassAttributesOptionToCreateSvgPortalNode = () => {
124124

125125
<br/>
126126

127-
<text>{
127+
<pre>{
128128
!hasAttrOption
129129
? `const portalNode = createSvgPortalNode();`
130130
: `const portalNode = createSvgPortalNode({ attributes: { stroke: "blue" } });`
131-
}</text>
131+
}</pre>
132132
</div>
133133
});
134134
};

tests/html.test.tsx

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,10 @@ import { render } from '@testing-library/react';
33
import { composeStory } from '@storybook/react';
44
import type { RenderResult } from '@testing-library/react';
55
import * as stories from '../stories/html.stories';
6+
import { wait, waitForVideoToLoad, getSpanOrder, findButtonByText } from './test-utils';
67

78
const allStoryNames = Object.keys(stories).filter(key => key !== 'default');
89

9-
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
10-
11-
const waitForVideoToLoad = (video: HTMLVideoElement): Promise<void> => {
12-
return new Promise<void>((resolve, reject) => {
13-
const timeout = setTimeout(() => reject(new Error('Video load timeout after 30s')), 30000);
14-
15-
const cleanup = () => clearTimeout(timeout);
16-
17-
if (video.readyState >= 2) {
18-
cleanup();
19-
resolve();
20-
return;
21-
}
22-
23-
const onSuccess = () => {
24-
cleanup();
25-
resolve();
26-
};
27-
28-
const onError = (e: Event) => {
29-
cleanup();
30-
reject(new Error(`Video failed to load: ${(e.target as HTMLVideoElement)?.error?.message || 'unknown error'}`));
31-
};
32-
33-
video.addEventListener('loadeddata', onSuccess, { once: true });
34-
video.addEventListener('canplay', onSuccess, { once: true });
35-
video.addEventListener('error', onError, { once: true });
36-
37-
video.load();
38-
});
39-
};
40-
41-
const getSpanOrder = (container: HTMLElement): string[] => {
42-
const spans = container.querySelectorAll('span');
43-
return Array.from(spans).map(span => span.textContent);
44-
};
45-
46-
const findButtonByText = (container: HTMLElement, text: string): HTMLButtonElement | undefined => {
47-
const buttons = container.querySelectorAll('button');
48-
return Array.from(buttons).find(btn => btn.textContent === text) as HTMLButtonElement | undefined;
49-
};
50-
5110
const storyTests: Record<string, (result: RenderResult) => void | Promise<void>> = {
5211
'RenderThingsInDifferentPlaces': ({ container }) => {
5312
expect(container.innerHTML).toContain(
@@ -223,6 +182,7 @@ const storyTests: Record<string, (result: RenderResult) => void | Promise<void>>
223182
expect(video.src).toContain('giphy.mp4');
224183

225184
await waitForVideoToLoad(video);
185+
expect(video.paused).toBe(true);
226186
await video.play();
227187
await wait(200);
228188

tests/svg.test.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { expect, test } from 'vitest';
2+
import { render } from '@testing-library/react';
3+
import { composeStory } from '@storybook/react';
4+
import type { RenderResult } from '@testing-library/react';
5+
import * as stories from '../stories/svg.stories';
6+
import { wait, waitForVideoToLoad } from './test-utils';
7+
8+
const allStoryNames = Object.keys(stories).filter(key => key !== 'default');
9+
10+
const storyTests: Record<string, (result: RenderResult) => void | Promise<void>> = {
11+
'WorksWithSVGs': ({ container }) => {
12+
const svg = container.querySelector('svg');
13+
expect(svg).not.toBeNull();
14+
15+
const texts = svg?.querySelectorAll('text');
16+
expect(texts?.length).toBe(1);
17+
expect(texts?.[0].textContent).toBe('test');
18+
expect(texts?.[0].getAttribute('fill')).toBe('red');
19+
},
20+
'CanMoveContentAroundWithinSVGs': async ({ container, getByText }) => {
21+
const svg = container.querySelector('svg');
22+
expect(svg).not.toBeNull();
23+
24+
const nestedSvgs = svg?.querySelectorAll('svg');
25+
expect(nestedSvgs?.length).toBe(2);
26+
27+
const firstSvg = nestedSvgs?.[0];
28+
const secondSvg = nestedSvgs?.[1];
29+
30+
expect(firstSvg?.querySelector('text')).toBeNull();
31+
expect(secondSvg?.querySelector('text')).not.toBeNull();
32+
expect(secondSvg?.querySelector('text')?.textContent).toBe('test');
33+
34+
const button = getByText('Click to move the OutPortal within the SVG');
35+
button.click();
36+
await wait(10);
37+
38+
expect(firstSvg?.querySelector('text')).not.toBeNull();
39+
expect(firstSvg?.querySelector('text')?.textContent).toBe('test');
40+
expect(secondSvg?.querySelector('text')).toBeNull();
41+
},
42+
'PersistDOMWhileMovingWithinSVGs': async ({ container }) => {
43+
const video = container.querySelector('video') as HTMLVideoElement;
44+
expect(video).not.toBeNull();
45+
expect(video.src).toContain('giphy.mp4');
46+
47+
await waitForVideoToLoad(video);
48+
expect(video.paused).toBe(true);
49+
await video.play();
50+
await wait(200);
51+
52+
expect(video.paused).toBe(false);
53+
const playbackPosition = video.currentTime;
54+
expect(playbackPosition).toBeGreaterThan(0);
55+
56+
const button = container.querySelector('button') as HTMLButtonElement;
57+
expect(button).not.toBeNull();
58+
button.click();
59+
await wait(50);
60+
61+
const videoAfterMove = container.querySelector('video') as HTMLVideoElement;
62+
expect(videoAfterMove).toBe(video);
63+
expect(videoAfterMove.paused).toBe(false);
64+
expect(videoAfterMove.currentTime).toBeGreaterThanOrEqual(playbackPosition);
65+
expect(videoAfterMove.currentTime).toBeGreaterThan(playbackPosition);
66+
},
67+
'CanPassAttributesOptionToCreateSvgPortalNode': async ({ container, getByText }) => {
68+
const svg = container.querySelector('svg');
69+
expect(svg).not.toBeNull();
70+
71+
const nestedSvg = svg?.querySelector('svg');
72+
expect(nestedSvg).not.toBeNull();
73+
74+
const portalWrapper = nestedSvg?.querySelector('g');
75+
expect(portalWrapper).not.toBeNull();
76+
expect(portalWrapper?.getAttribute('stroke')).toBeNull();
77+
78+
const button = getByText('Click to pass attributes option to the intermediary svg');
79+
button.click();
80+
await wait(10);
81+
82+
const portalWrapperAfter = nestedSvg?.querySelector('g');
83+
expect(portalWrapperAfter?.getAttribute('stroke')).toBe('blue');
84+
},
85+
};
86+
87+
test('all stories have tests', () => {
88+
const testedStories = Object.keys(storyTests);
89+
const untestedStories = allStoryNames.filter(name => !testedStories.includes(name));
90+
91+
if (untestedStories.length > 0) {
92+
throw new Error(
93+
`The following stories do not have tests:\n${untestedStories.map(s => ` - ${s}`).join('\n')}`
94+
);
95+
}
96+
});
97+
98+
Object.entries(storyTests).forEach(([storyName, testFn]) => {
99+
test(storyName, async () => {
100+
const Story = composeStory(
101+
(stories as any)[storyName],
102+
stories.default
103+
);
104+
const result = render(<Story />);
105+
await testFn(result);
106+
});
107+
});

tests/test-utils.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
2+
3+
export const waitForVideoToLoad = (video: HTMLVideoElement): Promise<void> => {
4+
return new Promise<void>((resolve, reject) => {
5+
const timeout = setTimeout(() => reject(new Error('Video load timeout after 30s')), 30000);
6+
7+
const cleanup = () => clearTimeout(timeout);
8+
9+
if (video.readyState >= 2) {
10+
cleanup();
11+
resolve();
12+
return;
13+
}
14+
15+
const onSuccess = () => {
16+
cleanup();
17+
resolve();
18+
};
19+
20+
const onError = (e: Event) => {
21+
cleanup();
22+
reject(new Error(`Video failed to load: ${(e.target as HTMLVideoElement)?.error?.message || 'unknown error'}`));
23+
};
24+
25+
video.addEventListener('loadeddata', onSuccess, { once: true });
26+
video.addEventListener('canplay', onSuccess, { once: true });
27+
video.addEventListener('error', onError, { once: true });
28+
29+
video.load();
30+
});
31+
};
32+
33+
export const getSpanOrder = (container: HTMLElement): string[] => {
34+
const spans = container.querySelectorAll('span');
35+
return Array.from(spans).map(span => span.textContent);
36+
};
37+
38+
export const findButtonByText = (container: HTMLElement, text: string): HTMLButtonElement | undefined => {
39+
const buttons = container.querySelectorAll('button');
40+
return Array.from(buttons).find(btn => btn.textContent === text) as HTMLButtonElement | undefined;
41+
};

0 commit comments

Comments
 (0)