0
# Story Composition
1
2
Core functionality for converting Storybook stories into testable React components with all decorators, parameters, and configurations applied.
3
4
## Capabilities
5
6
### Compose Single Story
7
8
Converts an individual story into a testable React component.
9
10
```typescript { .api }
11
/**
12
* Composes a single story with meta and global config, returning a component with all decorators applied
13
* @param story - Story object or function from stories file
14
* @param meta - Meta object (default export) from stories file
15
* @param globalConfig - Optional global configuration, defaults to setProjectAnnotations config
16
* @returns Composed story component with story properties
17
*/
18
function composeStory<GenericArgs extends Args>(
19
story: TestingStory<GenericArgs>,
20
meta: ComponentAnnotations<ReactRenderer>,
21
globalConfig?: ProjectAnnotations<ReactRenderer>
22
): ComposedStory<GenericArgs>;
23
24
interface ComposedStory<TArgs = Args> {
25
/** Render the story component with optional prop overrides */
26
(extraArgs?: Partial<TArgs>): JSX.Element;
27
/** Story name from storyName property or function name */
28
storyName?: string;
29
/** Combined args from meta and story levels */
30
args: TArgs;
31
/** Play function for interaction testing */
32
play: (context: TestingStoryPlayContext<TArgs>) => Promise<void>;
33
/** Combined decorators from all levels */
34
decorators: DecoratorFunction<ReactRenderer, TArgs>[];
35
/** Combined parameters from all levels */
36
parameters: Parameters;
37
}
38
```
39
40
**Usage Examples:**
41
42
```typescript
43
import { render, screen } from "@testing-library/react";
44
import { composeStory } from "@storybook/testing-react";
45
import Meta, { Primary as PrimaryStory } from "./Button.stories";
46
47
// Compose individual story
48
const Primary = composeStory(PrimaryStory, Meta);
49
50
test("renders composed story", () => {
51
render(<Primary />);
52
expect(screen.getByRole("button")).toHaveTextContent("Primary");
53
});
54
55
test("overrides story args", () => {
56
render(<Primary label="Custom Label" />);
57
expect(screen.getByRole("button")).toHaveTextContent("Custom Label");
58
});
59
60
test("accesses story properties", () => {
61
expect(Primary.args.label).toBe("Primary");
62
expect(Primary.storyName).toBe("Primary");
63
});
64
65
// Execute play function for interactions
66
test("runs play function", async () => {
67
const { container } = render(<Primary />);
68
await Primary.play({ canvasElement: container });
69
// Verify interactions occurred
70
});
71
```
72
73
### Compose All Stories
74
75
Processes all stories from a stories file import, returning an object with all composed stories.
76
77
```typescript { .api }
78
/**
79
* Processes all stories from a stories import, returning object with all composed stories
80
* @param storiesImport - Complete import from stories file (import * as stories)
81
* @param globalConfig - Optional global configuration, defaults to setProjectAnnotations config
82
* @returns Object mapping story names to composed story components
83
*/
84
function composeStories<TModule extends StoryFile>(
85
storiesImport: TModule,
86
globalConfig?: ProjectAnnotations<ReactRenderer>
87
): StoriesWithPartialProps<TModule>;
88
89
type StoriesWithPartialProps<T> = {
90
[K in keyof T]: T[K] extends StoryAnnotations<ReactRenderer, infer P>
91
? ComposedStory<Partial<P>>
92
: number;
93
};
94
```
95
96
**Usage Examples:**
97
98
```typescript
99
import { render, screen } from "@testing-library/react";
100
import { composeStories } from "@storybook/testing-react";
101
import * as stories from "./Button.stories";
102
103
// Compose all stories at once
104
const { Primary, Secondary, Large, Small } = composeStories(stories);
105
106
test("renders all story variants", () => {
107
render(<Primary />);
108
expect(screen.getByRole("button")).toHaveClass("primary");
109
110
render(<Secondary />);
111
expect(screen.getByRole("button")).toHaveClass("secondary");
112
});
113
114
// Batch testing pattern
115
const testCases = Object.values(composeStories(stories)).map(Story => [
116
Story.storyName!,
117
Story,
118
]);
119
120
test.each(testCases)("Renders %s story", async (_storyName, Story) => {
121
const { container } = render(<Story />);
122
expect(container.firstChild).toMatchSnapshot();
123
});
124
```
125
126
### Composed Story Properties
127
128
Composed stories include all the properties from the original story configuration:
129
130
```typescript { .api }
131
interface ComposedStoryProperties<TArgs = Args> {
132
/** Combined arguments from meta.args and story.args */
133
args: TArgs;
134
/** Story name from storyName property or export name */
135
storyName?: string;
136
/** Play function with bound context */
137
play: (context: TestingStoryPlayContext<TArgs>) => Promise<void>;
138
/** All decorators combined in application order */
139
decorators: DecoratorFunction<ReactRenderer, TArgs>[];
140
/** All parameters combined with proper precedence */
141
parameters: Parameters;
142
}
143
144
type TestingStoryPlayContext<TArgs = Args> = Partial<PlayFunctionContext<ReactRenderer, TArgs>> & {
145
/** DOM element containing the rendered story */
146
canvasElement: HTMLElement;
147
/** Additional context properties for play function */
148
args?: TArgs;
149
globals?: Record<string, any>;
150
parameters?: Parameters;
151
};
152
```
153
154
**Accessing Story Properties:**
155
156
```typescript
157
const { Primary } = composeStories(stories);
158
159
// Access story configuration
160
console.log(Primary.args); // { label: "Primary", size: "medium" }
161
console.log(Primary.storyName); // "Primary"
162
console.log(Primary.parameters); // Combined parameters object
163
164
// Use in tests
165
test("story has correct default args", () => {
166
expect(Primary.args.label).toBe("Primary");
167
expect(Primary.args.primary).toBe(true);
168
});
169
```
170
171
## Global Configuration
172
173
### Set Project Annotations
174
175
Configures global Storybook settings to be applied to all composed stories.
176
177
```typescript { .api }
178
/**
179
* Sets global Storybook configuration to be applied to all composed stories
180
* @param projectAnnotations - Configuration from .storybook/preview or array of configs
181
*/
182
function setProjectAnnotations(
183
projectAnnotations: ProjectAnnotations<ReactRenderer> | ProjectAnnotations<ReactRenderer>[]
184
): void;
185
186
interface ProjectAnnotations<TRenderer> {
187
/** Global decorators applied to all stories */
188
decorators?: DecoratorFunction<TRenderer, any>[];
189
/** Global parameters applied to all stories */
190
parameters?: Parameters;
191
/** Global arg types for controls and docs */
192
argTypes?: ArgTypes;
193
/** Global types for toolbar controls */
194
globalTypes?: GlobalTypes;
195
}
196
```
197
198
**Usage Examples:**
199
200
```typescript
201
// In test setup file (jest setupFilesAfterEnv)
202
import { setProjectAnnotations } from "@storybook/testing-react";
203
import * as globalStorybookConfig from "../.storybook/preview";
204
205
// Apply global configuration once
206
setProjectAnnotations(globalStorybookConfig);
207
208
// Or combine multiple configurations
209
setProjectAnnotations([
210
globalStorybookConfig,
211
{
212
decorators: [
213
(Story) => (
214
<div data-testid="test-wrapper">
215
<Story />
216
</div>
217
),
218
],
219
},
220
]);
221
```
222
223
**Per-Story Configuration Override:**
224
225
```typescript
226
// Override global config for specific story composition
227
const { Primary } = composeStories(stories, {
228
decorators: [TestDecorator],
229
parameters: {
230
backgrounds: { default: "light" }
231
},
232
});
233
234
// Or per individual story
235
const Primary = composeStory(PrimaryStory, Meta, {
236
decorators: [CustomTestDecorator],
237
parameters: { viewport: { defaultViewport: "mobile1" }},
238
});
239
```
240
241
### Deprecated: setGlobalConfig
242
243
```typescript { .api }
244
/**
245
* @deprecated Use setProjectAnnotations instead
246
* Legacy function for setting global configuration
247
*/
248
function setGlobalConfig(
249
projectAnnotations: ProjectAnnotations<ReactRenderer> | ProjectAnnotations<ReactRenderer>[]
250
): void;
251
```
252
253
This function is deprecated and will be removed in future versions. Use `setProjectAnnotations` instead.
254
255
## CSF3 Compatibility
256
257
The library supports Component Story Format v3 with both function and object-style stories:
258
259
**Function Stories:**
260
```typescript
261
export const Primary = (args) => <Button {...args} />;
262
Primary.args = { primary: true, label: "Button" };
263
```
264
265
**Object Stories:**
266
```typescript
267
export const Primary = {
268
args: { primary: true, label: "Button" },
269
render: (args) => <Button {...args} />,
270
};
271
```
272
273
**Object Stories with Play Function:**
274
```typescript
275
export const WithInteraction = {
276
args: { label: "Click me" },
277
play: async ({ canvasElement }) => {
278
const canvas = within(canvasElement);
279
await userEvent.click(canvas.getByRole("button"));
280
},
281
};
282
```
283
284
## Error Handling
285
286
The library throws descriptive errors for common issues:
287
288
- **Invalid story format**: When story is not a function or valid object
289
- **Missing component**: When CSF3 object story lacks render method and meta lacks component
290
- **Legacy story format**: When story uses deprecated `.story` property
291
- **Legacy passArgsFirst**: When story uses deprecated `passArgsFirst: false`
292
293
**Example Error Handling:**
294
295
```typescript
296
try {
297
const Primary = composeStory(invalidStory, Meta);
298
} catch (error) {
299
console.error(error.message);
300
// "Cannot compose story due to invalid format..."
301
}
302
```