Storybook for Angular: Develop, document, and test UI components in isolation
—
Utilities for using Storybook stories outside of the Storybook environment, such as in testing frameworks and other applications.
Sets global project annotations (preview configuration) for using stories outside of Storybook. This should be run once to apply global decorators, parameters, and other configuration.
/**
* Function that sets the globalConfig of your storybook. The global config is the preview module of
* your .storybook folder.
*
* It should be run a single time, so that your global config (e.g. decorators) is applied to your
* stories when using `composeStories` or `composeStory`.
*
* @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview')
* @returns Normalized project annotations
*/
declare function setProjectAnnotations(
projectAnnotations:
| NamedOrDefaultProjectAnnotations<any>
| NamedOrDefaultProjectAnnotations<any>[]
): NormalizedProjectAnnotations<AngularRenderer>;Set up portable stories for Jest testing:
// setup-tests.ts
import { setProjectAnnotations } from '@storybook/angular';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
// Import your global storybook preview configuration
import globalStorybookConfig from '../.storybook/preview';
// Set up global storybook configuration
setProjectAnnotations(globalStorybookConfig);
// Configure Angular TestBed
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule],
});
});Using stories in Jest tests:
// button.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { composeStories } from '@storybook/testing-angular';
import * as stories from './button.stories';
// Compose all stories from the story file
const { Primary, Secondary, Large } = composeStories(stories);
describe('ButtonComponent', () => {
let fixture: ComponentFixture<any>;
it('should render primary button correctly', async () => {
// Use the Primary story
const component = TestBed.createComponent(Primary.component);
component.componentInstance = { ...Primary.args };
fixture = component;
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Button');
expect(fixture.nativeElement.querySelector('.primary')).toBeTruthy();
});
it('should handle click events', async () => {
const component = TestBed.createComponent(Primary.component);
const clickSpy = jest.fn();
component.componentInstance = {
...Primary.args,
onClick: clickSpy
};
fixture = component;
fixture.detectChanges();
fixture.nativeElement.querySelector('button').click();
expect(clickSpy).toHaveBeenCalled();
});
});Use stories for end-to-end testing:
// setup-e2e.ts
import { setProjectAnnotations } from '@storybook/angular';
import globalStorybookConfig from '../.storybook/preview';
setProjectAnnotations(globalStorybookConfig);// button.e2e.spec.ts
import { test, expect } from '@playwright/test';
import { composeStories } from '@storybook/testing-angular';
import * as stories from './button.stories';
const { Primary, Secondary } = composeStories(stories);
test.describe('Button Component E2E', () => {
test('primary button should be clickable', async ({ page }) => {
// Navigate to a page that renders the Primary story
await page.goto('/storybook-iframe.html?id=button--primary');
const button = page.locator('button');
await expect(button).toBeVisible();
await expect(button).toHaveClass(/primary/);
await button.click();
// Assert expected behavior after click
});
});Use stories for visual regression testing:
// button.snapshot.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { composeStories } from '@storybook/testing-angular';
import * as stories from './button.stories';
const { Primary, Secondary, Large, Small } = composeStories(stories);
describe('Button Snapshots', () => {
let fixture: ComponentFixture<any>;
const renderStory = (story: any) => {
const component = TestBed.createComponent(story.component);
component.componentInstance = { ...story.args };
fixture = component;
fixture.detectChanges();
return fixture.nativeElement;
};
it('should match primary button snapshot', () => {
const element = renderStory(Primary);
expect(element).toMatchSnapshot();
});
it('should match secondary button snapshot', () => {
const element = renderStory(Secondary);
expect(element).toMatchSnapshot();
});
it('should match large button snapshot', () => {
const element = renderStory(Large);
expect(element).toMatchSnapshot();
});
it('should match small button snapshot', () => {
const element = renderStory(Small);
expect(element).toMatchSnapshot();
});
});Create reusable testing utilities with portable stories:
// story-test-utils.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { setProjectAnnotations } from '@storybook/angular';
import { composeStories } from '@storybook/testing-angular';
import globalStorybookConfig from '../.storybook/preview';
// Set up global configuration once
setProjectAnnotations(globalStorybookConfig);
export interface StoryTestOptions {
story: any;
props?: Record<string, any>;
detectChanges?: boolean;
}
export class StoryTestHelper {
private fixture: ComponentFixture<any>;
static create(options: StoryTestOptions): StoryTestHelper {
const helper = new StoryTestHelper();
helper.render(options);
return helper;
}
private render(options: StoryTestOptions): void {
const component = TestBed.createComponent(options.story.component);
component.componentInstance = {
...options.story.args,
...options.props
};
this.fixture = component;
if (options.detectChanges !== false) {
this.fixture.detectChanges();
}
}
get element(): HTMLElement {
return this.fixture.nativeElement;
}
get component(): any {
return this.fixture.componentInstance;
}
get fixture(): ComponentFixture<any> {
return this.fixture;
}
query(selector: string): HTMLElement | null {
return this.element.querySelector(selector);
}
queryAll(selector: string): NodeList {
return this.element.querySelectorAll(selector);
}
updateProps(props: Record<string, any>): void {
Object.assign(this.component, props);
this.fixture.detectChanges();
}
triggerEvent(selector: string, eventType: string, eventData?: any): void {
const element = this.query(selector);
if (element) {
const event = new Event(eventType);
if (eventData) {
Object.assign(event, eventData);
}
element.dispatchEvent(event);
this.fixture.detectChanges();
}
}
}
// Usage example:
// const helper = StoryTestHelper.create({ story: Primary });
// expect(helper.query('button')).toBeTruthy();
// helper.updateProps({ disabled: true });
// expect(helper.query('button')).toHaveAttribute('disabled');Test complete user workflows using multiple stories:
// user-workflow.spec.ts
import { TestBed } from '@angular/core/testing';
import { setProjectAnnotations } from '@storybook/angular';
import { composeStories } from '@storybook/testing-angular';
import * as formStories from './form.stories';
import * as buttonStories from './button.stories';
import * as modalStories from './modal.stories';
import globalStorybookConfig from '../.storybook/preview';
setProjectAnnotations(globalStorybookConfig);
const { DefaultForm } = composeStories(formStories);
const { Primary: PrimaryButton } = composeStories(buttonStories);
const { ConfirmationModal } = composeStories(modalStories);
describe('User Registration Workflow', () => {
it('should complete user registration flow', async () => {
// Step 1: Render registration form
const formComponent = TestBed.createComponent(DefaultForm.component);
formComponent.componentInstance = { ...DefaultForm.args };
const formFixture = formComponent;
formFixture.detectChanges();
// Step 2: Fill out form
const emailInput = formFixture.nativeElement.querySelector('input[type="email"]');
const passwordInput = formFixture.nativeElement.querySelector('input[type="password"]');
emailInput.value = 'test@example.com';
emailInput.dispatchEvent(new Event('input'));
passwordInput.value = 'password123';
passwordInput.dispatchEvent(new Event('input'));
formFixture.detectChanges();
// Step 3: Click submit button
const submitButton = formFixture.nativeElement.querySelector('button[type="submit"]');
expect(submitButton).not.toHaveAttribute('disabled');
submitButton.click();
formFixture.detectChanges();
// Step 4: Verify confirmation modal appears
const modalComponent = TestBed.createComponent(ConfirmationModal.component);
modalComponent.componentInstance = { ...ConfirmationModal.args };
const modalFixture = modalComponent;
modalFixture.detectChanges();
expect(modalFixture.nativeElement).toContainText('Registration Successful');
});
});// jest.setup.ts or vitest.setup.ts
import { setProjectAnnotations } from '@storybook/angular';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CommonModule } from '@angular/common';
// Import your global storybook configuration
import globalStorybookConfig from '../.storybook/preview';
// Apply global storybook configuration
setProjectAnnotations(globalStorybookConfig);
// Global Angular TestBed configuration
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
CommonModule,
BrowserAnimationsModule,
],
teardown: { destroyAfterEach: true },
}).compileComponents();
});// individual-test.spec.ts
import { setProjectAnnotations } from '@storybook/angular';
import { TestBed } from '@angular/core/testing';
// Test-specific configuration
beforeEach(() => {
setProjectAnnotations([
// Base global config
require('../.storybook/preview').default,
// Test-specific overrides
{
parameters: {
// Override parameters for this test suite
backgrounds: { default: 'light' },
},
decorators: [
// Additional test-specific decorators
],
},
]);
});setProjectAnnotations once in your test setup filesetProjectAnnotations repeatedly in individual testscomposeStories to precompile all stories from a fileInstall with Tessl CLI
npx tessl i tessl/npm-storybook--angular