or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-utilities.mdconfiguration.mdevents.mdindex.mdqueries.mdscoping.mduser-interactions.md

async-utilities.mddocs/

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

```