0
# Testing Integration
1
2
Transform CSF files for integration with testing frameworks, particularly Vitest, enabling automated testing of Storybook stories. This module provides the foundation for running Storybook stories as tests.
3
4
## Capabilities
5
6
### Vitest Transformation
7
8
Transform CSF files into Vitest-compatible test files that can execute stories as individual test cases.
9
10
```typescript { .api }
11
/**
12
* Transform CSF file for Vitest testing framework
13
* @param options - Transformation configuration
14
* @returns Promise resolving to transformed code with source maps
15
*/
16
async function vitestTransform(options: VitestTransformOptions): Promise<ReturnType<typeof formatCsf>>;
17
18
interface VitestTransformOptions {
19
/** Source code of the CSF file to transform */
20
code: string;
21
/** File name for the CSF file being transformed */
22
fileName: string;
23
/** Storybook configuration directory path */
24
configDir: string;
25
/** Stories configuration entries */
26
stories: StoriesEntry[];
27
/** Tag-based filtering configuration */
28
tagsFilter: TagsFilter;
29
/** Optional preview-level tags to apply */
30
previewLevelTags?: Tag[];
31
}
32
33
interface TagsFilter {
34
/** Tags that must be present for test inclusion */
35
include: string[];
36
/** Tags that exclude tests from running */
37
exclude: string[];
38
/** Tags that mark tests to be skipped */
39
skip: string[];
40
}
41
42
interface StoriesEntry {
43
/** Glob pattern for story files */
44
directory: string;
45
/** File patterns to match */
46
files: string;
47
/** Title prefix for stories */
48
titlePrefix?: string;
49
}
50
```
51
52
**Usage Examples:**
53
54
```typescript
55
import { vitestTransform } from "@storybook/csf-tools";
56
57
const csfCode = `
58
export default {
59
title: 'Button',
60
tags: ['component', 'ui']
61
};
62
export const Primary = {
63
args: { primary: true },
64
tags: ['test', 'smoke']
65
};
66
export const Secondary = {
67
args: { primary: false },
68
tags: ['test']
69
};
70
`;
71
72
const transformed = await vitestTransform({
73
code: csfCode,
74
fileName: './src/Button.stories.ts',
75
configDir: '.storybook',
76
stories: [
77
{ directory: './src', files: '**/*.stories.@(js|ts|jsx|tsx)' }
78
],
79
tagsFilter: {
80
include: ['test'],
81
exclude: ['skip'],
82
skip: ['wip']
83
},
84
previewLevelTags: ['autodocs']
85
});
86
87
console.log(transformed); // Vitest-compatible test code
88
```
89
90
### Story Sort Parameter Extraction
91
92
Extract story sort parameters from Storybook preview configuration for proper test ordering.
93
94
```typescript { .api }
95
/**
96
* Extract story sort parameter from preview configuration code
97
* @param previewCode - Source code of preview.js or preview.ts
98
* @returns Extracted sort function/configuration or undefined if not found
99
*/
100
function getStorySortParameter(previewCode: string): any;
101
```
102
103
**Usage Examples:**
104
105
```typescript
106
import { getStorySortParameter } from "@storybook/csf-tools";
107
108
const previewCode = `
109
export default {
110
parameters: {
111
options: {
112
storySort: {
113
order: ['Introduction', 'Example', 'Components', '*'],
114
method: 'alphabetical'
115
}
116
}
117
}
118
};
119
`;
120
121
const sortConfig = getStorySortParameter(previewCode);
122
console.log(sortConfig); // { order: [...], method: 'alphabetical' }
123
```
124
125
## Test Generation Patterns
126
127
### Basic Test Generation
128
129
Transform a simple CSF file into Vitest tests:
130
131
```typescript
132
// Input CSF file
133
const storyCode = `
134
export default { title: 'Components/Button' };
135
136
export const Default = {};
137
export const WithText = { args: { children: 'Click me' } };
138
export const Disabled = { args: { disabled: true } };
139
`;
140
141
// Transform for testing
142
const testCode = await vitestTransform({
143
code: storyCode,
144
fileName: 'Button.stories.ts',
145
configDir: '.storybook',
146
stories: [{ directory: './src', files: '**/*.stories.ts' }],
147
tagsFilter: { include: [], exclude: [], skip: [] }
148
});
149
150
// Generated test structure:
151
// - Imports Vitest test functions
152
// - Imports testStory helper from Storybook
153
// - Creates test cases for each valid story
154
// - Handles story composition and execution
155
```
156
157
### Tag-Based Filtering
158
159
Control which stories become tests using tags:
160
161
```typescript
162
const taggedStories = `
163
export default {
164
title: 'Button',
165
tags: ['component']
166
};
167
168
export const Interactive = {
169
tags: ['test', 'interaction'],
170
play: async ({ canvasElement }) => {
171
// Interaction test
172
}
173
};
174
175
export const Visual = {
176
tags: ['visual', 'skip-ci']
177
};
178
179
export const Documentation = {
180
tags: ['docs-only']
181
};
182
`;
183
184
// Only include stories tagged for testing
185
const testCode = await vitestTransform({
186
code: taggedStories,
187
fileName: 'Button.stories.ts',
188
configDir: '.storybook',
189
stories: [{ directory: './src', files: '**/*.stories.ts' }],
190
tagsFilter: {
191
include: ['test'], // Must have 'test' tag
192
exclude: ['docs-only'], // Exclude documentation-only stories
193
skip: ['skip-ci'] // Skip in CI environment
194
}
195
});
196
```
197
198
### Story Title Generation
199
200
Handle automatic title generation based on file paths:
201
202
```typescript
203
const storiesConfig = [
204
{
205
directory: './src/components',
206
files: '**/*.stories.@(js|ts)',
207
titlePrefix: 'Components'
208
},
209
{
210
directory: './src/pages',
211
files: '**/*.stories.@(js|ts)',
212
titlePrefix: 'Pages'
213
}
214
];
215
216
// Transform with automatic title generation
217
const testCode = await vitestTransform({
218
code: storyCode,
219
fileName: './src/components/Button/Button.stories.ts',
220
configDir: '.storybook',
221
stories: storiesConfig,
222
tagsFilter: { include: [], exclude: [], skip: [] }
223
});
224
225
// Result: Stories get title "Components/Button/Button"
226
```
227
228
### Preview-Level Tags
229
230
Apply tags at the preview level that affect all stories:
231
232
```typescript
233
const testCode = await vitestTransform({
234
code: storyCode,
235
fileName: 'Button.stories.ts',
236
configDir: '.storybook',
237
stories: storiesConfig,
238
tagsFilter: {
239
include: ['test'],
240
exclude: ['dev-only'],
241
skip: []
242
},
243
previewLevelTags: ['autodocs', 'test-utils'] // Applied to all stories
244
});
245
```
246
247
## Generated Test Structure
248
249
The Vitest transformation produces test files with the following structure:
250
251
### Imports
252
253
```typescript
254
// Generated imports
255
import { test, expect } from 'vitest';
256
import { testStory } from '@storybook/experimental-addon-test/internal/test-utils';
257
import { default as _meta, Primary, Secondary } from './Button.stories';
258
```
259
260
### Test Guard
261
262
Prevents duplicate test execution when stories are imported:
263
264
```typescript
265
// Generated guard to prevent duplicate execution
266
const isRunningFromThisFile = import.meta.url.includes(
267
globalThis.__vitest_worker__?.filepath ?? expect.getState().testPath
268
);
269
270
if (isRunningFromThisFile) {
271
// Test cases go here
272
}
273
```
274
275
### Test Cases
276
277
Each valid story becomes a test case:
278
279
```typescript
280
// Generated test cases
281
test('Primary', testStory('Primary', Primary, _meta, skipTags));
282
test('Secondary', testStory('Secondary', Secondary, _meta, skipTags));
283
```
284
285
### Empty File Handling
286
287
Files with no valid tests get a skip block:
288
289
```typescript
290
// Generated for files with no valid stories
291
describe.skip('No valid tests found');
292
```
293
294
## Advanced Configuration
295
296
### Custom Story Sorting
297
298
Extract and apply custom story sorting from preview configuration:
299
300
```typescript
301
import { getStorySortParameter, vitestTransform } from "@storybook/csf-tools";
302
303
// Extract sort configuration
304
const previewCode = await fs.readFile('.storybook/preview.js', 'utf-8');
305
const sortConfig = getStorySortParameter(previewCode);
306
307
// Use in transformation (sorting logic is handled by Storybook core)
308
const testCode = await vitestTransform({
309
code: storyCode,
310
fileName: 'Button.stories.ts',
311
configDir: '.storybook',
312
stories: storiesConfig,
313
tagsFilter: { include: ['test'], exclude: [], skip: [] }
314
});
315
```
316
317
### Batch Processing
318
319
Transform multiple story files for testing:
320
321
```typescript
322
import { glob } from 'glob';
323
import { vitestTransform } from "@storybook/csf-tools";
324
import { readFile, writeFile } from 'fs/promises';
325
326
async function generateTests(
327
storyPattern: string,
328
outputDir: string,
329
tagsFilter: TagsFilter
330
) {
331
const storyFiles = glob.sync(storyPattern);
332
333
for (const storyFile of storyFiles) {
334
const code = await readFile(storyFile, 'utf-8');
335
336
const testCode = await vitestTransform({
337
code,
338
fileName: storyFile,
339
configDir: '.storybook',
340
stories: [{ directory: './src', files: '**/*.stories.@(js|ts)' }],
341
tagsFilter
342
});
343
344
const testFile = storyFile
345
.replace('.stories.', '.test.')
346
.replace('./src/', `${outputDir}/`);
347
348
await writeFile(testFile, testCode as string);
349
console.log(`Generated: ${testFile}`);
350
}
351
}
352
353
// Generate test files for all stories
354
await generateTests(
355
'./src/**/*.stories.ts',
356
'./tests/generated',
357
{ include: ['test'], exclude: ['visual'], skip: ['wip'] }
358
);
359
```
360
361
## Testing Utilities
362
363
The transformation system integrates with Storybook's testing utilities:
364
365
### testStory Function
366
367
The generated tests use the `testStory` helper function:
368
369
```typescript
370
// From @storybook/experimental-addon-test/internal/test-utils
371
function testStory(
372
exportName: string,
373
story: any,
374
meta: any,
375
skipTags: string[]
376
): () => Promise<void>;
377
```
378
379
This function handles:
380
- Story composition and rendering
381
- Play function execution
382
- Tag-based skipping
383
- Error handling and reporting
384
- Cleanup after test execution