or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

config-management.mdcsf-processing.mdindex.mdstory-enhancement.mdtesting-integration.md

testing-integration.mddocs/

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