or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-testing.mdasync.mdcomponent-rendering.mdconfiguration.mdelement-queries.mdevent-simulation.mdevents.mdhook-testing.mdhooks.mdindex.mdqueries.mdquick-reference.mdrendering.md

async.mddocs/

0

# Async Utilities

1

2

Wait for asynchronous changes with automatic act() wrapping.

3

4

## API

5

6

### waitFor Function

7

8

Waits for a condition to be met, repeatedly calling the callback until it succeeds or times out. Useful for waiting for asynchronous state updates, API calls, or side effects.

9

10

```typescript { .api }

11

/**

12

* Wait for a condition to be met by repeatedly calling the callback

13

* @param callback - Function to call repeatedly until it doesn't throw

14

* @param options - Configuration options

15

* @returns Promise resolving to callback's return value

16

* @throws When timeout is reached or onTimeout callback returns error

17

*/

18

function waitFor<T>(

19

callback: () => T | Promise<T>,

20

options?: {

21

/**

22

* Maximum time to wait in milliseconds (default: 1000)

23

*/

24

timeout?: number;

25

26

/**

27

* Time between callback calls in milliseconds (default: 50)

28

*/

29

interval?: number;

30

31

/**

32

* Custom error handler when timeout is reached

33

*/

34

onTimeout?: (error: Error) => Error;

35

36

/**

37

* Suppress timeout errors for debugging (default: false)

38

*/

39

mutationObserverOptions?: MutationObserverInit;

40

}

41

): Promise<T>;

42

```

43

44

### waitForElementToBeRemoved Function

45

46

Waits for an element to be removed from the DOM. Useful for testing loading states, modals closing, or elements being unmounted.

47

48

```typescript { .api }

49

/**

50

* Wait for element(s) to be removed from the DOM

51

* @param callback - Element, elements, or function returning element(s) to wait for removal

52

* @param options - Configuration options

53

* @returns Promise that resolves when element(s) are removed

54

* @throws When timeout is reached

55

*/

56

function waitForElementToBeRemoved<T>(

57

callback: T | (() => T),

58

options?: {

59

/**

60

* Maximum time to wait in milliseconds (default: 1000)

61

*/

62

timeout?: number;

63

64

/**

65

* Time between checks in milliseconds (default: 50)

66

*/

67

interval?: number;

68

69

/**

70

* Suppress timeout errors for debugging (default: false)

71

*/

72

mutationObserverOptions?: MutationObserverInit;

73

}

74

): Promise<void>;

75

```

76

77

### findBy* Queries

78

79

Async query variants that wait for elements to appear. These are convenience wrappers around `waitFor` + `getBy*` queries.

80

81

```typescript { .api }

82

/**

83

* Async queries that wait for elements to appear

84

* All getBy* queries have findBy* async equivalents

85

*/

86

findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;

87

findAllByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement[]>;

88

89

findByLabelText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement>;

90

findAllByLabelText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement[]>;

91

92

findByPlaceholderText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;

93

findAllByPlaceholderText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

94

95

findByText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement>;

96

findAllByText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement[]>;

97

98

findByDisplayValue(value: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;

99

findAllByDisplayValue(value: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

100

101

findByAltText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;

102

findAllByAltText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

103

104

findByTitle(title: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;

105

findAllByTitle(title: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

106

107

findByTestId(testId: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;

108

findAllByTestId(testId: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

109

```

110

111

## Common Patterns

112

113

### Wait for Element to Appear

114

```typescript

115

// Using findBy* (recommended for single element)

116

const message = await screen.findByText(/loaded/i);

117

118

// Using waitFor (for complex assertions)

119

await waitFor(() => {

120

expect(screen.getByText(/loaded/i)).toBeInTheDocument();

121

});

122

```

123

124

### Wait for Element to Disappear

125

```typescript

126

// Wait for removal

127

await waitForElementToBeRemoved(() => screen.getByText(/loading/i));

128

129

// Or use waitFor with queryBy

130

await waitFor(() => {

131

expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();

132

});

133

```

134

135

### Wait for Multiple Conditions

136

```typescript

137

await waitFor(() => {

138

expect(screen.getByText('Title')).toBeInTheDocument();

139

expect(screen.getByText('Content')).toBeInTheDocument();

140

expect(screen.queryByText('Loading')).not.toBeInTheDocument();

141

});

142

```

143

