Skip to content

Commit 5c5bbc8

Browse files
committed
🧪 Improve test coverage to 99%
Add comprehensive tests for: - viewport utilities (parseViewport, formatViewport, setViewport, getCommonViewports) - pattern matching (matchPattern, filterByPattern, findMatchingHook edge cases) - screenshot functions (captureScreenshot, captureAndSendScreenshot) - crawler functions (readIndexJson, discoverStories, generateStoryUrl validation) Coverage: 99.33% lines, 92.86% branches, 97.06% functions
1 parent 9691eb6 commit 5c5bbc8

File tree

4 files changed

+535
-2
lines changed

4 files changed

+535
-2
lines changed

clients/storybook/tests/crawler.test.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
import { describe, it } from 'node:test';
66
import assert from 'node:assert/strict';
7+
import { writeFile, mkdir, rm } from 'node:fs/promises';
8+
import { join } from 'node:path';
9+
import { tmpdir } from 'node:os';
710
import {
811
extractStoryConfig,
912
filterStories,
1013
generateStoryUrl,
1114
parseStories,
15+
readIndexJson,
16+
discoverStories,
1217
} from '../src/crawler.js';
1318

1419
describe('parseStories', () => {
@@ -179,4 +184,116 @@ describe('generateStoryUrl', () => {
179184

180185
assert.ok(url.includes('id=my%20story%2Fwith%20spaces'));
181186
});
187+
188+
it('should throw error for empty baseUrl', () => {
189+
assert.throws(() => generateStoryUrl('', 'button--primary'), {
190+
message: 'baseUrl must be a non-empty string',
191+
});
192+
});
193+
194+
it('should throw error for null baseUrl', () => {
195+
assert.throws(() => generateStoryUrl(null, 'button--primary'), {
196+
message: 'baseUrl must be a non-empty string',
197+
});
198+
});
199+
200+
it('should throw error for empty storyId', () => {
201+
assert.throws(() => generateStoryUrl('http://localhost:6006', ''), {
202+
message: 'storyId must be a non-empty string',
203+
});
204+
});
205+
206+
it('should throw error for null storyId', () => {
207+
assert.throws(() => generateStoryUrl('http://localhost:6006', null), {
208+
message: 'storyId must be a non-empty string',
209+
});
210+
});
211+
});
212+
213+
describe('readIndexJson', () => {
214+
let testDir;
215+
216+
it('should read and parse valid index.json', async () => {
217+
testDir = join(tmpdir(), `storybook-test-${Date.now()}`);
218+
await mkdir(testDir, { recursive: true });
219+
220+
let indexData = {
221+
v: 7,
222+
entries: {
223+
'test--story': { id: 'test--story', title: 'Test', name: 'Story', type: 'story' },
224+
},
225+
};
226+
await writeFile(join(testDir, 'index.json'), JSON.stringify(indexData));
227+
228+
let result = await readIndexJson(testDir);
229+
230+
assert.deepEqual(result, indexData);
231+
232+
await rm(testDir, { recursive: true });
233+
});
234+
235+
it('should throw error for missing index.json', async () => {
236+
testDir = join(tmpdir(), `storybook-test-missing-${Date.now()}`);
237+
await mkdir(testDir, { recursive: true });
238+
239+
await assert.rejects(readIndexJson(testDir), /Failed to read Storybook index\.json/);
240+
241+
await rm(testDir, { recursive: true });
242+
});
243+
244+
it('should throw error for invalid JSON', async () => {
245+
testDir = join(tmpdir(), `storybook-test-invalid-${Date.now()}`);
246+
await mkdir(testDir, { recursive: true });
247+
await writeFile(join(testDir, 'index.json'), 'not valid json');
248+
249+
await assert.rejects(readIndexJson(testDir), /Failed to read Storybook index\.json/);
250+
251+
await rm(testDir, { recursive: true });
252+
});
253+
});
254+
255+
describe('discoverStories', () => {
256+
let testDir;
257+
258+
it('should discover and filter stories from storybook path', async () => {
259+
testDir = join(tmpdir(), `storybook-test-discover-${Date.now()}`);
260+
await mkdir(testDir, { recursive: true });
261+
262+
let indexData = {
263+
v: 7,
264+
entries: {
265+
'button--primary': { id: 'button--primary', title: 'Button', name: 'Primary', type: 'story' },
266+
'button--secondary': { id: 'button--secondary', title: 'Button', name: 'Secondary', type: 'story' },
267+
'card--default': { id: 'card--default', title: 'Card', name: 'Default', type: 'story' },
268+
},
269+
};
270+
await writeFile(join(testDir, 'index.json'), JSON.stringify(indexData));
271+
272+
let stories = await discoverStories(testDir, { include: 'button*' });
273+
274+
assert.equal(stories.length, 2);
275+
assert.ok(stories.every(s => s.id.startsWith('button')));
276+
277+
await rm(testDir, { recursive: true });
278+
});
279+
280+
it('should return all stories when no filter', async () => {
281+
testDir = join(tmpdir(), `storybook-test-discover-all-${Date.now()}`);
282+
await mkdir(testDir, { recursive: true });
283+
284+
let indexData = {
285+
v: 7,
286+
entries: {
287+
'button--primary': { id: 'button--primary', title: 'Button', name: 'Primary', type: 'story' },
288+
'card--default': { id: 'card--default', title: 'Card', name: 'Default', type: 'story' },
289+
},
290+
};
291+
await writeFile(join(testDir, 'index.json'), JSON.stringify(indexData));
292+
293+
let stories = await discoverStories(testDir, {});
294+
295+
assert.equal(stories.length, 2);
296+
297+
await rm(testDir, { recursive: true });
298+
});
182299
});
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/**
2+
* Tests for pattern matching utilities
3+
*/
4+
5+
import { describe, it } from 'node:test';
6+
import assert from 'node:assert/strict';
7+
import {
8+
matchPattern,
9+
filterByPattern,
10+
findMatchingHook,
11+
} from '../src/utils/patterns.js';
12+
13+
describe('matchPattern', () => {
14+
it('should match exact string', () => {
15+
assert.ok(matchPattern('button--primary', 'button--primary'));
16+
});
17+
18+
it('should match with wildcard', () => {
19+
assert.ok(matchPattern('button--primary', 'button*'));
20+
assert.ok(matchPattern('button--secondary', 'button*'));
21+
});
22+
23+
it('should match with double wildcard', () => {
24+
assert.ok(matchPattern('components/atoms/button', 'components/**'));
25+
assert.ok(matchPattern('components/atoms/button/primary', 'components/**'));
26+
});
27+
28+
it('should not match different strings', () => {
29+
assert.ok(!matchPattern('card--default', 'button*'));
30+
});
31+
32+
it('should return true for empty pattern', () => {
33+
assert.ok(matchPattern('anything', ''));
34+
assert.ok(matchPattern('anything', null));
35+
});
36+
37+
it('should return false for empty string', () => {
38+
assert.ok(!matchPattern('', 'pattern'));
39+
assert.ok(!matchPattern(null, 'pattern'));
40+
});
41+
});
42+
43+
describe('filterByPattern', () => {
44+
let stories = [
45+
{ id: 'button--primary', title: 'Button' },
46+
{ id: 'button--secondary', title: 'Button' },
47+
{ id: 'card--default', title: 'Card' },
48+
];
49+
50+
it('should filter by include pattern', () => {
51+
let filtered = filterByPattern(stories, 'button*', null);
52+
53+
assert.equal(filtered.length, 2);
54+
assert.ok(filtered.every(s => s.id.startsWith('button')));
55+
});
56+
57+
it('should filter by exclude pattern', () => {
58+
let filtered = filterByPattern(stories, null, 'button*');
59+
60+
assert.equal(filtered.length, 1);
61+
assert.equal(filtered[0].id, 'card--default');
62+
});
63+
64+
it('should apply both include and exclude', () => {
65+
let filtered = filterByPattern(stories, 'button*', 'button--secondary');
66+
67+
assert.equal(filtered.length, 1);
68+
assert.equal(filtered[0].id, 'button--primary');
69+
});
70+
71+
it('should return all stories when no patterns', () => {
72+
let filtered = filterByPattern(stories, null, null);
73+
74+
assert.equal(filtered.length, 3);
75+
});
76+
77+
it('should use title as fallback when no id', () => {
78+
let storiesWithoutId = [
79+
{ title: 'Button' },
80+
{ title: 'Card' },
81+
];
82+
83+
let filtered = filterByPattern(storiesWithoutId, 'Button', null);
84+
85+
assert.equal(filtered.length, 1);
86+
assert.equal(filtered[0].title, 'Button');
87+
});
88+
});
89+
90+
describe('findMatchingHook', () => {
91+
it('should find matching hook from simple object format', () => {
92+
let hook = () => {};
93+
let interactions = {
94+
'button*': hook,
95+
};
96+
let story = { id: 'button--primary' };
97+
98+
let result = findMatchingHook(story, interactions);
99+
100+
assert.equal(result, hook);
101+
});
102+
103+
it('should return null when no match in simple format', () => {
104+
let interactions = {
105+
'card*': () => {},
106+
};
107+
let story = { id: 'button--primary' };
108+
109+
let result = findMatchingHook(story, interactions);
110+
111+
assert.equal(result, null);
112+
});
113+
114+
it('should find matching hook from patterns array format', () => {
115+
let hook = () => {};
116+
let interactions = {
117+
patterns: [
118+
{ match: 'card*', beforeScreenshot: () => {} },
119+
{ match: 'button*', beforeScreenshot: hook },
120+
],
121+
};
122+
let story = { id: 'button--primary' };
123+
124+
let result = findMatchingHook(story, interactions);
125+
126+
assert.equal(result, hook);
127+
});
128+
129+
it('should return null when no match in patterns array format', () => {
130+
let interactions = {
131+
patterns: [
132+
{ match: 'card*', beforeScreenshot: () => {} },
133+
],
134+
};
135+
let story = { id: 'button--primary' };
136+
137+
let result = findMatchingHook(story, interactions);
138+
139+
assert.equal(result, null);
140+
});
141+
142+
it('should return null for null interactions', () => {
143+
let story = { id: 'button--primary' };
144+
145+
let result = findMatchingHook(story, null);
146+
147+
assert.equal(result, null);
148+
});
149+
150+
it('should return null for undefined interactions', () => {
151+
let story = { id: 'button--primary' };
152+
153+
let result = findMatchingHook(story, undefined);
154+
155+
assert.equal(result, null);
156+
});
157+
158+
it('should return null for empty interactions object', () => {
159+
let story = { id: 'button--primary' };
160+
161+
let result = findMatchingHook(story, {});
162+
163+
assert.equal(result, null);
164+
});
165+
166+
it('should use title as fallback when no id', () => {
167+
let hook = () => {};
168+
let interactions = {
169+
'Button*': hook,
170+
};
171+
let story = { title: 'Button' };
172+
173+
let result = findMatchingHook(story, interactions);
174+
175+
assert.equal(result, hook);
176+
});
177+
178+
it('should return first matching hook in patterns array', () => {
179+
let firstHook = () => 'first';
180+
let secondHook = () => 'second';
181+
let interactions = {
182+
patterns: [
183+
{ match: 'button*', beforeScreenshot: firstHook },
184+
{ match: 'button--primary', beforeScreenshot: secondHook },
185+
],
186+
};
187+
let story = { id: 'button--primary' };
188+
189+
let result = findMatchingHook(story, interactions);
190+
191+
assert.equal(result, firstHook);
192+
});
193+
});

0 commit comments

Comments
 (0)