CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--csf-tools

Parse and manipulate CSF and Storybook config files

Pending
Overview
Eval results
Files

testing-integration.mddocs/

Testing Integration

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.

Capabilities

Vitest Transformation

Transform CSF files into Vitest-compatible test files that can execute stories as individual test cases.

/**
 * Transform CSF file for Vitest testing framework
 * @param options - Transformation configuration
 * @returns Promise resolving to transformed code with source maps
 */
async function vitestTransform(options: VitestTransformOptions): Promise<ReturnType<typeof formatCsf>>;

interface VitestTransformOptions {
  /** Source code of the CSF file to transform */
  code: string;
  /** File name for the CSF file being transformed */
  fileName: string;
  /** Storybook configuration directory path */
  configDir: string;
  /** Stories configuration entries */
  stories: StoriesEntry[];
  /** Tag-based filtering configuration */
  tagsFilter: TagsFilter;
  /** Optional preview-level tags to apply */
  previewLevelTags?: Tag[];
}

interface TagsFilter {
  /** Tags that must be present for test inclusion */
  include: string[];
  /** Tags that exclude tests from running */
  exclude: string[];
  /** Tags that mark tests to be skipped */
  skip: string[];
}

interface StoriesEntry {
  /** Glob pattern for story files */
  directory: string;
  /** File patterns to match */
  files: string;
  /** Title prefix for stories */
  titlePrefix?: string;
}

Usage Examples:

import { vitestTransform } from "@storybook/csf-tools";

const csfCode = `
  export default { 
    title: 'Button',
    tags: ['component', 'ui']
  };
  export const Primary = { 
    args: { primary: true },
    tags: ['test', 'smoke']
  };
  export const Secondary = { 
    args: { primary: false },
    tags: ['test']  
  };
`;

const transformed = await vitestTransform({
  code: csfCode,
  fileName: './src/Button.stories.ts',
  configDir: '.storybook',
  stories: [
    { directory: './src', files: '**/*.stories.@(js|ts|jsx|tsx)' }
  ],
  tagsFilter: {
    include: ['test'],
    exclude: ['skip'],
    skip: ['wip']
  },
  previewLevelTags: ['autodocs']
});

console.log(transformed); // Vitest-compatible test code

Story Sort Parameter Extraction

Extract story sort parameters from Storybook preview configuration for proper test ordering.

/**
 * Extract story sort parameter from preview configuration code
 * @param previewCode - Source code of preview.js or preview.ts
 * @returns Extracted sort function/configuration or undefined if not found
 */
function getStorySortParameter(previewCode: string): any;

Usage Examples:

import { getStorySortParameter } from "@storybook/csf-tools";

const previewCode = `
  export default {
    parameters: {
      options: {
        storySort: {
          order: ['Introduction', 'Example', 'Components', '*'],
          method: 'alphabetical'
        }
      }
    }
  };
`;

const sortConfig = getStorySortParameter(previewCode);
console.log(sortConfig); // { order: [...], method: 'alphabetical' }

Test Generation Patterns

Basic Test Generation

Transform a simple CSF file into Vitest tests:

// Input CSF file
const storyCode = `
  export default { title: 'Components/Button' };
  
  export const Default = {};
  export const WithText = { args: { children: 'Click me' } };
  export const Disabled = { args: { disabled: true } };
`;

// Transform for testing
const testCode = await vitestTransform({
  code: storyCode,
  fileName: 'Button.stories.ts',
  configDir: '.storybook',
  stories: [{ directory: './src', files: '**/*.stories.ts' }],
  tagsFilter: { include: [], exclude: [], skip: [] }
});

// Generated test structure:
// - Imports Vitest test functions
// - Imports testStory helper from Storybook
// - Creates test cases for each valid story
// - Handles story composition and execution

Tag-Based Filtering

Control which stories become tests using tags:

const taggedStories = `
  export default { 
    title: 'Button',
    tags: ['component'] 
  };
  
  export const Interactive = { 
    tags: ['test', 'interaction'],
    play: async ({ canvasElement }) => {
      // Interaction test
    }
  };
  
  export const Visual = { 
    tags: ['visual', 'skip-ci'] 
  };
  
  export const Documentation = { 
    tags: ['docs-only'] 
  };
`;

// Only include stories tagged for testing
const testCode = await vitestTransform({
  code: taggedStories,
  fileName: 'Button.stories.ts',
  configDir: '.storybook',
  stories: [{ directory: './src', files: '**/*.stories.ts' }],
  tagsFilter: {
    include: ['test'],        // Must have 'test' tag
    exclude: ['docs-only'],   // Exclude documentation-only stories
    skip: ['skip-ci']         // Skip in CI environment
  }
});