144

### Custom Timeout

145

```typescript

146

await waitFor(

147

() => expect(screen.getByText('Slow load')).toBeInTheDocument(),

148

{ timeout: 5000 } // 5 seconds

149

);

150

```

151

152

## Testing Patterns

153

154

### API Data Loading

155

```typescript

156

test('loads user data', async () => {

157

render(<UserProfile userId="123" />);

158

159

// Initially loading

160

expect(screen.getByText(/loading/i)).toBeInTheDocument();

161

162

// Wait for data

163

const name = await screen.findByText(/john doe/i);

164

expect(name).toBeInTheDocument();

165

166

// Loading gone

167

expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();

168

});

169

```

170

171

### Async State Updates

172

```typescript

173

test('updates after async operation', async () => {

174

render(<AsyncCounter />);

175

176

fireEvent.click(screen.getByRole('button', { name: /increment/i }));

177

178

// Wait for state update

179

await waitFor(() => {

180

expect(screen.getByText('Count: 1')).toBeInTheDocument();

181

});

182

});

183

```

184

185

### Modal Closing

186

```typescript

187

test('closes modal', async () => {

188

render(<App />);

189

190

const closeButton = screen.getByRole('button', { name: /close/i });

191

const modal = screen.getByRole('dialog');

192

193

fireEvent.click(closeButton);

194

195

await waitForElementToBeRemoved(modal);

196

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

197

});

198

```

199

200

### Form Validation

201

```typescript

202

test('shows validation after submit', async () => {

203

render(<Form />);

204

205

fireEvent.click(screen.getByRole('button', { name: /submit/i }));

206

207

const error = await screen.findByRole('alert');

208

expect(error).toHaveTextContent('Email is required');

209

});

210

```

211

212

### Polling/Retries

213

```typescript

214

test('retries failed request', async () => {

215

render(<DataFetcher />);

216

217

// Shows initial error

218

expect(await screen.findByText(/error/i)).toBeInTheDocument();

219

220

// Retries and succeeds

221

const data = await screen.findByText(/success/i, {}, { timeout: 5000 });

222

expect(data).toBeInTheDocument();

223

});

224

```

225

226

## Advanced Patterns

227

228

### Wait for Multiple Elements

229

```typescript

230

test('loads all items', async () => {

231

render(<ItemList />);

232

233

const items = await screen.findAllByRole('listitem');

234

expect(items).toHaveLength(5);

235

});

236

```

237

238

### Async Callback Return Value

239

```typescript

240

test('returns value from waitFor', async () => {

241

render(<Component />);

242

243

const element = await waitFor(() => screen.getByText('Value'));

244

expect(element).toHaveAttribute('data-loaded', 'true');

245

});

246

```

247

248

### Custom Error Messages

249

```typescript

250

test('provides context on timeout', async () => {

251

render(<Component />);

252

253

await waitFor(

254

() => expect(screen.getByText('Expected')).toBeInTheDocument(),

255

{

256

timeout: 2000,

257

onTimeout: (error) => {

258

screen.debug();

259

return new Error(`Element not found after 2s: ${error.message}`);

260

},

261

}

262

);

263

});

264

```

265

266

### Waiting for Hook Updates

267

```typescript

268

test('hook updates async state', async () => {

269

const { result } = renderHook(() => useAsyncData());

270

271

expect(result.current.loading).toBe(true);

272

273

await waitFor(() => {

274

expect(result.current.loading).toBe(false);

275

});

276

277

expect(result.current.data).toBeDefined();

278

});

279

```

280

281

## findBy* vs waitFor

282

283

### Use findBy* when:

284

- Waiting for single element to appear

285

- Simple query without complex assertions

286

287

```typescript

288

const button = await screen.findByRole('button');

289

```

290

291

### Use waitFor when:

292

- Multiple assertions needed

293

- Complex conditions

294

- Checking element properties

295

296

```typescript

297

await waitFor(() => {

298

const button = screen.getByRole('button');

299

expect(button).toBeEnabled();

300

expect(button).toHaveTextContent('Submit');

301

});

302

```

303

304

## Common Pitfalls

305

306

### ❌ Wrong: Using getBy without waiting

307

```typescript

308

// Will fail if element not immediately present

309

fireEvent.click(button);

310

expect(screen.getByText('Success')).toBeInTheDocument(); // ❌

311

```

312

313

### ✅ Correct: Use findBy or waitFor

314

```typescript

315

fireEvent.click(button);

316

const success = await screen.findByText('Success'); // ✅

317

expect(success).toBeInTheDocument();

318

```

319

320

### ❌ Wrong: Return value instead of assertion

321

```typescript

322

await waitFor(() => element !== null); // ❌ Never retries

323

```

324

325

### ✅ Correct: Use assertions that throw

326

```typescript

327

await waitFor(() => {

328

expect(element).toBeInTheDocument(); // ✅ Throws until true

329

});

330

```

331

332

### ❌ Wrong: waitFor for synchronous state

333

```typescript

334

fireEvent.click(button);

335

await waitFor(() => { // ❌ Unnecessary

336

expect(screen.getByText('Count: 1')).toBeInTheDocument();

337

});

338

```

339

340

### ✅ Correct: Direct assertion for sync updates

341

```typescript

342

fireEvent.click(button);

343

expect(screen.getByText('Count: 1')).toBeInTheDocument(); // ✅

344

```

345

346

## Configuration

347

348

### Global Timeout

349

```typescript

350

import { configure } from '@testing-library/react';

351

352

configure({ asyncUtilTimeout: 2000 }); // 2 seconds default

353

```

354

355

### Per-Test Timeout

356

```typescript

357

test('slow operation', async () => {

358

await waitFor(

359

() => expect(screen.getByText('Done')).toBeInTheDocument(),

360

{ timeout: 10000 } // Override for this test

361

);

362

});

363

```

364

365

## Debugging

366

367

### Print State on Timeout

368

```typescript

369

await waitFor(

370

() => {

371

screen.debug(); // Print current DOM

372

expect(screen.getByText('Expected')).toBeInTheDocument();

373

},

374

{

375

onTimeout: (error) => {

376

screen.debug(); // Print final state

377

return error;

378

},

379

}

380

);

381

```

382

383

## Important Notes

384

385

### Automatic act() Wrapping

386

387

All async utilities automatically wrap operations in React's `act()`, ensuring state updates are properly flushed:

388

389

```typescript

390

// No manual act() needed

391

await waitFor(() => {

392

expect(screen.getByText('Updated')).toBeInTheDocument();

393

});

394

```

395

396

### Assertions in waitFor

397

398

The callback passed to `waitFor` should contain assertions or throw errors. `waitFor` will retry until the callback doesn't throw:

399

400

```typescript

401

// CORRECT: Assertion that throws

402

await waitFor(() => {

403

expect(element).toBeInTheDocument();

404

});

405

406

// WRONG: Always returns true, never retries

407

await waitFor(() => {

408

return element !== null;

409

});

410

```

411

412

### Default Timeouts

413

414

- Default timeout: 1000ms (1 second)

415

- Default interval: 50ms

416

- Can be configured globally via `configure()` or per-call via options

417

418

### waitFor vs findBy

419

420

Both work similarly, but have different use cases:

421

422

```typescript

423

// findBy: Convenient for single queries

424

const element = await screen.findByText('Hello');

425

426

// waitFor: Better for complex assertions

427

await waitFor(() => {

428

expect(screen.getByText('Hello')).toBeInTheDocument();

429

expect(screen.getByText('World')).toBeInTheDocument();

430

});

431

```

432

433

### Error Messages

434

435

When `waitFor` times out, it includes the last error thrown by the callback:

436

437

```typescript

438

// Timeout error will include "Expected element to be in document"

439

await waitFor(() => {

440

expect(screen.getByText('Missing')).toBeInTheDocument();

441

});

442

```

443

444

### MutationObserver

445

446

`waitFor` uses MutationObserver to efficiently wait for DOM changes. It will check the callback:

447

448

1. After every DOM mutation

449

2. At the specified interval (default 50ms)

450

3. Until timeout is reached (default 1000ms)

451

452

This makes it efficient for most async scenarios without constant polling.

453

454

## Best Practices

455

456

1. **Prefer findBy*** for simple waits

457

2. **Use waitFor** for complex conditions

458

3. **Set realistic timeouts** - default 1s usually sufficient

459

4. **Don't waitFor sync operations** - fireEvent updates are synchronous

460

5. **Assert in callbacks** - waitFor needs thrown errors to retry

461

6. **Use queryBy in waitFor** - for checking absence

462

7. **Avoid waitFor for everything** - only use when actually async

463