CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--angular

Storybook for Angular: Develop, document, and test UI components in isolation

Pending
Overview
Eval results
Files

portable-stories.mddocs/

Portable Stories

Utilities for using Storybook stories outside of the Storybook environment, such as in testing frameworks and other applications.

Capabilities

setProjectAnnotations Function

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>;

Usage Examples

Testing with Jest

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();
  });
});

Testing with Playwright

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
  });
});

Snapshot Testing

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();
  });
});

Custom Testing Utilities

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');

Integration Testing

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');
  });
});

Setup Patterns

Global Setup for Testing Framework

// 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();
});

Per-Test Setup

// 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
      ],
    },
  ]);
});

Best Practices

Configuration Management

  • Set up setProjectAnnotations once in your test setup file
  • Import the exact same preview configuration used by Storybook
  • Use array format for multiple configuration sources when needed

Performance Optimization

  • Avoid calling setProjectAnnotations repeatedly in individual tests
  • Configure TestBed once per test suite rather than per test
  • Use composeStories to precompile all stories from a file

Type Safety

  • Import story types to maintain type safety in tests
  • Use TypeScript strict mode for better error detection
  • Leverage Angular's dependency injection in test scenarios

Integration with CI/CD

  • Use portable stories for visual regression testing
  • Include story-based tests in your CI pipeline
  • Generate test coverage reports that include story usage

Install with Tessl CLI

npx tessl i tessl/npm-storybook--angular

docs

cli-builders.md

decorators.md

framework-config.md

index.md

portable-stories.md

story-types.md

template-utilities.md

tile.json