Storybook Component/Interactions Testing

Using storybook 7? Having trouble implementing component/interaction testing? This is the guide for you.

Storybook Component/Interactions Testing

Couple of months ago at work, we thought about a testing solution for handling the testability of our common react components being used in many different applications. While it might be trivial these days achieving it with Storybook (using vitest or even playwright ct library), working with legacy Storybook v7, I found out that it might require some tweaking. The annoying part was actually getting it working both in Storybook's interactions tab (which is a storybook addon) and in the CLI as in almost any common test runner.

When you're stuck on Storybook v7 (often due to enterprise constraints), setting up automated interaction testing becomes a bit more complex than the modern v8+ experience. Some of the challenges I faced were limited tooling compatibility, some extra configurations, error messages and debugging, and more.

The goal was simple: execute the interaction tests defined in play functions directly from the CLI or from Storybook's UI.

The Component Context: Complex Interactions Need Complex Testing

To understand why this testing setup was so crucial, let me briefly introduce the component that drove this need. A Suggestion component which is a common component in many applications, and has a lot of different states and interactions.

  • Multi-step user flows (type → filter → select → validate)
  • Keyboard navigation through options with proper focus management
  • Dynamic option management (add, edit, delete suggestions in real-time)
  • Complex validation states with async feedback
  • Multiple selection modes with chip management
  • Loading states during async operations

Before finding the solution, I tried several approaches that are commonly recommended but fail with v7:

Attempt 1: @storybook/addon-vitest

npm install @storybook/addon-vitest

I cannot emphasize on how vitest is a quality of life game changer in terms of performance, therefore it was my first choice, but this addon is designed for Storybook v8+ and caused immediate startup crashes. Sadly the addon expects internal APIs that don't exist in v7's architecture.

Attempt 2: Newer @storybook/test-runner Versions

npm install @storybook/test-runner@latest

Runtime errors and compatibility issues. The newer versions expect v8 features and fail with cryptic Playwright errors (which is its dependency).


After extensive trial and error, I found the sweet spot: @storybook/test-runner version 0.13.0. This specific version is the last one with solid Storybook v7 compatibility.

Complete Installation Steps

  1. Install the test runner and dependencies:
yarn add -D @storybook/test-runner@0.13.0
yarn add -D @storybook/addon-interactions@^7.6.0
yarn add -D @storybook/jest@^0.2.0
yarn add -D @storybook/testing-library@^0.2.0
  1. Add the script to package.json:
{
  "scripts": {
    "test:component": "test-storybook"
  }
}
  1. Eject the configuration (critical step):
yarn test:component --eject

Why This Version Matters

Version 0.13.0 is crucial because:

  • Last v7-compatible release before the v8 transition
  • Stable Playwright integration without modern API dependencies
  • Proven Jest configuration that works with v7's architecture
  • Mature error handling for complex interaction scenarios
  • No dependency on internal v8 APIs that don't exist in v7

Because the default configuration doesn't work out of the box, you must eject and customize:

This creates the essential test-runner-jest.config.js:

const { getJestConfig } = require('@storybook/test-runner')

/**
 * @type {import('@jest/types').Config.InitialOptions}
 */
module.exports = {
  // The default configuration comes from @storybook/test-runner
  ...getJestConfig(),
  /** Add your own overrides below
   * @see https://jestjs.io/docs/configuration
   */
  testEnvironmentOptions: {
    'jest-playwright': {
      collectCoverage: true,
      contextOptions: {},
      exitOnPageError: false, // Critical for preventing non-critical page errors from failing tests
    },
  },
}

Without this configuration, you'll encounter the infamous error:

TypeError: Cannot read properties of undefined (reading 'goto')

This happens because the Playwright page object isn't properly initialized in the default v7 environment setup.

Essential Storybook Configuration

Your .storybook/main.ts must include the interactions addon:

import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
  stories: [
    '../src/**/*.stories.mdx',
    '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-docs',
    '@storybook/addon-interactions', // ← Critical for interaction testing
  ],
  framework: {
    name: '@storybook/react-vite', // or '@storybook/react-webpack5'
    options: {},
  },
}
export default config

Required Dependencies

Your package.json needs these exact versions:

