Reuse Storybook stories in unit tests with portable stories API
Portable stories let you reuse your Storybook stories in external testing environments like Jest. This eliminates duplication between your stories and tests while ensuring your components work exactly as shown in Storybook.
Portable stories solve a common problem: maintaining component examples in two places—once in Storybook and again in tests. With portable stories, you write stories once and reuse them in tests.Benefits:
Single source of truth: Stories define component states, tests verify behavior
Better coverage: Test the exact scenarios shown in Storybook
Less maintenance: Update stories in one place
Consistent setup: Args, decorators, and parameters apply automatically
import { test, expect } from '@jest/globals';import { render, screen } from '@testing-library/react-native';import { composeStories } from '@storybook/react';import * as stories from './Button.stories';// Every export maps 1:1 with stories from the file// but includes all annotations from story, meta, and project levelsconst { Primary, Secondary } = composeStories(stories);test('renders primary button with default args', () => { render(<Primary />); const buttonElement = screen.getByText('Text coming from args in stories file!'); expect(buttonElement).not.toBeNull();});test('renders primary button with overridden props', () => { // Override props - they merge with story args render(<Primary>Hello world</Primary>); const buttonElement = screen.getByText(/Hello world/i); expect(buttonElement).not.toBeNull();});
import { jest, test, expect } from '@jest/globals';import { render, screen, userEvent } from '@testing-library/react-native';import { composeStory } from '@storybook/react';import meta, { Primary } from './Button.stories';test('onclick handler is called', async () => { // Returns a story with all annotations from story, meta, and global levels const PrimaryStory = composeStory(Primary, meta); const onPressSpy = jest.fn(); render(<PrimaryStory onPress={onPressSpy} />); const user = userEvent.setup({}); const actionButton = screen.getByText('Press me!'); await user.press(actionButton); expect(onPressSpy).toHaveBeenCalled();});
Call this once before tests run (typically in a Jest setup file) to apply global decorators, parameters, and other project configuration to all composed stories.
// setup-portable-stories.tsimport { setProjectAnnotations } from '@storybook/react';import * as addonAnnotations from 'my-addon/preview';import * as previewAnnotations from './.rnstorybook/preview';setProjectAnnotations([previewAnnotations, addonAnnotations]);
Some addons require decorators or loaders to work correctly. For example, a router addon might wrap stories in a navigation context.If the addon automatically applies decorators/loaders, include the addon’s preview annotations:
import * as addonAnnotations from 'my-addon/preview';setProjectAnnotations([previewAnnotations, addonAnnotations]);
If you manually apply them in preview.tsx (like withThemeFromJSXProvider from @storybook/addon-themes), they’re already included:
Project level: .rnstorybook/preview.tsx (applies to all stories)
Component level: Meta/default export (applies to all stories for that component)
Story level: Individual story objects (applies to that story only)
Portable stories compose all three levels automatically:
// Global decorators from preview.tsx// + Component decorators from meta// + Story decorators from individual story// = Fully composed storyconst { Primary } = composeStories(stories);
Ensure you’ve called setProjectAnnotations with your preview configuration:
// setup-portable-stories.tsimport { setProjectAnnotations } from '@storybook/react';import * as previewAnnotations from './.rnstorybook/preview';setProjectAnnotations(previewAnnotations);