CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--testing-react

Testing utilities that allow you to reuse your stories in your unit tests

Pending
Overview
Eval results
Files

composition.mddocs/

Story Composition

Core functionality for converting Storybook stories into testable React components with all decorators, parameters, and configurations applied.

Capabilities

Compose Single Story

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

Compose All Stories

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 Story Properties

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

Global Configuration

Set Project Annotations

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: setGlobalConfig

/**
 * @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.

CSF3 Compatibility

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

Error Handling

The library throws descriptive errors for common issues:

  • Invalid story format: When story is not a function or valid object
  • Missing component: When CSF3 object story lacks render method and meta lacks component
  • Legacy story format: When story uses deprecated .story property
  • Legacy passArgsFirst: When story uses deprecated passArgsFirst: false

Example 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

docs

composition.md

index.md

tile.json