0
# Portable Stories
1
2
Utilities for using Storybook stories outside of the Storybook environment, such as in testing frameworks and other applications.
3
4
## Capabilities
5
6
### setProjectAnnotations Function
7
8
Sets global project annotations (preview configuration) for using stories outside of Storybook. This should be run once to apply global decorators, parameters, and other configuration.
9
10
```typescript { .api }
11
/**
12
* Function that sets the globalConfig of your storybook. The global config is the preview module of
13
* your .storybook folder.
14
*
15
* It should be run a single time, so that your global config (e.g. decorators) is applied to your
16
* stories when using `composeStories` or `composeStory`.
17
*
18
* @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview')
19
* @returns Normalized project annotations
20
*/
21
declare function setProjectAnnotations(
22
projectAnnotations:
23
| NamedOrDefaultProjectAnnotations<any>
24
| NamedOrDefaultProjectAnnotations<any>[]
25
): NormalizedProjectAnnotations<AngularRenderer>;
26
```
27
28
## Usage Examples
29
30
### Testing with Jest
31
32
Set up portable stories for Jest testing:
33
34
```typescript
35
// setup-tests.ts
36
import { setProjectAnnotations } from '@storybook/angular';
37
import { TestBed } from '@angular/core/testing';
38
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
39
40
// Import your global storybook preview configuration
41
import globalStorybookConfig from '../.storybook/preview';
42
43
// Set up global storybook configuration
44
setProjectAnnotations(globalStorybookConfig);
45
46
// Configure Angular TestBed
47
beforeEach(() => {
48
TestBed.configureTestingModule({
49
imports: [BrowserAnimationsModule],
50
});
51
});
52
```
53
54
**Using stories in Jest tests:**
55
56
```typescript
57
// button.component.spec.ts
58
import { ComponentFixture, TestBed } from '@angular/core/testing';
59
import { composeStories } from '@storybook/testing-angular';
60
import * as stories from './button.stories';
61
62
// Compose all stories from the story file
63
const { Primary, Secondary, Large } = composeStories(stories);
64
65
describe('ButtonComponent', () => {
66
let fixture: ComponentFixture<any>;
67
68
it('should render primary button correctly', async () => {
69
// Use the Primary story
70
const component = TestBed.createComponent(Primary.component);
71
component.componentInstance = { ...Primary.args };
72
fixture = component;
73
fixture.detectChanges();
74
75
expect(fixture.nativeElement.textContent).toContain('Button');
76
expect(fixture.nativeElement.querySelector('.primary')).toBeTruthy();
77
});
78
79
it('should handle click events', async () => {
80
const component = TestBed.createComponent(Primary.component);
81
const clickSpy = jest.fn();
82
83
component.componentInstance = {
84
...Primary.args,
85
onClick: clickSpy
86
};
87
88
fixture = component;
89
fixture.detectChanges();
90
91
fixture.nativeElement.querySelector('button').click();
92
expect(clickSpy).toHaveBeenCalled();
93
});
94
});
95
```
96
97
### Testing with Playwright
98
99
Use stories for end-to-end testing:
100
101
```typescript
102
// setup-e2e.ts
103
import { setProjectAnnotations } from '@storybook/angular';
104
import globalStorybookConfig from '../.storybook/preview';
105
106
setProjectAnnotations(globalStorybookConfig);
107
```
108
109
```typescript
110
// button.e2e.spec.ts
111
import { test, expect } from '@playwright/test';
112
import { composeStories } from '@storybook/testing-angular';
113
import * as stories from './button.stories';
114
115
const { Primary, Secondary } = composeStories(stories);
116
117
test.describe('Button Component E2E', () => {
118
test('primary button should be clickable', async ({ page }) => {
119
// Navigate to a page that renders the Primary story
120
await page.goto('/storybook-iframe.html?id=button--primary');
121
122
const button = page.locator('button');
123
await expect(button).toBeVisible();
124
await expect(button).toHaveClass(/primary/);
125
126
await button.click();
127
// Assert expected behavior after click
128
});
129
});
130
```
131
132
### Snapshot Testing
133
134
Use stories for visual regression testing:
135
136
```typescript
137
// button.snapshot.spec.ts
138
import { ComponentFixture, TestBed } from '@angular/core/testing';
139
import { composeStories } from '@storybook/testing-angular';
140
import * as stories from './button.stories';
141
142
const { Primary, Secondary, Large, Small } = composeStories(stories);
143
144
describe('Button Snapshots', () => {
145
let fixture: ComponentFixture<any>;
146
147
const renderStory = (story: any) => {
148
const component = TestBed.createComponent(story.component);
149
component.componentInstance = { ...story.args };
150
fixture = component;
151
fixture.detectChanges();
152
return fixture.nativeElement;
153
};
154
155
it('should match primary button snapshot', () => {
156
const element = renderStory(Primary);
157
expect(element).toMatchSnapshot();
158
});
159
160
it('should match secondary button snapshot', () => {
161
const element = renderStory(Secondary);
162
expect(element).toMatchSnapshot();
163
});
164
165
it('should match large button snapshot', () => {
166
const element = renderStory(Large);
167
expect(element).toMatchSnapshot();
168
});
169
170
it('should match small button snapshot', () => {
171
const element = renderStory(Small);
172
expect(element).toMatchSnapshot();
173
});
174
});
175
```
176
177
### Custom Testing Utilities
178
179
Create reusable testing utilities with portable stories:
180
181
```typescript
182
// story-test-utils.ts
183
import { ComponentFixture, TestBed } from '@angular/core/testing';
184
import { setProjectAnnotations } from '@storybook/angular';
185
import { composeStories } from '@storybook/testing-angular';
186
import globalStorybookConfig from '../.storybook/preview';
187
188
// Set up global configuration once
189
setProjectAnnotations(globalStorybookConfig);
190
191
export interface StoryTestOptions {
192
story: any;
193
props?: Record<string, any>;
194
detectChanges?: boolean;
195
}
196
197
export class StoryTestHelper {
198
private fixture: ComponentFixture<any>;
199
200
static create(options: StoryTestOptions): StoryTestHelper {
201
const helper = new StoryTestHelper();
202
helper.render(options);
203
return helper;
204
}
205
206
private render(options: StoryTestOptions): void {
207
const component = TestBed.createComponent(options.story.component);
208
component.componentInstance = {
209
...options.story.args,
210
...options.props
211
};
212
213
this.fixture = component;
214
215
if (options.detectChanges !== false) {
216
this.fixture.detectChanges();
217
}
218
}
219
220
get element(): HTMLElement {
221
return this.fixture.nativeElement;
222
}
223
224
get component(): any {
225
return this.fixture.componentInstance;
226
}
227
228
get fixture(): ComponentFixture<any> {
229
return this.fixture;
230
}
231
232
query(selector: string): HTMLElement | null {
233
return this.element.querySelector(selector);
234
}
235
236
queryAll(selector: string): NodeList {
237
return this.element.querySelectorAll(selector);
238
}
239
240
updateProps(props: Record<string, any>): void {
241
Object.assign(this.component, props);
242
this.fixture.detectChanges();
243
}
244
245
triggerEvent(selector: string, eventType: string, eventData?: any): void {
246
const element = this.query(selector);
247
if (element) {
248
const event = new Event(eventType);
249
if (eventData) {
250
Object.assign(event, eventData);
251
}
252
element.dispatchEvent(event);
253
this.fixture.detectChanges();
254
}
255
}
256
}
257
258
// Usage example:
259
// const helper = StoryTestHelper.create({ story: Primary });
260
// expect(helper.query('button')).toBeTruthy();
261
// helper.updateProps({ disabled: true });
262
// expect(helper.query('button')).toHaveAttribute('disabled');
263
```
264
265
### Integration Testing
266
267
Test complete user workflows using multiple stories:
268
269
```typescript
270
// user-workflow.spec.ts
271
import { TestBed } from '@angular/core/testing';
272
import { setProjectAnnotations } from '@storybook/angular';
273
import { composeStories } from '@storybook/testing-angular';
274
import * as formStories from './form.stories';
275
import * as buttonStories from './button.stories';
276
import * as modalStories from './modal.stories';
277
import globalStorybookConfig from '../.storybook/preview';
278
279
setProjectAnnotations(globalStorybookConfig);
280
281
const { DefaultForm } = composeStories(formStories);
282
const { Primary: PrimaryButton } = composeStories(buttonStories);
283
const { ConfirmationModal } = composeStories(modalStories);
284
285
describe('User Registration Workflow', () => {
286
it('should complete user registration flow', async () => {
287
// Step 1: Render registration form
288
const formComponent = TestBed.createComponent(DefaultForm.component);
289
formComponent.componentInstance = { ...DefaultForm.args };
290
const formFixture = formComponent;
291
formFixture.detectChanges();
292
293
// Step 2: Fill out form
294
const emailInput = formFixture.nativeElement.querySelector('input[type="email"]');
295
const passwordInput = formFixture.nativeElement.querySelector('input[type="password"]');
296
297
emailInput.value = 'test@example.com';
298
emailInput.dispatchEvent(new Event('input'));
299
300
passwordInput.value = 'password123';
301
passwordInput.dispatchEvent(new Event('input'));
302
303
formFixture.detectChanges();
304
305
// Step 3: Click submit button
306
const submitButton = formFixture.nativeElement.querySelector('button[type="submit"]');
307
expect(submitButton).not.toHaveAttribute('disabled');
308
309
submitButton.click();
310
formFixture.detectChanges();
311
312
// Step 4: Verify confirmation modal appears
313
const modalComponent = TestBed.createComponent(ConfirmationModal.component);
314
modalComponent.componentInstance = { ...ConfirmationModal.args };
315
const modalFixture = modalComponent;
316
modalFixture.detectChanges();
317
318
expect(modalFixture.nativeElement).toContainText('Registration Successful');
319
});
320
});
321
```
322
323
## Setup Patterns
324
325
### Global Setup for Testing Framework
326
327
```typescript
328
// jest.setup.ts or vitest.setup.ts
329
import { setProjectAnnotations } from '@storybook/angular';
330
import { TestBed } from '@angular/core/testing';
331
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
332
import { CommonModule } from '@angular/common';
333
334
// Import your global storybook configuration
335
import globalStorybookConfig from '../.storybook/preview';
336
337
// Apply global storybook configuration
338
setProjectAnnotations(globalStorybookConfig);
339
340
// Global Angular TestBed configuration
341
beforeEach(async () => {
342
await TestBed.configureTestingModule({
343
imports: [
344
CommonModule,
345
BrowserAnimationsModule,
346
],
347
teardown: { destroyAfterEach: true },
348
}).compileComponents();
349
});
350
```
351
352
### Per-Test Setup
353
354
```typescript
355
// individual-test.spec.ts
356
import { setProjectAnnotations } from '@storybook/angular';
357
import { TestBed } from '@angular/core/testing';
358
359
// Test-specific configuration
360
beforeEach(() => {
361
setProjectAnnotations([
362
// Base global config
363
require('../.storybook/preview').default,
364
// Test-specific overrides
365
{
366
parameters: {
367
// Override parameters for this test suite
368
backgrounds: { default: 'light' },
369
},
370
decorators: [
371
// Additional test-specific decorators
372
],
373
},
374
]);
375
});
376
```
377
378
## Best Practices
379
380
### Configuration Management
381
382
- Set up `setProjectAnnotations` once in your test setup file
383
- Import the exact same preview configuration used by Storybook
384
- Use array format for multiple configuration sources when needed
385
386
### Performance Optimization
387
388
- Avoid calling `setProjectAnnotations` repeatedly in individual tests
389
- Configure TestBed once per test suite rather than per test
390
- Use `composeStories` to precompile all stories from a file
391
392
### Type Safety
393
394
- Import story types to maintain type safety in tests
395
- Use TypeScript strict mode for better error detection
396
- Leverage Angular's dependency injection in test scenarios
397
398
### Integration with CI/CD
399
400
- Use portable stories for visual regression testing
401
- Include story-based tests in your CI pipeline
402
- Generate test coverage reports that include story usage