Storybook Web Components renderer for developing, documenting, and testing UI components in isolation
—
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.
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>;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);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();
});
});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();
});
});
});// 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);// 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");
});// 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]);// 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);
});
}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