Storybook Test Runner

repository·next·Indexed 42 days ago

https://github.com/storybookjs/test-runner

A tool that converts Storybook stories into executable tests using Jest and Playwright. It enables automated smoke testing for component rendering and interaction testing via Storybook's `play` functions. Supports code coverage via Istanbul, custom Jest configuration through ejection, and experimental tag-based story filtering. Version 0.24.0 is compatible with Storybook ^10.0.0.

Tokens
25.3K
Snippets
52
Records
75
Agent score
55%

What's inside Storybook Test Runner

  1. What is the Storybook Test Runner?

    next

    The Storybook Test Runner turns your stories into executable tests. It is powered by Jest and Playwright, enabling multi-browser testing.

    Key capabilities include:

    • Smoke testing
    • DOM snapshot testing
    • Accessibility (A11y) testing
    • Visual Regression testing
    • Interaction testing
    • Parallel testing
  2. Filter stories using tags (experimental)

    next

    You can use Storybook tags to include, exclude, or skip specific stories during test execution.

    1. Annotate Stories

    Add tags to your component meta or individual stories. Tags must be defined inline as an array of strings due to Storybook's static analysis requirements (do not import constants for tags).

    • Meta tags: Apply to all stories in a file.
    • Story tags: Apply only to that specific story.
    • Negation: Use !tagname to remove a tag (e.g., tags: ['!test']).

    2. Apply Filters

    You can filter these tags in three ways:

    • CLI Flags: Use --includeTags, --excludeTags, or --skipTags. CLI flags take precedence over configuration file settings.
    • Config File: Define tags in your ejected test-runner-jest.config.js.

    Difference between Skip and Exclude:

    • --excludeTags: The story is completely ignored and does not appear in the output.
    • --skipTags: The story is marked as "skipped" in the CLI output, which is useful for temporarily disabling tests.
    const meta = {
      component: Button,
      tags: ['atom'],
    };
    export default meta;
    
    // will inherit tags from project and meta to be ['dev', 'test', 'atom']
    export const Primary = {};
    
    export const Secondary = {
      // will combine with project and meta tags to be ['dev', 'test', 'atom', 'design']
      tags: ['design'],
    };
    
    export const Tertiary = {
      // will combine with project and meta tags to be ['dev', 'atom']
      tags: ['!test'],
    };
  3. How index.json mode works

    next

    The test runner has two modes:

    1. Default mode: Transforms your local story files into tests. This requires access to the source code and assumes local files are in sync with the Storybook.
    2. index.json mode: Runs directly against the Storybook's static index data (index.json or stories.json).

    When to use index.json mode:

    • When running against a deployed Storybook (ensures the index is in sync with the deployed version).
    • When you do not have access to the source code.
    • When testing .mdx stories or custom CSF dialects (e.g., addon-svelte-csf) that cannot be run in default mode.

    Note: index.json mode is not compatible with watch mode.

  4. Compare portable stories vs. test-runner

    next

    When deciding how to migrate from Storyshots, choose based on your testing requirements:

    Portable Stories (Node/JSDOM)

    Best if you want to keep a single test file running in a JSDOM environment, similar to the Storyshots experience.

    • Pros: Similar workflow to Storyshots.
    • Cons: Not testing in a real browser; requires mocking browser utilities (canvas, window APIs, etc.); debugging is more difficult as you cannot access a browser.

    Test Runner (Playwright/Browser)

    Best if you need a more powerful, modern testing setup.

    • Pros: Runs against a real browser; uses Playwright; provides multi-browser support.
    • Cons: Different architectural approach than Storyshots.
  5. Use Test Hooks to extend the test lifecycle

    next

    The test runner executes a story's play function in the browser. For tasks that require Node.js access (like visual snapshots or DOM snapshots), you can override global test hooks in .storybook/test-runner.js.

    There are three primary hooks:

    • setup: Runs once in Node before all tests. Use this for global configuration (e.g., extending Jest expect).
    • preVisit: Runs in the test before a story renders. Use this to configure the Playwright Page (e.g., setting viewport size).
    • postVisit: Runs in the test after a story renders. Use this for assertions like DOM or image snapshotting.

    Note: While these hooks provide access to the Playwright Page object, it is encouraged to perform as much testing as possible within the story's play function.

    // .storybook/test-runner.ts
    import type { TestRunnerConfig } from '@storybook/test-runner';
    
    const config: TestRunnerConfig = {
      async setup() {
        // Runs once in Node before all tests
      },
      async preVisit(page, context) {
        // Runs before the story renders
      },
      async postVisit(page, context) {
        // Runs after the story renders
      },
    };
    export default config;
  6. Detect the test runner in the browser

    next

    The test runner adds a StorybookTestRunner entry to the browser's user agent. You can detect this within a story's render function, play function, or preview.js to conditionally disable certain features or behaviors when running in a test environment.

    export const MyStory = {
      render: () => {
        const isTestRunner = window.navigator.userAgent.match(/StorybookTestRunner/);
        return (
          <div>
            <p>Is this story running in the test runner?</p>
            <p>{isTestRunner ? 'Yes' : 'No'}</p>
          </div>
        );
      },
    };
  7. How Storybook test runner works

    next

    The Storybook test runner uses Jest as the test runner and Playwright as the browser automation framework. It transforms every .stories file into a Jest spec file, where each story becomes an individual test executed in a headless browser.

    Test Types:

    • Smoke Tests: For stories without a play function, the runner verifies the story renders without errors.
    • Interaction Tests: For stories with a play function, the runner executes the function and validates that all assertions pass.

    If a test fails, the runner provides an error message and a direct link to the failing story for visual debugging.

  8. Migrate Storyshots features to Test Runner

    next

    If you were using specific utilities from @storybook/addon-storyshots or @storybook/addon-storyshots-puppeteer, you can achieve similar results in @storybook/test-runner using different patterns:

    • Smoke testing: Now built-in by default; no configuration required.
    • Accessibility testing: Implement using the test-runner's accessibility testing pattern (refer to project documentation).
    • Image snapshot testing: Implement using the test-runner's image snapshot pattern (refer to project documentation).
    • DOM Snapshot testing: Implement using the test-runner's DOM snapshot HTML pattern (refer to project documentation).
  9. Perform image snapshot testing

    next

    To perform visual regression testing, use jest-image-snapshot within the postVisit hook.

    It is highly recommended to use the waitForPageReady(page) utility from @storybook/test-runner before taking the screenshot to ensure fonts and other assets are fully loaded. You can use context.id as a customSnapshotIdentifier to ensure unique filenames for each story.

    // .storybook/test-runner.ts
    import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner';
    import { toMatchImageSnapshot } from 'jest-image-snapshot';
    
    const customSnapshotsDir = `${process.cwd()}/__snapshots__`;
    
    const config: TestRunnerConfig = {
      setup() {
        expect.extend({ toMatchImageSnapshot });
      },
      async postVisit(page, context) {
        await waitForPageReady(page);
    
        const image = await page.screenshot();
        expect(image).toMatchImageSnapshot({
          customSnapshotsDir,
          customSnapshotIdentifier: context.id,
        });
      },
    };
    export default config;"
  10. Eject configuration to customize the test runner

    next

    The test runner is built on Jest. While it works out of the box, you can eject its configuration to gain full control over Jest and jest-playwright settings.

    To eject, run: test-storybook --eject

    This creates a test-runner-jest.config.js file in your project root (or in your Storybook config directory if you use --config-dir). This file allows you to:

    1. Override Jest options: Such as testTimeout, maxWorkers, or reporters.
    2. Configure Playwright: Pass testEnvironmentOptions to configure the jest-playwright environment.

    Note: The test runner uses an internal version of Jest. If you already have a compatible version of Jest in your project, the runner will use yours instead of installing a duplicate.

    // ./test-runner-jest.config.js
    const { getJestConfig } = require('@storybook/test-runner');
    
    const testRunnerConfig = getJestConfig();
    
    /**
     * @type {import('@jest/types').Config.InitialOptions}
     */
    module.exports = {
      // The default Jest configuration comes from @storybook/test-runner
      ...testRunnerConfig,
      /** Add your own overrides below
       * @see https://jestjs.io/docs/configuration
       */
      testTimeout: 20000, // default timeout is 15s
    };
  11. Perform DOM (HTML) snapshot testing

    next

    You can use Playwright's APIs within the postVisit hook to capture the HTML of a story and compare it against a snapshot using expect(innerHTML).toMatchSnapshot().

    In Storybook 6.x, the selector for the story container is #root. In modern versions, use #storybook-root.

    Note on storage: When running with --stories-json, snapshots are stored in a temporary folder. To store them in your working directory, you must use --eject and configure a custom snapshotResolver in your Jest configuration.

    // .storybook/test-runner.ts
    import type { TestRunnerConfig } from '@storybook/test-runner';
    
    const config: TestRunnerConfig = {
      async postVisit(page, context) {
        // The #storybook-root element wraps the story.
        const elementHandler = await page.$('#storybook-root');
        const innerHTML = await elementHandler.innerHTML();
        expect(innerHTML).toMatchSnapshot();
      },
    };
    export default config;"
  12. Migrate to Portable stories

    next

    Portable stories allow you to reuse stories in Node-based tests (using JSDOM) with Jest or Vitest. This is the simplest migration path if you use Storyshots extensively with complex mocking mechanisms and snapshot serializers.

    Supported Renderers:

    • React
    • React Native (Note: does NOT support the react-native-web addon in this mode)
    • Vue3

    Refer to the specific migration guide for detailed steps: ./MIGRATION.portable-stories.md.