Testing utilities that allow you to reuse your stories in your unit tests
—
Core functionality for converting Storybook stories into testable React components with all decorators, parameters, and configurations applied.
Converts an individual story into a testable React component.
/**
* Composes a single story with meta and global config, returning a component with all decorators applied
* @param story - Story object or function from stories file
* @param meta - Meta object (default export) from stories file
* @param globalConfig - Optional global configuration, defaults to setProjectAnnotations config
* @returns Composed story component with story properties
*/
function composeStory<GenericArgs extends Args>(
story: TestingStory<GenericArgs>,
meta: ComponentAnnotations<ReactRenderer>,
globalConfig?: ProjectAnnotations<ReactRenderer>
): ComposedStory<GenericArgs>;
interface ComposedStory<TArgs = Args> {
/** Render the story component with optional prop overrides */
(extraArgs?: Partial<TArgs>): JSX.Element;
/** Story name from storyName property or function name */
storyName?: string;
/** Combined args from meta and story levels */
args: TArgs;
/** Play function for interaction testing */
play: (context: TestingStoryPlayContext<TArgs>) => Promise<void>;
/** Combined decorators from all levels */
decorators: DecoratorFunction<ReactRenderer, TArgs>[];
/** Combined parameters from all levels */
parameters: Parameters;
}Usage Examples:
import { render, screen } from "@testing-library/react";
import { composeStory } from "@storybook/testing-react";
import Meta, { Primary as PrimaryStory } from "./Button.stories";
// Compose individual story
const Primary = composeStory(PrimaryStory, Meta);
test("renders composed story", () => {
render(<Primary />);
expect(screen.getByRole("button")).toHaveTextContent("Primary");
});
test("overrides story args", () => {
render(<Primary label="Custom Label" />);
expect(screen.getByRole("button")).toHaveTextContent("Custom Label");
});
test("accesses story properties", () => {
expect(Primary.args.label).toBe("Primary");
expect(Primary.storyName).toBe("Primary");
});
// Execute play function for interactions
test("runs play function", async () => {
const { container } = render(<Primary />);
await Primary.play({ canvasElement: container });
// Verify interactions occurred
});Processes all stories from a stories file import, returning an object with all composed stories.
/**
* Processes all stories from a stories import, returning object with all composed stories
* @param storiesImport - Complete import from stories file (import * as stories)
* @param globalConfig - Optional global configuration, defaults to setProjectAnnotations config
* @returns Object mapping story names to composed story components
*/
function composeStories<TModule extends StoryFile>(
storiesImport: TModule,
globalConfig?: ProjectAnnotations<ReactRenderer>
): StoriesWithPartialProps<TModule>;
type StoriesWithPartialProps<T> = {
[K in keyof T]: T[K] extends StoryAnnotations<ReactRenderer, infer P>
? ComposedStory<Partial<P>>
: number;
};Usage Examples:
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/testing-react";
import * as stories from "./Button.stories";
// Compose all stories at once
const { Primary, Secondary, Large, Small } = composeStories(stories);
test("renders all story variants", () => {
render(<Primary />);
expect(screen.getByRole("button")).toHaveClass("primary");
render(<Secondary />);
expect(screen.getByRole("button")).toHaveClass("secondary");
});
// Batch testing pattern
const testCases = Object.values(composeStories(stories)).map(Story => [
Story.storyName!,
Story,
]);
test.each(testCases)("Renders %s story", async (_storyName, Story) => {
const { container } = render(<Story />);
expect(container.firstChild).toMatchSnapshot();
});Composed stories include all the properties from the original story configuration:
interface ComposedStoryProperties<TArgs = Args> {
/** Combined arguments from meta.args and story.args */
args: TArgs;
/** Story name from storyName property or export name */
storyName?: string;
/** Play function with bound context */
play: (context: TestingStoryPlayContext<TArgs>) => Promise<void>;
/** All decorators combined in application order */
decorators: DecoratorFunction<ReactRenderer, TArgs>[];
/** All parameters combined with proper precedence */
parameters: Parameters;
}
type TestingStoryPlayContext<TArgs = Args> = Partial<PlayFunctionContext<ReactRenderer, TArgs>> & {
/** DOM element containing the rendered story */
canvasElement: HTMLElement;
/** Additional context properties for play function */
args?: TArgs;
globals?: Record<string, any>;
parameters?: Parameters;
};Accessing Story Properties:
const { Primary } = composeStories(stories);
// Access story configuration
console.log(Primary.args); // { label: "Primary", size: "medium" }
console.log(Primary.storyName); // "Primary"
console.log(Primary.parameters); // Combined parameters object
// Use in tests
test("story has correct default args", () => {
expect(Primary.args.label).toBe("Primary");
expect(Primary.args.primary).toBe(true);
});Configures global Storybook settings to be applied to all composed stories.
/**
* Sets global Storybook configuration to be applied to all composed stories
* @param projectAnnotations - Configuration from .storybook/preview or array of configs
*/
function setProjectAnnotations(
projectAnnotations: ProjectAnnotations<ReactRenderer> | ProjectAnnotations<ReactRenderer>[]
): void;
interface ProjectAnnotations<TRenderer> {
/** Global decorators applied to all stories */
decorators?: DecoratorFunction<TRenderer, any>[];
/** Global parameters applied to all stories */
parameters?: Parameters;
/** Global arg types for controls and docs */
argTypes?: ArgTypes;
/** Global types for toolbar controls */
globalTypes?: GlobalTypes;
}Usage Examples:
// In test setup file (jest setupFilesAfterEnv)
import { setProjectAnnotations } from "@storybook/testing-react";
import * as globalStorybookConfig from "../.storybook/preview";
// Apply global configuration once
setProjectAnnotations(globalStorybookConfig);
// Or combine multiple configurations
setProjectAnnotations([
globalStorybookConfig,
{
decorators: [
(Story) => (
<div data-testid="test-wrapper">
<Story />
</div>
),
],
},
]);Per-Story Configuration Override:
// Override global config for specific story composition
const { Primary } = composeStories(stories, {
decorators: [TestDecorator],
parameters: {
backgrounds: { default: "light" }
},
});
// Or per individual story
const Primary = composeStory(PrimaryStory, Meta, {
decorators: [CustomTestDecorator],
parameters: { viewport: { defaultViewport: "mobile1" }},
});/**
* @deprecated Use setProjectAnnotations instead
* Legacy function for setting global configuration
*/
function setGlobalConfig(
projectAnnotations: ProjectAnnotations<ReactRenderer> | ProjectAnnotations<ReactRenderer>[]
): void;This function is deprecated and will be removed in future versions. Use setProjectAnnotations instead.
The library supports Component Story Format v3 with both function and object-style stories:
Function Stories:
export const Primary = (args) => <Button {...args} />;
Primary.args = { primary: true, label: "Button" };Object Stories:
export const Primary = {
args: { primary: true, label: "Button" },
render: (args) => <Button {...args} />,
};Object Stories with Play Function:
export const WithInteraction = {
args: { label: "Click me" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button"));
},
};The library throws descriptive errors for common issues:
.story propertypassArgsFirst: falseExample Error Handling:
try {
const Primary = composeStory(invalidStory, Meta);
} catch (error) {
console.error(error.message);
// "Cannot compose story due to invalid format..."
}Install with Tessl CLI
npx tessl i tessl/npm-storybook--testing-react