Story Title Generation

Handle automatic title generation based on file paths:

const storiesConfig = [
  {
    directory: './src/components',
    files: '**/*.stories.@(js|ts)',
    titlePrefix: 'Components'
  },
  {
    directory: './src/pages',
    files: '**/*.stories.@(js|ts)',
    titlePrefix: 'Pages'
  }
];

// Transform with automatic title generation
const testCode = await vitestTransform({
  code: storyCode,
  fileName: './src/components/Button/Button.stories.ts',
  configDir: '.storybook',
  stories: storiesConfig,
  tagsFilter: { include: [], exclude: [], skip: [] }
});

// Result: Stories get title "Components/Button/Button"

Preview-Level Tags

Apply tags at the preview level that affect all stories:

const testCode = await vitestTransform({
  code: storyCode,
  fileName: 'Button.stories.ts',
  configDir: '.storybook',
  stories: storiesConfig,
  tagsFilter: {
    include: ['test'],
    exclude: ['dev-only'],
    skip: []
  },
  previewLevelTags: ['autodocs', 'test-utils'] // Applied to all stories
});

Generated Test Structure

The Vitest transformation produces test files with the following structure:

Imports

// Generated imports
import { test, expect } from 'vitest';
import { testStory } from '@storybook/experimental-addon-test/internal/test-utils';
import { default as _meta, Primary, Secondary } from './Button.stories';

Test Guard

Prevents duplicate test execution when stories are imported:

// Generated guard to prevent duplicate execution
const isRunningFromThisFile = import.meta.url.includes(
  globalThis.__vitest_worker__?.filepath ?? expect.getState().testPath
);

if (isRunningFromThisFile) {
  // Test cases go here
}

Test Cases

Each valid story becomes a test case:

// Generated test cases
test('Primary', testStory('Primary', Primary, _meta, skipTags));
test('Secondary', testStory('Secondary', Secondary, _meta, skipTags));

Empty File Handling

Files with no valid tests get a skip block:

// Generated for files with no valid stories
describe.skip('No valid tests found');

Advanced Configuration

Custom Story Sorting

Extract and apply custom story sorting from preview configuration:

import { getStorySortParameter, vitestTransform } from "@storybook/csf-tools";

// Extract sort configuration
const previewCode = await fs.readFile('.storybook/preview.js', 'utf-8');
const sortConfig = getStorySortParameter(previewCode);

// Use in transformation (sorting logic is handled by Storybook core)
const testCode = await vitestTransform({
  code: storyCode,
  fileName: 'Button.stories.ts',
  configDir: '.storybook',
  stories: storiesConfig,
  tagsFilter: { include: ['test'], exclude: [], skip: [] }
});

Batch Processing

Transform multiple story files for testing:

import { glob } from 'glob';
import { vitestTransform } from "@storybook/csf-tools";
import { readFile, writeFile } from 'fs/promises';

async function generateTests(
  storyPattern: string, 
  outputDir: string,
  tagsFilter: TagsFilter
) {
  const storyFiles = glob.sync(storyPattern);
  
  for (const storyFile of storyFiles) {
    const code = await readFile(storyFile, 'utf-8');
    
    const testCode = await vitestTransform({
      code,
      fileName: storyFile,
      configDir: '.storybook',
      stories: [{ directory: './src', files: '**/*.stories.@(js|ts)' }],
      tagsFilter
    });
    
    const testFile = storyFile
      .replace('.stories.', '.test.')
      .replace('./src/', `${outputDir}/`);
      
    await writeFile(testFile, testCode as string);
    console.log(`Generated: ${testFile}`);
  }
}

// Generate test files for all stories
await generateTests(
  './src/**/*.stories.ts',
  './tests/generated',
  { include: ['test'], exclude: ['visual'], skip: ['wip'] }
);

Testing Utilities

The transformation system integrates with Storybook's testing utilities:

testStory Function

The generated tests use the testStory helper function:

// From @storybook/experimental-addon-test/internal/test-utils
function testStory(
  exportName: string,
  story: any,
  meta: any,
  skipTags: string[]
): () => Promise<void>;

This function handles:

  • Story composition and rendering
  • Play function execution
  • Tag-based skipping
  • Error handling and reporting
  • Cleanup after test execution

Install with Tessl CLI

npx tessl i tessl/npm-storybook--csf-tools

docs

config-management.md

csf-processing.md

index.md

story-enhancement.md

testing-integration.md

tile.json