Interactive Angular Component Testing with Storybook and Vitest

Published: (January 14, 2026 at 12:36 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Storybook is a frontend workshop for building UI components and pages in isolation. Using Storybook together with Vitest enables interactive component testing.

This guide walks you through setting up Storybook with Angular using Vite and the @storybook/addon-vitest package for interactive component testing.

1. Setting up Storybook

If you don’t have Storybook set up yet, run the following command to initialise it for an Angular project:

npx storybook@latest init --type angular

This installs the necessary Storybook dependencies and creates a Storybook configuration with example components (including a Button component). Follow the prompts and commit the generated files.

2. Configuring Storybook to use Vite with Analog

Install the Angular integration for Analog

npm install @analogjs/storybook-angular @angular/animations zone.js --save-dev

Update .storybook/main.ts

import { StorybookConfig } from '@analogjs/storybook-angular';

const config: StorybookConfig = {
  stories: [
    '../src/**/*.mdx',
    '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@analogjs/storybook-angular',
    options: {},
  },
};

export default config;

Adjust the Storybook targets in angular.json

{
  "storybook": {
    "builder": "@analogjs/storybook-angular:start-storybook",
    "options": {
      "configDir": ".storybook",
      "styles": ["src/styles.css"],
      "experimentalZoneless": true
    }
  },
  "build-storybook": {
    "builder": "@analogjs/storybook-angular:build-storybook",
    "options": {
      "configDir": ".storybook",
      "styles": ["src/styles.css"],
      "experimentalZoneless": true
    }
  }
}

Run Storybook to verify the setup

npm run storybook

You should see Storybook launching with the example components.

3. Setting up Vitest for Interaction Testing

Install Vitest‑related dependencies

npm install @analogjs/vitest-angular @storybook/addon-vitest vitest @vitest/browser-playwright --save-dev

Add the Vitest addon to .storybook/main.ts

import { StorybookConfig } from '@analogjs/storybook-angular';

const config: StorybookConfig = {
  stories: [
    '../src/**/*.mdx',
    '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-vitest',
  ],
  framework: {
    name: '@analogjs/storybook-angular',
    options: {},
  },
};

export default config;

Create the Vitest setup file

.storybook/vitest.setup.ts

import '@angular/compiler';
import { setProjectAnnotations } from '@analogjs/storybook-angular/testing';
import { beforeAll } from 'vitest';
import * as projectAnnotations from './preview';

const project = setProjectAnnotations([projectAnnotations]);

beforeAll(project.beforeAll);

Update Storybook’s TypeScript config

.storybook/tsconfig.json

{
  "extends": "../tsconfig.app.json",
  "compilerOptions": {
    "types": ["node"],
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true
  },
  "exclude": ["../src/test.ts", "../src/**/*.spec.ts"],
  "include": [
    "../src/**/*.stories.*",
    "./preview.ts",
    "./vitest.setup.ts"
  ],
  "files": ["./typings.d.ts"]
}

Add a Vitest configuration file

vitest.config.ts

/// 
import { defineConfig } from 'vite';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';

const dirname =
  typeof __dirname !== 'undefined'
    ? __dirname
    : path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  test: {
    projects: [
      {
        extends: true,
        plugins: [
          storybookTest({
            configDir: path.join(dirname, '.storybook'),
          }),
        ],
        test: {
          name: 'storybook',
          browser: {
            enabled: true,
            headless: true,
            provider: playwright(),
            instances: [{ browser: 'chromium' }],
          },
          setupFiles: ['.storybook/vitest.setup.ts'],
        },
      },
    ],
  },
});

Install Playwright browser binaries

npx playwright install chromium

Add a test-storybook target to angular.json

{
  "test-storybook": {
    "builder": "@analogjs/vitest-angular:test",
    "options": {
      "configFile": "vitest.config.ts"
    }
  }
}

Add a script to package.json

{
  "scripts": {
    "test-storybook": "ng run your-app:test-storybook"
  }
}

Replace your-app with the actual name of your Angular project.

4. Writing Interaction Tests

The Button component generated by Storybook’s initial setup is a good candidate for interaction testing. Below is an example of adding a play function that uses Storybook’s testing utilities.

import type { Meta, StoryObj } from '@storybook/angular';
import { fn, expect, userEvent, within } from '@storybook/test';
import { Button } from './button.component';

const meta: Meta = {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    onClick: { action: 'clicked' },
  },
};

export default meta;

type Story = StoryObj;

export const Primary: Story = {
  args: {
    label: 'Button',
    primary: true,
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: /button/i });

    await userEvent.click(button);
    await expect(args.onClick).toHaveBeenCalled();
  },
};

Running npm run test-storybook will execute the interaction test against the Storybook instance.

Below is a more extensive example that includes additional stories and demonstrates the play function in different scenarios.

import type { Meta, StoryObj } from '@storybook/angular';
import { within, userEvent, expect } from '@storybook/testing-library';
import { fn } from '@storybook/addon-actions';
import { Button } from './button.component';

const meta: Meta = {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
  args: { onClick: fn() },
};

export default meta;
type Story = StoryObj;

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: /Button/i });

    await userEvent.click(button);
    await expect(args.onClick).toHaveBeenCalled();
  },
};

export const Secondary: Story = {
  args: {
    label: 'Button',
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: /Button/i });

    await expect(button).toBeInTheDocument();
    await expect(button).toHaveClass('storybook-button--secondary');
  },
};

The play function allows you to:

  • Query elements using within(canvasElement)
  • Simulate user interactions with userEvent
  • Make assertions with expect
  • And more …

Running Tests

Run your interaction tests with:

npm run test-storybook

This runs all stories with play functions as tests in a real browser using Playwright, giving you confidence that your components work as expected.

You can also run tests directly in the Storybook UI:

  1. Start Storybook.
  2. Use the Run Tests button in the sidebar, or navigate to a story to see interaction tests run automatically in the Interactions panel.

Conclusion

Using Storybook with Angular and Vite provides faster builds and a better developer experience. Adding Vitest for interaction testing lets you verify component behavior directly in your stories.

See the example repository with a full Storybook setup.

If you enjoyed this post, click the ❤️ so other people will see it. Follow AnalogJS and me on Twitter/X, and subscribe to my YouTube Channel for more content!

Back to Blog

Related posts

Read more »

Building a Tailwind CSS Dropdown Menu

Building a Tailwind CSS Dropdown Menu !Cover image for Building a Tailwind CSS Dropdown Menuhttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover...