or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

composition.mdindex.md

composition.mddocs/

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

```