Portable stories in Vitest
Portable stories are Storybook stories which can be used in external environments, such as Vitest.
Normally, Storybook composes a story and its annotations automatically, as part of the story pipeline. When using stories in Vitest tests, you must handle the story pipeline yourself, which is what the composeStories
and composeStory
functions enable.
The API specified here is available in Storybook 8.2.7
and up. If you're using an older version of Storybook, you can upgrade to the latest version (npx storybook@latest upgrade
) to use this API. If you're unable to upgrade, you can use previous API, which uses the .play()
method instead of .run()
, but is otherwise identical.
Using Next.js
? Next.js requires specific configuration that is only available in Jest. The portable stories API is not supported in Next.js with Vitest.
composeStories
composeStories
will process the component's stories you specify, compose each of them with the necessary annotations, and return an object containing the composed stories.
By default, the composed story will render the component with the args that are defined in the story. You can also pass any props to the component in your test and those props will override the values passed in the story's args.
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { composeStories } from '@storybook/react';
// Import all stories and the component annotations from the stories file
import * as stories from './Button.stories';
// Every component that is returned maps 1:1 with the stories,
// but they already contain all annotations from story, meta, and project levels
const { Primary, Secondary } = composeStories(stories);
test('renders primary button with default args', async () => {
await Primary.run();
const buttonElement = screen.getByText('Text coming from args in stories file!');
expect(buttonElement).not.toBeNull();
});
test('renders primary button with overridden props', async () => {
// You can override props by passing them in the context argument of the play function
await Primary.run({ args: { ...Primary.args, children: 'Hello world' } });
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).not.toBeNull();
});
Type
(
csfExports: CSF file exports,
projectAnnotations?: ProjectAnnotations
) => Record<string, ComposedStoryFn>
Parameters
csfExports
(Required)
Type: CSF file exports
Specifies which component's stories you want to compose. Pass the full set of exports from the CSF file (not the default export!). E.g. import * as stories from './Button.stories'
projectAnnotations
Type: ProjectAnnotation | ProjectAnnotation[]
Specifies the project annotations to be applied to the composed stories.
This parameter is provided for convenience. You should likely use setProjectAnnotations
instead. Details about the ProjectAnnotation
type can be found in that function's projectAnnotations
parameter.
This parameter can be used to override the project annotations applied via setProjectAnnotations
.
Return
Type: Record<string, ComposedStoryFn>
An object where the keys are the names of the stories and the values are the composed stories.
Additionally, the composed story will have the following properties:
Property | Type | Description |
---|---|---|
args | Record<string, any> | The story's args |
argTypes | ArgType | The story's argTypes |
id | string | The story's id |
parameters | Record<string, any> | The story's parameters |
play | (context) => Promise<void> | undefined | Executes the play function of a given story |
run | (context) => Promise<void> | undefined | Mounts and executes the play function of a given story |
storyName | string | The story's name |
tags | string[] | The story's tags |
composeStory
You can use composeStory
if you wish to compose a single story for a component.
import { vi, test, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { composeStory } from '@storybook/react';
import meta, { Primary as PrimaryStory } from './Button.stories';
// Returns a story which already contains all annotations from story, meta and global levels
const Primary = composeStory(PrimaryStory, meta);
test('renders primary button with default args', async () => {
await Primary.run();
const buttonElement = screen.getByText('Text coming from args in stories file!');
expect(buttonElement).not.toBeNull();
});
test('renders primary button with overridden props', async () => {
await Primary.run({ args: { ...Primary.args, label: 'Hello world' } });
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).not.toBeNull();
});
Type
(
story: Story export,
componentAnnotations: Meta,
projectAnnotations?: ProjectAnnotations,
exportsName?: string
) => ComposedStoryFn
Parameters
story
(Required)
Type: Story export
Specifies which story you want to compose.
componentAnnotations
(Required)
Type: Meta
The default export from the stories file containing the story
.
projectAnnotations
Type: ProjectAnnotation | ProjectAnnotation[]
Specifies the project annotations to be applied to the composed story.
This parameter is provided for convenience. You should likely use setProjectAnnotations
instead. Details about the ProjectAnnotation
type can be found in that function's projectAnnotations
parameter.
This parameter can be used to override the project annotations applied via setProjectAnnotations
.
exportsName
Type: string
You probably don't need this. Because composeStory
accepts a single story, it does not have access to the name of that story's export in the file (like composeStories
does). If you must ensure unique story names in your tests and you cannot use composeStories
, you can pass the name of the story's export here.
Return
Type: ComposedStoryFn
A single composed story.
setProjectAnnotations
This API should be called once, before the tests run, typically in a setup file. This will make sure that whenever composeStories
or composeStory
are called, the project annotations are taken into account as well.
These are the configurations needed in the setup file:
- preview annotations: those defined in
.storybook/preview.ts
- addon annotations (optional): those exported by addons
- beforeAll: code that runs before all tests (more info)
- testingLibraryRender: the render function from Testing Library
import { beforeAll } from 'vitest';
import { render as testingLibraryRender } from '@testing-library/react';
import { setProjectAnnotations } from '@storybook/react';
// 👇 Import the exported annotations, if any, from the addons you're using; otherwise remove this
import * as addonAnnotations from 'my-addon/preview';
import * as previewAnnotations from './.storybook/preview';
const annotations = setProjectAnnotations([
previewAnnotations,
addonAnnotations,
// You MUST provide this option to use portable stories with vitest
{ testingLibraryRender },
]);
// Run Storybook's beforeAll hook
beforeAll(annotations.beforeAll);
You must provide testingLibraryRenderer
to use portable stories with Vitest.
Sometimes a story can require an addon's decorator or loader to render properly. For example, an addon can apply a decorator that wraps your story in the necessary router context. In this case, you must include that addon's preview
export in the project annotations set. See addonAnnotations
in the example above.
Note: If the addon doesn't automatically apply the decorator or loader itself, but instead exports them for you to apply manually in .storybook/preview.js|ts
(e.g. using withThemeFromJSXProvider
from @storybook/addon-themes), then you do not need to do anything else. They are already included in the previewAnnotations
in the example above.
If you need to configure Testing Library's render
or use a different render function, please let us know in this discussion so we can learn more about your needs.
Type
(projectAnnotations: ProjectAnnotation | ProjectAnnotation[]) => ProjectAnnotation
Parameters
projectAnnotations
(Required)
Type: ProjectAnnotation | ProjectAnnotation[]
A set of project annotations (those defined in .storybook/preview.js|ts
) or an array of sets of project annotations, which will be applied to all composed stories.
Annotations
Annotations are the metadata applied to a story, like args, decorators, loaders, and play functions. They can be defined for a specific story, all stories for a component, or all stories in the project.
Story pipeline
To preview your stories in Storybook, Storybook runs a story pipeline, which includes applying project annotations, loading data, rendering the story, and playing interactions. This is a simplified version of the pipeline:
When you want to reuse a story in a different environment, however, it's crucial to understand that all these steps make a story. The portable stories API provides you with the mechanism to recreate that story pipeline in your external environment:
1. Apply project-level annotations
Annotations come from the story itself, that story's component, and the project. The project-level annotations are those defined in your .storybook/preview.js
file and by addons you're using. In portable stories, these annotations are not applied automatically — you must apply them yourself.
👉 For this, you use the setProjectAnnotations
API.
2. Compose
The story is prepared by running composeStories
or composeStory
. The outcome is a renderable component that represents the render function of the story.
3. Run
Finally, stories can prepare data they need (e.g. setting up some mocks or fetching data) before rendering by defining loaders, beforeEach or by having all the story code in the play function when using the mount. In portable stories, all of these steps will be executed when you call the run
method of the composed story.
👉 For this, you use the composeStories
or composeStory
API. The composed story will return a run
method to be called.
import { test } from 'vitest';
import { composeStories } from '@storybook/react';
import * as stories from './Button.stories';
const { Primary } = composeStories(stories);
test('renders and executes the play function', async () => {
// Mount story and run interactions
await Primary.run();
});
If your play function contains assertions (e.g. expect
calls), your test will fail when those assertions fail.
Overriding globals
If your stories behave differently based on globals (e.g. rendering text in English or Spanish), you can define those global values in portable stories by overriding project annotations when composing a story:
import { test } from 'vitest';
import { render } from '@testing-library/react';
import { composeStory } from '@storybook/react';
import meta, { Primary as PrimaryStory } from './Button.stories';
test('renders in English', async () => {
const Primary = composeStory(
PrimaryStory,
meta,
{ globals: { locale: 'en' } } // 👈 Project annotations to override the locale
);
await Primary.run();
});
test('renders in Spanish', async () => {
const Primary = composeStory(PrimaryStory, meta, { globals: { locale: 'es' } });
await Primary.run();
});