{
  "devDependencies": {
    "@storybook/addon-interactions": "^7.6.0",
    "@storybook/jest": "^0.2.0",
    "@storybook/test-runner": "0.13.0",
    "@storybook/testing-library": "^0.2.0"
  }
}

Version compatibility is critical:

  • @storybook/test-runner@0.13.0 - Last v7-compatible version
  • @storybook/jest@0.2.0 - Provides Jest matchers for Storybook
  • @storybook/testing-library@0.2.0 - Testing utilities for interactions

Story File Setup

Your story files need proper imports for interaction testing:

import type { Meta, StoryObj } from '@storybook/react'
import {
  configure,
  screen,
  within,
  userEvent,
} from '@storybook/testing-library'
import { expect } from '@storybook/jest'

// Configure testing library
configure({ testIdAttribute: 'data-testid' })

// Your component and story setup
const meta: Meta<typeof YourComponent> = {
  title: 'Components/YourComponent',
  component: YourComponent,
}

export default meta
type Story = StoryObj<typeof YourComponent>

How the Test Runner Works Under the Hood

Understanding the mechanics helps debug issues and optimize performance:

  1. Browser Automation: Uses Playwright to launch a headless browser (Chromium by default)
  2. Story Navigation: Automatically discovers and navigates to each story's dedicated URL
  3. Test Execution:
    • Runs a basic "smoke test" for every story (ensures rendering without errors) - By default.
    • Executes play function interactions for stories that have them
  4. Result Aggregation: Collects results from all tests and reports them to the CLI

test runner component tests

Writing Interaction Tests

Here's the complete test structure that handles all the complex scenarios:

export const InteractionTests: StoryObj<typeof InteractionTestComponent> = {
  render: () => <InteractionTestComponent />,
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement)

    await step('Interactive Typing Test', async () => {
      const wrapper = await canvas.findByTestId('default-suggestion-input')
      const input = within(wrapper).getByRole('textbox')
      await userEvent.clear(input)
      await userEvent.type(input, 'Marg')
      await expect(input).toHaveValue('Marg')
    })

    await step('Keyboard Navigation Test', async () => {
      const wrapper = await canvas.findByTestId('default-suggestion-input')
      const input = within(wrapper).getByRole('textbox')
      await userEvent.click(input)
      await expect(input).toHaveFocus()
      await userEvent.keyboard('{ArrowDown}')
      await userEvent.keyboard('{ArrowDown}')
      await userEvent.keyboard('{ArrowUp}')
      await userEvent.keyboard('{Enter}')
      await expect(input).toHaveFocus()
    })

    await step('Multiple Selection Test', async () => {
      const wrapper = await canvas.findByTestId(
        'states-suggestion-multiple-input',
      )
      const input = within(wrapper).getByRole('textbox')
      await userEvent.click(input)

      await userEvent.type(input, 'React')
      await userEvent.keyboard('{ArrowDown}')
      await userEvent.keyboard('{Enter}')

      await userEvent.type(input, 'Vue')
      await userEvent.keyboard('{ArrowDown}')
      await userEvent.keyboard('{Enter}')

      const reactChip = await canvas.findByTestId(
        'states-suggestion-multiple-input.tag.react',
      )
      const vueChip = await canvas.findByTestId(
        'states-suggestion-multiple-input.tag.vue',
      )

      expect(reactChip).toBeInTheDocument()
      expect(vueChip).toBeInTheDocument()
    })

    await step('Component States Test', async () => {
      const loadingWrapper = await canvas.findByTestId(
        'loading-state-suggestion',
      )
      const progress = within(loadingWrapper).getByRole('progressbar')
      expect(progress).toBeInTheDocument()

      const errorWrapper = await canvas.findByTestId('error-state-suggestion')
      const warningIcon = within(errorWrapper).getByText('warning')
      expect(warningIcon).toBeInTheDocument()
    })

    await step('Accessibility Test', async () => {
      const wrapper = await canvas.findByTestId('default-suggestion-input')
      const input = within(wrapper).getByRole('textbox')
      expect(input).toHaveAttribute('aria-autocomplete', 'list')
      const label = await canvas.findByText('Default Suggestion')
      expect(label).toBeInTheDocument()
    })
  },
}

