Interactive Angular Component Testing with Storybook and Vitest
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:
- Start Storybook.
- 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!