0
# Portable Stories
1
2
Testing utilities for using Storybook stories outside of the Storybook environment, particularly useful for unit testing and integration testing. Portable stories allow you to reuse your story definitions in testing frameworks like Jest, Vitest, or Playwright.
3
4
## Capabilities
5
6
### Project Configuration
7
8
Function for setting global project annotations to enable portable stories functionality.
9
10
```typescript { .api }
11
/**
12
* Sets the global config for portable stories functionality.
13
* Should be run once in test setup to apply global configuration like decorators.
14
*
15
* @param projectAnnotations - Project annotations (e.g., from .storybook/preview)
16
* @returns Normalized project annotations for web components renderer
17
*/
18
function setProjectAnnotations(
19
projectAnnotations:
20
| NamedOrDefaultProjectAnnotations<any>
21
| NamedOrDefaultProjectAnnotations<any>[]
22
): NormalizedProjectAnnotations<WebComponentsRenderer>;
23
```
24
25
## Usage Patterns
26
27
### Test Setup
28
29
Configure portable stories in your test setup file:
30
31
```typescript
32
// setup-file.js or setup-tests.js
33
import { setProjectAnnotations } from "@storybook/web-components";
34
import * as projectAnnotations from "../.storybook/preview";
35
36
// Apply global project configuration
37
setProjectAnnotations(projectAnnotations);
38
```
39
40
### Testing Individual Stories
41
42
Use stories directly in your tests by importing them and rendering manually:
43
44
```typescript
45
// button.test.js
46
import { render } from "@testing-library/dom";
47
import { setProjectAnnotations } from "@storybook/web-components";
48
import * as projectAnnotations from "../.storybook/preview";
49
import Meta, { Primary, Secondary } from "./Button.stories";
50
51
// Set up project annotations once
52
setProjectAnnotations(projectAnnotations);
53
54
describe("Button Component", () => {
55
it("renders primary button correctly", () => {
56
// Manually render the story
57
const element = Primary.render ? Primary.render(Primary.args, {}) : document.createElement("my-button");
58
if (Primary.args) {
59
Object.entries(Primary.args).forEach(([key, value]) => {
60
element.setAttribute(key, String(value));
61
});
62
}
63
64
render(element, { container: document.body });
65
66
const button = document.querySelector("my-button");
67
expect(button).toBeInTheDocument();
68
expect(button.getAttribute("variant")).toBe("primary");
69
});
70
71
it("handles custom args", () => {
72
const customArgs = {
73
...Primary.args,
74
label: "Custom Label",
75
disabled: true,
76
};
77
78
const element = document.createElement("my-button");
79
Object.entries(customArgs).forEach(([key, value]) => {
80
element.setAttribute(key, String(value));
81
});
82
83
render(element, { container: document.body });
84
const button = document.querySelector("my-button");
85
expect(button.getAttribute("label")).toBe("Custom Label");
86
expect(button.hasAttribute("disabled")).toBe(true);
87
});
88
89
it("applies decorators and global config", () => {
90
// Global decorators from setProjectAnnotations are available
91
// You'll need to manually apply them to your story rendering
92
const element = document.createElement("my-button");
93
if (Secondary.args) {
94
Object.entries(Secondary.args).forEach(([key, value]) => {
95
element.setAttribute(key, String(value));
96
});
97
}
98
99
render(element, { container: document.body });
100
101
expect(document.querySelector("my-button")).toBeInTheDocument();
102
});
103
});
104
```
105
106
### Testing Multiple Stories
107
108
Test multiple stories from a story file:
109
110
```typescript
111
// button.test.js
112
import * as stories from "./Button.stories";
113
114
const storyExports = Object.entries(stories)
115
.filter(([key]) => key !== 'default')
116
.map(([name, story]) => ({ name, story }));
117
118
describe("All Button Stories", () => {
119
storyExports.forEach(({ name, story }) => {
120
it(`renders ${name} story correctly`, () => {
121
const element = document.createElement("my-button");
122
123
if (story.args) {
124
Object.entries(story.args).forEach(([key, value]) => {
125
element.setAttribute(key, String(value));
126
});
127
}
128
129
render(element, { container: document.body });
130
131
expect(document.querySelector("my-button")).toBeInTheDocument();
132
});
133
});
134
});
135
```
136
137
### Integration with Testing Frameworks
138
139
#### Jest/Vitest Setup
140
141
```javascript
142
// vitest.config.js
143
import { defineConfig } from "vitest/config";
144
145
export default defineConfig({
146
test: {
147
setupFiles: ["./src/test-setup.js"],
148
environment: "jsdom",
149
},
150
});
151
152
// src/test-setup.js
153
import { setProjectAnnotations } from "@storybook/web-components";
154
import * as projectAnnotations from "../.storybook/preview";
155
156
setProjectAnnotations(projectAnnotations);
157
```
158
159
#### Playwright Integration
160
161
```typescript
162
// tests/stories.spec.ts
163
import { test, expect } from "@playwright/test";
164
import Meta, { Primary } from "../src/components/Button.stories";
165
166
test("visual regression test for primary button", async ({ page }) => {
167
// Render story in the browser
168
await page.setContent(`
169
<html>
170
<head>
171
<script type="module" src="/src/components/my-button.js"></script>
172
</head>
173
<body>
174
<my-button id="test-button"></my-button>
175
<script type="module">
176
const button = document.getElementById('test-button');
177
const args = ${JSON.stringify(Primary.args)};
178
Object.entries(args).forEach(([key, value]) => {
179
button.setAttribute(key, String(value));
180
});
181
</script>
182
</body>
183
</html>
184
`);
185
186
await page.locator("my-button").waitFor();
187
await expect(page).toHaveScreenshot("primary-button.png");
188
});
189
```
190
191
### Advanced Configuration
192
193
#### Custom Project Annotations
194
195
```typescript
196
// test-annotations.js
197
import type { Preview } from "@storybook/web-components";
198
199
const testAnnotations: Preview = {
200
decorators: [
201
// Test-specific decorators
202
(story) => {
203
const wrapper = document.createElement("div");
204
wrapper.setAttribute("data-test-wrapper", "true");
205
wrapper.appendChild(story());
206
return wrapper;
207
},
208
],
209
parameters: {
210
// Test-specific parameters
211
docs: { disable: true },
212
},
213
};
214
215
export default testAnnotations;
216
217
// test-setup.js
218
import { setProjectAnnotations } from "@storybook/web-components";
219
import previewAnnotations from "../.storybook/preview";
220
import testAnnotations from "./test-annotations";
221
222
// Combine preview and test annotations
223
setProjectAnnotations([previewAnnotations, testAnnotations]);
224
```
225
226
#### Conditional Configuration
227
228
```typescript
229
// setup-file.js
230
import { setProjectAnnotations } from "@storybook/web-components";
231
232
// Different configs for different test environments
233
if (process.env.NODE_ENV === "test") {
234
import("./test-preview").then((testConfig) => {
235
setProjectAnnotations(testConfig.default);
236
});
237
} else {
238
import("../.storybook/preview").then((previewConfig) => {
239
setProjectAnnotations(previewConfig.default);
240
});
241
}
242
```
243
244
## Benefits of Portable Stories
245
246
### Consistency
247
- **Same Components**: Test the exact same component definitions used in Storybook
248
- **Same Decorators**: Global decorators and configurations are automatically applied
249
- **Same Args**: Use the same argument validation and processing
250
251
### Efficiency
252
- **Reuse Stories**: No need to duplicate component setup in tests
253
- **Fast Feedback**: Quick unit tests without running full Storybook
254
- **CI Integration**: Run story-based tests in continuous integration
255
256
### Coverage
257
- **All Scenarios**: Easily test all story variants
258
- **Edge Cases**: Include edge case stories in automated testing
259
- **Visual Testing**: Use stories for visual regression testing
260
261
## Troubleshooting
262
263
### Common Issues
264
265
**Stories not rendering correctly:**
266
```typescript
267
// Make sure setProjectAnnotations is called before using stories
268
import { setProjectAnnotations } from "@storybook/web-components";
269
import projectAnnotations from "../.storybook/preview";
270
271
// Call this ONCE in your test setup
272
setProjectAnnotations(projectAnnotations);
273
```
274
275
**Missing decorators:**
276
```typescript
277
// Ensure your preview.js exports are properly structured
278
// .storybook/preview.js
279
export const decorators = [myDecorator];
280
export const parameters = { /* ... */ };
281
282
// Or use default export
283
export default {
284
decorators: [myDecorator],
285
parameters: { /* ... */ },
286
};
287
```
288
289
**Web Components not defined:**
290
```typescript
291
// Make sure to register your custom elements in tests
292
import "./my-button"; // Import component definition
293
294
// Or register manually
295
customElements.define("my-button", MyButtonElement);
296
```