CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--web-components

Storybook Web Components renderer for developing, documenting, and testing UI components in isolation

Pending
Overview
Eval results
Files

portable-stories.mddocs/

Portable Stories

Testing utilities for using Storybook stories outside of the Storybook environment, particularly useful for unit testing and integration testing. Portable stories allow you to reuse your story definitions in testing frameworks like Jest, Vitest, or Playwright.

Capabilities

Project Configuration

Function for setting global project annotations to enable portable stories functionality.

/**
 * Sets the global config for portable stories functionality.
 * Should be run once in test setup to apply global configuration like decorators.
 * 
 * @param projectAnnotations - Project annotations (e.g., from .storybook/preview)
 * @returns Normalized project annotations for web components renderer
 */
function setProjectAnnotations(
  projectAnnotations:
    | NamedOrDefaultProjectAnnotations<any>
    | NamedOrDefaultProjectAnnotations<any>[]
): NormalizedProjectAnnotations<WebComponentsRenderer>;

Usage Patterns

Test Setup

Configure portable stories in your test setup file:

// setup-file.js or setup-tests.js
import { setProjectAnnotations } from "@storybook/web-components";
import * as projectAnnotations from "../.storybook/preview";

// Apply global project configuration
setProjectAnnotations(projectAnnotations);

Testing Individual Stories

Use stories directly in your tests by importing them and rendering manually:

// button.test.js
import { render } from "@testing-library/dom";
import { setProjectAnnotations } from "@storybook/web-components";
import * as projectAnnotations from "../.storybook/preview";
import Meta, { Primary, Secondary } from "./Button.stories";

// Set up project annotations once
setProjectAnnotations(projectAnnotations);

describe("Button Component", () => {
  it("renders primary button correctly", () => {
    // Manually render the story
    const element = Primary.render ? Primary.render(Primary.args, {}) : document.createElement("my-button");
    if (Primary.args) {
      Object.entries(Primary.args).forEach(([key, value]) => {
        element.setAttribute(key, String(value));
      });
    }
    
    render(element, { container: document.body });
    
    const button = document.querySelector("my-button");
    expect(button).toBeInTheDocument();
    expect(button.getAttribute("variant")).toBe("primary");
  });

  it("handles custom args", () => {
    const customArgs = {
      ...Primary.args,
      label: "Custom Label",
      disabled: true,
    };
    
    const element = document.createElement("my-button");
    Object.entries(customArgs).forEach(([key, value]) => {
      element.setAttribute(key, String(value));
    });
    
    render(element, { container: document.body });
    const button = document.querySelector("my-button");
    expect(button.getAttribute("label")).toBe("Custom Label");
    expect(button.hasAttribute("disabled")).toBe(true);
  });

  it("applies decorators and global config", () => {
    // Global decorators from setProjectAnnotations are available
    // You'll need to manually apply them to your story rendering
    const element = document.createElement("my-button");
    if (Secondary.args) {
      Object.entries(Secondary.args).forEach(([key, value]) => {
        element.setAttribute(key, String(value));
      });
    }
    
    render(element, { container: document.body });
    
    expect(document.querySelector("my-button")).toBeInTheDocument();
  });
});

Testing Multiple Stories

Test multiple stories from a story file:

// button.test.js
import * as stories from "./Button.stories";

const storyExports = Object.entries(stories)
  .filter(([key]) => key !== 'default')
  .map(([name, story]) => ({ name, story }));

describe("All Button Stories", () => {
  storyExports.forEach(({ name, story }) => {
    it(`renders ${name} story correctly`, () => {
      const element = document.createElement("my-button");
      
      if (story.args) {
        Object.entries(story.args).forEach(([key, value]) => {
          element.setAttribute(key, String(value));
        });
      }
      
      render(element, { container: document.body });
      
      expect(document.querySelector("my-button")).toBeInTheDocument();
    });
  });
});

Integration with Testing Frameworks

Jest/Vitest Setup

// vitest.config.js
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    setupFiles: ["./src/test-setup.js"],
    environment: "jsdom",
  },
});

// src/test-setup.js
import { setProjectAnnotations } from "@storybook/web-components";
import * as projectAnnotations from "../.storybook/preview";

setProjectAnnotations(projectAnnotations);

Playwright Integration

// tests/stories.spec.ts
import { test, expect } from "@playwright/test";
import Meta, { Primary } from "../src/components/Button.stories";

test("visual regression test for primary button", async ({ page }) => {
  // Render story in the browser
  await page.setContent(`
    <html>
      <head>
        <script type="module" src="/src/components/my-button.js"></script>
      </head>
      <body>
        <my-button id="test-button"></my-button>
        <script type="module">
          const button = document.getElementById('test-button');
          const args = ${JSON.stringify(Primary.args)};
          Object.entries(args).forEach(([key, value]) => {
            button.setAttribute(key, String(value));
          });
        </script>
      </body>
    </html>
  `);
  
  await page.locator("my-button").waitFor();
  await expect(page).toHaveScreenshot("primary-button.png");
});

Advanced Configuration

Custom Project Annotations

// test-annotations.js
import type { Preview } from "@storybook/web-components";

const testAnnotations: Preview = {
  decorators: [
    // Test-specific decorators
    (story) => {
      const wrapper = document.createElement("div");
      wrapper.setAttribute("data-test-wrapper", "true");
      wrapper.appendChild(story());
      return wrapper;
    },
  ],
  parameters: {
    // Test-specific parameters
    docs: { disable: true },
  },
};

export default testAnnotations;

// test-setup.js
import { setProjectAnnotations } from "@storybook/web-components";
import previewAnnotations from "../.storybook/preview";
import testAnnotations from "./test-annotations";

// Combine preview and test annotations
setProjectAnnotations([previewAnnotations, testAnnotations]);

Conditional Configuration

// setup-file.js
import { setProjectAnnotations } from "@storybook/web-components";

// Different configs for different test environments
if (process.env.NODE_ENV === "test") {
  import("./test-preview").then((testConfig) => {
    setProjectAnnotations(testConfig.default);
  });
} else {
  import("../.storybook/preview").then((previewConfig) => {
    setProjectAnnotations(previewConfig.default);
  });
}

Benefits of Portable Stories

Consistency

  • Same Components: Test the exact same component definitions used in Storybook
  • Same Decorators: Global decorators and configurations are automatically applied
  • Same Args: Use the same argument validation and processing

Efficiency

  • Reuse Stories: No need to duplicate component setup in tests
  • Fast Feedback: Quick unit tests without running full Storybook
  • CI Integration: Run story-based tests in continuous integration

Coverage

  • All Scenarios: Easily test all story variants
  • Edge Cases: Include edge case stories in automated testing
  • Visual Testing: Use stories for visual regression testing

Troubleshooting

Common Issues

Stories not rendering correctly:

// Make sure setProjectAnnotations is called before using stories
import { setProjectAnnotations } from "@storybook/web-components";
import projectAnnotations from "../.storybook/preview";

// Call this ONCE in your test setup
setProjectAnnotations(projectAnnotations);

Missing decorators:

// Ensure your preview.js exports are properly structured
// .storybook/preview.js
export const decorators = [myDecorator];
export const parameters = { /* ... */ };

// Or use default export
export default {
  decorators: [myDecorator],
  parameters: { /* ... */ },
};

Web Components not defined:

// Make sure to register your custom elements in tests
import "./my-button"; // Import component definition

// Or register manually
customElements.define("my-button", MyButtonElement);

Install with Tessl CLI

npx tessl i tessl/npm-storybook--web-components

docs

advanced-functions.md

framework-integration.md

index.md

portable-stories.md

story-types.md

tile.json