DEV Community

Cover image for Interactive Angular Component Testing with Storybook and Vitest
Brandon Roberts
Brandon Roberts

Posted on

Interactive Angular Component Testing with Storybook and Vitest

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

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

Setting up Storybook

If you don't have Storybook setup already, run the following command to initialize Storybook for your project:

npx storybook@latest init --type angular
Enter fullscreen mode Exit fullscreen mode

This installs the necessary Storybook dependencies and sets up a Storybook with example components including a Button component.

Follow the provided prompts, and commit your changes.

Setting up Storybook to use Vite with Analog

Install the AnalogJS Storybook Angular integration:

npm install @analogjs/storybook-angular @angular/animations zone.js --save-dev
Enter fullscreen mode Exit fullscreen mode

Update the .storybook/main.ts file to use the @analogjs/storybook-angular package:

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;
Enter fullscreen mode Exit fullscreen mode

Update the angular.json storybook targets to use the @analogjs/storybook-angular builders:

"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
  }
}
Enter fullscreen mode Exit fullscreen mode

Run Storybook to verify everything works:

npm run storybook
Enter fullscreen mode Exit fullscreen mode

Setting up Vitest for Interaction Testing

Install the Vitest addon and dependencies:

npm install @analogjs/vitest-angular @storybook/addon-vitest vitest @vitest/browser-playwright --save-dev
Enter fullscreen mode Exit fullscreen mode

Add the addon to your .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;
Enter fullscreen mode Exit fullscreen mode

Create a .storybook/vitest.setup.ts file:

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);
Enter fullscreen mode Exit fullscreen mode

Update .storybook/tsconfig.json to include the setup file:

{
  "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"]
}
Enter fullscreen mode Exit fullscreen mode

Create a vitest.config.ts file in your project root:

/// <reference types="vitest/config" />
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'],
        },
      },
    ],
  },
});
Enter fullscreen mode Exit fullscreen mode

Install Playwright browser binaries:

npx playwright install chromium
Enter fullscreen mode Exit fullscreen mode

Add the test-storybook target to your angular.json:

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

Add a test script to your package.json:

"scripts": {
  "test-storybook": "ng run your-app:test-storybook"
}
Enter fullscreen mode Exit fullscreen mode

Writing Interaction Tests

The Button component from Storybook's initial setup is a great example for interaction testing. Here's how to add a play function to test user interactions:

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

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

export default meta;
type Story = StoryObj<Button>;

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');
  },
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. Start Storybook and 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

https://github.com/brandonroberts/angular-vite-storybook

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!

Top comments (0)