0
# Async Utilities
1
2
Asynchronous utilities for waiting for DOM changes and element state transitions, with instrumentation for Storybook interaction tracking. These utilities help test dynamic content and asynchronous operations.
3
4
## Capabilities
5
6
### Wait For Function
7
8
Waits for a condition to become true by repeatedly calling a callback function until it succeeds or times out. Instrumented for Storybook interactions.
9
10
```typescript { .api }
11
/**
12
* Wait for a condition to be true by repeatedly calling callback
13
* @param callback - Function to call repeatedly until it succeeds
14
* @param options - Timeout and retry configuration
15
* @returns Promise that resolves with callback result or rejects on timeout
16
*/
17
function waitFor<T>(
18
callback: () => T | Promise<T>,
19
options?: WaitForOptions
20
): Promise<T>;
21
22
/**
23
* Configuration options for waitFor function
24
*/
25
interface WaitForOptions {
26
/** Maximum time to wait in milliseconds (default: 1000) */
27
timeout?: number;
28
/** Time between retries in milliseconds (default: 50) */
29
interval?: number;
30
/** Custom error handler for timeout scenarios */
31
onTimeout?: (error: Error) => Error;
32
/** Show original stack trace in errors */
33
showOriginalStackTrace?: boolean;
34
/** Custom error message for timeout */
35
errorMessage?: string;
36
}
37
```
38
39
### Wait For Element To Be Removed
40
41
Waits for an element to be removed from the DOM. Instrumented for Storybook interactions.
42
43
```typescript { .api }
44
/**
45
* Wait for an element to be removed from the DOM
46
* @param callback - Function returning element to wait for removal, or the element itself
47
* @param options - Timeout and retry configuration
48
* @returns Promise that resolves when element is removed or rejects on timeout
49
*/
50
function waitForElementToBeRemoved<T>(
51
callback: (() => T) | T,
52
options?: WaitForOptions
53
): Promise<void>;
54
```
55
56
## Usage Examples
57
58
### Basic Wait For Usage
59
60
```typescript
61
import { within, waitFor, userEvent } from "@storybook/testing-library";
62
63
export const BasicWaitForExample = {
64
play: async ({ canvasElement }) => {
65
const canvas = within(canvasElement);
66
67
// Click button that triggers async operation
68
const loadButton = canvas.getByRole('button', { name: /load data/i });
69
await userEvent.click(loadButton);
70
71
// Wait for loading to complete and data to appear
72
await waitFor(() => {
73
expect(canvas.getByText(/data loaded successfully/i)).toBeInTheDocument();
74
});
75
76
// Verify data is displayed
77
const dataItems = canvas.getAllByTestId('data-item');
78
expect(dataItems).toHaveLength(3);
79
}
80
};
81
```
82
83
### Wait With Custom Timeout
84
85
```typescript
86
import { within, waitFor, userEvent } from "@storybook/testing-library";
87
88
export const CustomTimeoutExample = {
89
play: async ({ canvasElement }) => {
90
const canvas = within(canvasElement);
91
92
// Operation that takes longer than default timeout
93
const slowButton = canvas.getByRole('button', { name: /slow operation/i });
94
await userEvent.click(slowButton);
95
96
// Wait with extended timeout
97
await waitFor(
98
() => {
99
expect(canvas.getByText(/operation completed/i)).toBeInTheDocument();
100
},
101
{ timeout: 5000, interval: 200 } // Wait up to 5 seconds, check every 200ms
102
);
103
}
104
};
105
```
106
107
### Wait For Element Removal
108
109
```typescript
110
import { within, waitForElementToBeRemoved, userEvent } from "@storybook/testing-library";
111
112
export const ElementRemovalExample = {
113
play: async ({ canvasElement }) => {
114
const canvas = within(canvasElement);
115
116
// Find element that will be removed
117
const loadingSpinner = canvas.getByTestId('loading-spinner');
118
expect(loadingSpinner).toBeInTheDocument();
119
120
// Trigger action that removes the element
121
const startButton = canvas.getByRole('button', { name: /start/i });
122
await userEvent.click(startButton);
123
124
// Wait for loading spinner to be removed
125
await waitForElementToBeRemoved(loadingSpinner);
126
127
// Verify content appears after loading
128
expect(canvas.getByText(/content loaded/i)).toBeInTheDocument();
129
}
130
};
131
```
132
133
### Wait For Multiple Conditions
134
135
```typescript
136
import { within, waitFor, userEvent } from "@storybook/testing-library";
137
138
export const MultipleConditionsExample = {
139
play: async ({ canvasElement }) => {
140
const canvas = within(canvasElement);
141
142
const submitButton = canvas.getByRole('button', { name: /submit/i });
143
await userEvent.click(submitButton);
144
145
// Wait for multiple conditions to be true
146
await waitFor(() => {
147
// All these conditions must be true
148
expect(canvas.getByText(/form submitted/i)).toBeInTheDocument();
149
expect(canvas.getByTestId('success-icon')).toBeInTheDocument();
150
expect(canvas.queryByTestId('loading')).not.toBeInTheDocument();
151
});
152
}
153
};
154
```
155
156
### Async Form Validation
157
158
```typescript
159
import { within, waitFor, userEvent } from "@storybook/testing-library";
160
161
export const AsyncValidationExample = {
162
play: async ({ canvasElement }) => {
163
const canvas = within(canvasElement);
164
165
const emailInput = canvas.getByLabelText(/email/i);
166
167
// Type invalid email
168
await userEvent.type(emailInput, 'invalid-email');
169
await userEvent.tab(); // Trigger validation
170
171
// Wait for validation error to appear
172
await waitFor(() => {
173
expect(canvas.getByText(/invalid email format/i)).toBeInTheDocument();
174
});
175
176
// Clear and type valid email
177
await userEvent.clear(emailInput);
178
await userEvent.type(emailInput, 'user@example.com');
179
180
// Wait for error to disappear
181
await waitFor(() => {
182
expect(canvas.queryByText(/invalid email format/i)).not.toBeInTheDocument();
183
});
184
}
185
};
186
```
187
188
### API Response Waiting
189
190
```typescript
191
import { within, waitFor, userEvent } from "@storybook/testing-library";
192
193
export const ApiResponseExample = {
194
play: async ({ canvasElement }) => {
195
const canvas = within(canvasElement);
196
197
const searchInput = canvas.getByLabelText(/search/i);
198
const searchButton = canvas.getByRole('button', { name: /search/i });
199
200
// Perform search
201
await userEvent.type(searchInput, 'test query');
202
await userEvent.click(searchButton);
203
204
// Wait for loading state
205
await waitFor(() => {
206
expect(canvas.getByTestId('loading')).toBeInTheDocument();
207
});
208
209
// Wait for results to load
210
await waitFor(
211
() => {
212
expect(canvas.queryByTestId('loading')).not.toBeInTheDocument();
213
expect(canvas.getByText(/search results/i)).toBeInTheDocument();
214
},
215
{ timeout: 3000 }
216
);
217
218
// Verify results
219
const results = canvas.getAllByTestId('search-result');
220
expect(results.length).toBeGreaterThan(0);
221
}
222
};
223
```
224
225
### Animation Waiting
226
227
```typescript
228
import { within, waitFor, userEvent } from "@storybook/testing-library";
229
230
export const AnimationExample = {
231
play: async ({ canvasElement }) => {
232
const canvas = within(canvasElement);
233
234
const toggleButton = canvas.getByRole('button', { name: /toggle panel/i });
235
await userEvent.click(toggleButton);
236
237
// Wait for animation to start
238
await waitFor(() => {
239
const panel = canvas.getByTestId('animated-panel');
240
expect(panel).toHaveClass('animating');
241
});
242
243
// Wait for animation to complete
244
await waitFor(
245
() => {
246
const panel = canvas.getByTestId('animated-panel');
247
expect(panel).toHaveClass('visible');
248
expect(panel).not.toHaveClass('animating');
249
},
250
{ timeout: 2000 } // Animations can take time
251
);
252
}
253
};
254
```
255
256
### Error Handling
257
258
```typescript
259
import { within, waitFor, userEvent } from "@storybook/testing-library";
260
261
export const ErrorHandlingExample = {
262
play: async ({ canvasElement }) => {
263
const canvas = within(canvasElement);
264
265
const failButton = canvas.getByRole('button', { name: /trigger error/i });
266
await userEvent.click(failButton);
267
268
try {
269
// This will timeout and throw an error
270
await waitFor(
271
() => {
272
expect(canvas.getByText(/this will never appear/i)).toBeInTheDocument();
273
},
274
{
275
timeout: 1000,
276
onTimeout: (error) => new Error(`Custom error: ${error.message}`)
277
}
278
);
279
} catch (error) {
280
// Handle the timeout error
281
console.log('Expected timeout occurred:', error.message);
282
}
283
284
// Verify error state instead
285
await waitFor(() => {
286
expect(canvas.getByText(/error occurred/i)).toBeInTheDocument();
287
});
288
}
289
};
290
```
291
292
### Wait For Custom Condition
293
294
```typescript
295
import { within, waitFor, userEvent } from "@storybook/testing-library";
296
297
export const CustomConditionExample = {
298
play: async ({ canvasElement }) => {
299
const canvas = within(canvasElement);
300
301
const incrementButton = canvas.getByRole('button', { name: /increment/i });
302
303
// Click button multiple times
304
await userEvent.click(incrementButton);
305
await userEvent.click(incrementButton);
306
await userEvent.click(incrementButton);
307
308
// Wait for counter to reach specific value
309
await waitFor(() => {
310
const counter = canvas.getByTestId('counter');
311
const value = parseInt(counter.textContent || '0');
312
expect(value).toBeGreaterThanOrEqual(3);
313
});
314
315
// Wait for element to have specific style
316
await waitFor(() => {
317
const progressBar = canvas.getByTestId('progress-bar');
318
const width = getComputedStyle(progressBar).width;
319
expect(parseInt(width)).toBeGreaterThan(50);
320
});
321
}
322
};
323
```
324
325
### Polling Pattern
326
327
```typescript
328
import { within, waitFor, userEvent } from "@storybook/testing-library";
329
330
export const PollingExample = {
331
play: async ({ canvasElement }) => {
332
const canvas = within(canvasElement);
333
334
const refreshButton = canvas.getByRole('button', { name: /auto refresh/i });
335
await userEvent.click(refreshButton);
336
337
// Poll for status changes
338
let attempts = 0;
339
await waitFor(
340
() => {
341
attempts++;
342
const status = canvas.getByTestId('status');
343
console.log(`Attempt ${attempts}: Status is ${status.textContent}`);
344
expect(status).toHaveTextContent('Complete');
345
},
346
{
347
timeout: 10000,
348
interval: 500 // Check every 500ms
349
}
350
);
351
352
expect(attempts).toBeGreaterThan(1);
353
}
354
};
355
```
356
357
## Advanced Patterns
358
359
### Retry Logic
360
361
```typescript
362
import { within, waitFor } from "@storybook/testing-library";
363
364
export const RetryLogicExample = {
365
play: async ({ canvasElement }) => {
366
const canvas = within(canvasElement);
367
368
// Custom retry logic with exponential backoff
369
let retryCount = 0;
370
await waitFor(
371
() => {
372
retryCount++;
373
const element = canvas.queryByTestId('flaky-element');
374
375
if (!element && retryCount < 3) {
376
throw new Error(`Attempt ${retryCount} failed`);
377
}
378
379
expect(element).toBeInTheDocument();
380
},
381
{ timeout: 5000, interval: 1000 }
382
);
383
}
384
};
385
```
386
387
### Combination with findBy Queries
388
389
```typescript
390
import { within, waitFor, userEvent } from "@storybook/testing-library";
391
392
export const FindByWaitForExample = {
393
play: async ({ canvasElement }) => {
394
const canvas = within(canvasElement);
395
396
const loadButton = canvas.getByRole('button', { name: /load/i });
397
await userEvent.click(loadButton);
398
399
// findBy* queries include built-in waiting
400
const result = await canvas.findByText(/loaded content/i);
401
expect(result).toBeInTheDocument();
402
403
// Use waitFor for more complex conditions
404
await waitFor(() => {
405
const items = canvas.getAllByTestId('item');
406
expect(items).toHaveLength(5);
407
expect(items.every(item => item.textContent?.includes('data'))).toBe(true);
408
});
409
}
410
};
411
```