The Execution Workflow: Two-Terminal Setup

The testing workflow requires careful orchestration:

# Terminal 1: Start Storybook (and keep it running)
yarn storybook
# Wait for "Local: http://localhost:6006/"

# Terminal 2: Run the tests
yarn test:component

Critical timing: The test runner expects Storybook to be fully loaded before starting. Rushing this step causes connection errors.

Debugging Test Failures in Storybook v7

Storybook v7's error messages can be overwhelming. Here's a real failure example and how to decode it (Just changed the role to a wrong role for the sake of the example):

FAIL   browser: chromium  src/schemaRender/stories/base/suggestion/index.stories.tsx
  Development/Base/Suggestion
    InteractionTests
      ✕ play-test (617 ms)

  ● Development/Base/Suggestion › InteractionTests › play-test

    page.evaluate: StorybookTestRunnerError:
    An error occurred in the following story. Access the link for full output:
    http://127.0.0.1:6006/?path=/story/development-base-suggestion--interaction-tests&addonPanel=storybook/interactions/panel

    Message:
     Unable to find an accessible element with the role "textbo"

    Here are the accessible roles:
      textbox:
      Name "":
      <input
        id="someidinput"
        placeholder=""
        type="text"
        value="Pizza"
      />

Debugging Strategy:

  1. Use the provided URL: The error includes a direct link to the failing story
  2. Check element selectors: The error shows available roles - use these for your queries
  3. Verify timing: Often failures are due to elements not being ready
  4. Test manually first: Run the interaction manually in Storybook before automating

The Screen vs Canvas Problem

A common mistake in Storybook v7 interaction tests:

// ❌ Wrong - causes warnings
const element = screen.getByRole('textbox')

// ✅ Correct - use canvas
const canvas = within(canvasElement)
const element = canvas.getByRole('textbox')

The screen object works but generates warnings about Testing Library best practices.

Storybook v7 vs v8: What You're Missing

If you're stuck on v7, here's what v8+ offers that you can't easily replicate:

  • Built-in Vitest integration with @storybook/addon-vitest
  • Improved error messages with better stack traces
  • Faster test execution through optimized browser handling
  • Better TypeScript support in test configurations
  • Streamlined setup with working defaults

But with the setup I've outlined, you can achieve 90% of the functionality with v7.

So what we achieved here?

Automated Test Coverage

  • Interaction testing for complex user flows
  • Accessibility validation built into every test
  • Regression prevention through CI integration
  • Visual testing combined with functional testing

Development Benefits

  • Faster debugging with direct Storybook links in failures
  • Confident refactoring knowing interactions are covered
  • Documentation through tests showing expected behavior
  • Onboarding acceleration for new team members

Integrating with CI/CD

For production use, add this to your CI pipeline:

# .github/workflows/storybook-tests.yml
name: Storybook Tests
on: [push, pull_request]

jobs:
  interaction-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Build Storybook
        run: yarn build-storybook --quiet

      - name: Serve Storybook and run tests
        run: |
          npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
            "npx http-server storybook-static --port 6006 --silent" \
            "npx wait-on http://127.0.0.1:6006 && yarn test:component"

Best Practices for Storybook v7 Testing

1. Test Structure

// ✅ Good: Clear steps with descriptive names
await step(
  'User can select multiple options with keyboard navigation',
  async () => {
    // Test implementation
  }
)

// ❌ Bad: Vague step descriptions
await step('Test selection', async () => {
  // Test implementation
})

2. Element Selection

// ✅ Good: Use data-testid attributes
const input = canvas.getByTestId('suggestion-input')

// ❌ Bad: Fragile CSS selectors
const input = canvas.querySelector('.someCssClass')

3. Async Handling

// ✅ Good: Proper async waiting
await userEvent.type(input, 'search term')
await waitFor(() => expect(dropdown).toBeVisible())

// ❌ Bad: Not waiting for async operations
userEvent.type(input, 'search term')
expect(dropdown).toBeVisible() // Might fail due to timing

storybook interactions tests in storybook ui


That's it. I would love to know and hear if you have any other tips or tricks for Storybook v7 testing or even better solutions for such cases.



Tags:
Share: