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-testing.mddocs/

0

# Async Testing Utilities

1

2

Utilities for handling asynchronous behavior in React components, including waiting for elements to appear, disappear, and for arbitrary conditions to be met. These utilities are essential for testing components with async state updates, API calls, and delayed rendering.

3

4

## Capabilities

5

6

### WaitFor Function

7

8

Waits for a callback function to pass without throwing an error, with configurable timeout and polling interval.

9

10

```typescript { .api }

11

/**

12

* Wait for a condition to be true by repeatedly calling a callback

13

* @param callback - Function to test, can be sync or async

14

* @param options - Configuration for waiting behavior

15

* @returns Promise that resolves with callback return value

16

*/

17

function waitFor<T>(

18

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

19

options?: WaitForOptions

20

): Promise<T>;

21

22

interface WaitForOptions {

23

/** Container to search within for DOM queries */

24

container?: HTMLElement;

25

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

26

timeout?: number;

27

/** Polling interval in milliseconds (default: 50) */

28

interval?: number;

29

/** Custom error handler for timeout */

30

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

31

/** Suppress console errors during polling */

32

showOriginalStackTrace?: boolean;

33

}

34

```

35

36

**Basic Usage:**

37

38

```typescript

39

function AsyncComponent() {

40

const [data, setData] = useState(null);

41

const [loading, setLoading] = useState(true);

42

43

useEffect(() => {

44

setTimeout(() => {

45

setData('Async data loaded');

46

setLoading(false);

47

}, 1000);

48

}, []);

49

50

return (

51

<div>

52

{loading ? <p>Loading...</p> : <p>{data}</p>}

53

</div>

54

);

55

}

56

57

render(<AsyncComponent />);

58

59

// Wait for data to appear

60

await waitFor(() => {

61

expect(screen.getByText('Async data loaded')).toBeInTheDocument();

62

});

63

64

// Wait for loading to disappear

65

await waitFor(() => {

66

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

67

});

68

```

69

70

### WaitForElementToBeRemoved Function

71

72

Waits for elements to be removed from the DOM, useful for testing disappearing content.

73

74

```typescript { .api }

75

/**

76

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

77

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

78

* @param options - Configuration for waiting behavior

79

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

80

*/

81

function waitForElementToBeRemoved<T>(

82

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

83

options?: WaitForOptions

84

): Promise<void>;

85

```

86

87

**Usage Examples:**

88

89

```typescript

90

function DisappearingComponent() {

91

const [visible, setVisible] = useState(true);

92

93

useEffect(() => {

94

const timer = setTimeout(() => setVisible(false), 1000);

95

return () => clearTimeout(timer);

96

}, []);

97

98

return visible ? <div data-testid="disappearing">Will disappear</div> : null;

99

}

100

101

render(<DisappearingComponent />);

102

103

// Element is initially present

104

const element = screen.getByTestId('disappearing');

105

expect(element).toBeInTheDocument();

106

107

// Wait for it to be removed

108

await waitForElementToBeRemoved(element);

109

110

// Element is no longer in DOM

111

expect(screen.queryByTestId('disappearing')).not.toBeInTheDocument();

112

113

// Alternative: pass function

114

await waitForElementToBeRemoved(() => screen.queryByTestId('disappearing'));

115

```

116

117

## Advanced Async Patterns

118

119

### API Call Testing

120

121

```typescript

122

function UserProfile({ userId }: { userId: string }) {

123

const [user, setUser] = useState(null);

124

const [loading, setLoading] = useState(true);

125

const [error, setError] = useState(null);

126

127

useEffect(() => {

128

fetch(`/api/users/${userId}`)

129

.then(response => {

130

if (!response.ok) throw new Error('Failed to fetch');

131

return response.json();

132

})

133

.then(userData => {

134

setUser(userData);

135

setLoading(false);

136

})

137

.catch(err => {

138

setError(err.message);

139

setLoading(false);

140

});

141

}, [userId]);

142

143

if (loading) return <div>Loading user...</div>;

144

if (error) return <div>Error: {error}</div>;

145

146

return (

147

<div>

148

<h1>{user.name}</h1>

149

<p>{user.email}</p>

150

</div>

151

);

152

}

153

154

// Mock fetch

155

global.fetch = jest.fn();

156

157

// Test successful API call

158

fetch.mockResolvedValueOnce({

159

ok: true,

160

json: async () => ({ name: 'John Doe', email: 'john@example.com' })

161

});

162

163

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

164

165

// Initially loading

166

expect(screen.getByText('Loading user...')).toBeInTheDocument();

167

168

// Wait for data to load

169

await waitFor(() => {

170

expect(screen.getByText('John Doe')).toBeInTheDocument();

171

});

172

173

expect(screen.getByText('john@example.com')).toBeInTheDocument();

174

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

175

176

// Test error scenario

177

fetch.mockRejectedValueOnce(new Error('Network error'));

178

179

rerender(<UserProfile userId="456" />);

180

181

await waitFor(() => {

182

expect(screen.getByText('Error: Network error')).toBeInTheDocument();

183

});

184

```

185

186

### Form Validation Testing

187

188

```typescript

189

function ValidatedForm() {

190

const [email, setEmail] = useState('');

191

const [errors, setErrors] = useState({});

192

const [isValidating, setIsValidating] = useState(false);

193

194

const validateEmail = async (value) => {

195

setIsValidating(true);

196

197

// Simulate async validation

198

await new Promise(resolve => setTimeout(resolve, 500));

199

200

const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);

201

202

setErrors(prev => ({

203

...prev,

204

email: isValid ? null : 'Invalid email format'

205

}));

206

207

setIsValidating(false);

208

};

209

210

const handleEmailChange = (e) => {

211

const value = e.target.value;

212

setEmail(value);

213

214

if (value) {

215

validateEmail(value);

216

} else {

217

setErrors(prev => ({ ...prev, email: null }));

218

}

219

};

220

221

return (

222

<form>

223

<input

224

type="email"

225

value={email}

226

onChange={handleEmailChange}

227

placeholder="Enter email"

228

data-testid="email-input"

229

/>

230

{isValidating && <span>Validating...</span>}

231

{errors.email && <span data-testid="email-error">{errors.email}</span>}

232

</form>

233

);

234

}

235

236

render(<ValidatedForm />);

237

238

const emailInput = screen.getByTestId('email-input');

239

240

// Type invalid email

241

fireEvent.change(emailInput, { target: { value: 'invalid-email' } });

242

243

// Wait for validation to start

244

await waitFor(() => {

245

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

246

});

247

248

// Wait for validation to complete

249

await waitFor(() => {

250

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

251

});

252

253

// Check error appears

254

expect(screen.getByTestId('email-error')).toHaveTextContent('Invalid email format');

255

256

// Type valid email

257

fireEvent.change(emailInput, { target: { value: 'valid@example.com' } });

258

259

// Wait for error to disappear

260

await waitFor(() => {

261

expect(screen.queryByTestId('email-error')).not.toBeInTheDocument();

262

});

263

```

264

265

### Animation Testing

266

267

```typescript

268

function AnimatedComponent() {

269

const [isVisible, setIsVisible] = useState(false);

270

const [animationClass, setAnimationClass] = useState('');

271

272

const show = () => {

273

setIsVisible(true);

274

setAnimationClass('fade-in');

275

276

// Remove animation class after animation completes

277

setTimeout(() => setAnimationClass(''), 300);

278

};

279

280

const hide = () => {

281

setAnimationClass('fade-out');

282

283

// Hide element after animation completes

284

setTimeout(() => {

285

setIsVisible(false);

286

setAnimationClass('');

287

}, 300);

288

};

289

290

return (

291

<div>

292

<button onClick={show}>Show</button>

293

<button onClick={hide}>Hide</button>

294

{isVisible && (

295

<div

296

className={animationClass}

297

data-testid="animated-element"

298

>

299

Animated content

300

</div>

301

)}

302

</div>

303

);

304

}

305

306

render(<AnimatedComponent />);

307

308

// Initially hidden

309

expect(screen.queryByTestId('animated-element')).not.toBeInTheDocument();

310

311

// Show element

312

fireEvent.click(screen.getByText('Show'));

313

314

// Wait for element to appear

315

await waitFor(() => {

316

expect(screen.getByTestId('animated-element')).toBeInTheDocument();

317

});

318

319

// Check animation class is applied

320

expect(screen.getByTestId('animated-element')).toHaveClass('fade-in');

321

322

// Wait for animation to complete

323

await waitFor(() => {

324

expect(screen.getByTestId('animated-element')).not.toHaveClass('fade-in');

325

});

326

327

// Hide element

328

fireEvent.click(screen.getByText('Hide'));

329

330

// Check fade-out class is applied

331

await waitFor(() => {

332

expect(screen.getByTestId('animated-element')).toHaveClass('fade-out');

333

});

334

335

// Wait for element to be removed

336

await waitForElementToBeRemoved(screen.getByTestId('animated-element'));

337

```

338

339

### Debounced Input Testing

340

341

```typescript

342

function SearchComponent() {

343

const [query, setQuery] = useState('');

344

const [results, setResults] = useState([]);

345

const [isSearching, setIsSearching] = useState(false);

346

347

useEffect(() => {

348

if (!query) {

349

setResults([]);

350

return;

351

}

352

353

setIsSearching(true);

354

355

const searchTimer = setTimeout(async () => {

356

// Simulate API call

357

const response = await fetch(`/api/search?q=${query}`);

358

const data = await response.json();

359

360

setResults(data.results);

361

setIsSearching(false);

362

}, 300);

363

364

return () => {

365

clearTimeout(searchTimer);

366

setIsSearching(false);

367

};

368

}, [query]);

369

370

return (

371

<div>

372

<input

373

type="text"

374

value={query}

375

onChange={(e) => setQuery(e.target.value)}

376

placeholder="Search..."

377

data-testid="search-input"

378

/>

379

{isSearching && <div data-testid="searching">Searching...</div>}

380

<ul>

381

{results.map((result, index) => (

382

<li key={index} data-testid="result-item">

383

{result.title}

384

</li>

385

))}

386

</ul>

387

</div>

388

);

389

}

390

391

// Mock fetch

392

global.fetch = jest.fn(() =>

393

Promise.resolve({

394

json: () => Promise.resolve({

395

results: [

396

{ title: 'Search result 1' },

397

{ title: 'Search result 2' }

398

]

399

})

400

})

401

);

402

403

render(<SearchComponent />);

404

405

const searchInput = screen.getByTestId('search-input');

406

407

// Type search query

408

fireEvent.change(searchInput, { target: { value: 'test query' } });

409

410

// Wait for debounced search to start

411

await waitFor(() => {

412

expect(screen.getByTestId('searching')).toBeInTheDocument();

413

});

414

415

// Wait for search to complete

416

await waitFor(() => {

417

expect(screen.queryByTestId('searching')).not.toBeInTheDocument();

418

});

419

420

// Check results appear

421

const resultItems = screen.getAllByTestId('result-item');

422

expect(resultItems).toHaveLength(2);

423

expect(resultItems[0]).toHaveTextContent('Search result 1');

424

```

425

426

### Error Boundary Testing

427

428

```typescript

429

function ErrorBoundary({ children }) {

430

const [hasError, setHasError] = useState(false);

431

432

useEffect(() => {

433

const handleError = () => setHasError(true);

434

window.addEventListener('error', handleError);

435

return () => window.removeEventListener('error', handleError);

436

}, []);

437

438

if (hasError) {

439

return <div data-testid="error-message">Something went wrong</div>;

440

}

441

442

return children;

443

}

444

445

function ThrowingComponent({ shouldThrow }) {

446

useEffect(() => {

447

if (shouldThrow) {

448

setTimeout(() => {

449

throw new Error('Async error');

450

}, 100);

451

}

452

}, [shouldThrow]);

453

454

return <div>Component content</div>;

455

}

456

457

render(

458

<ErrorBoundary>

459

<ThrowingComponent shouldThrow={true} />

460

</ErrorBoundary>

461

);

462

463

// Initially no error

464

expect(screen.getByText('Component content')).toBeInTheDocument();

465

expect(screen.queryByTestId('error-message')).not.toBeInTheDocument();

466

467

// Wait for error to be thrown and handled

468

await waitFor(() => {

469

expect(screen.getByTestId('error-message')).toBeInTheDocument();

470

});

471

472

expect(screen.queryByText('Component content')).not.toBeInTheDocument();

473

```

474

475

### Custom Timeout and Error Handling

476

477

```typescript

478

function SlowComponent() {

479

const [data, setData] = useState(null);

480

481

useEffect(() => {

482

// Very slow operation

483

setTimeout(() => {

484

setData('Finally loaded');

485

}, 2000);

486

}, []);

487

488

return data ? <div>{data}</div> : <div>Loading...</div>;

489

}

490

491

render(<SlowComponent />);

492

493

// Custom timeout for slow operations

494

await waitFor(

495

() => {

496

expect(screen.getByText('Finally loaded')).toBeInTheDocument();

497

},

498

{ timeout: 3000 } // Wait up to 3 seconds

499

);

500

501

// Custom error message

502

await waitFor(

503

() => {

504

expect(screen.getByText('Non-existent text')).toBeInTheDocument();

505

},

506

{

507

timeout: 1000,

508

onTimeout: (error) => new Error('Custom timeout message: Element not found')

509

}

510

).catch(error => {

511

expect(error.message).toContain('Custom timeout message');

512

});

513

514

// Custom polling interval

515

let pollCount = 0;

516

await waitFor(

517

() => {

518

pollCount++;

519

return expect(screen.getByText('Finally loaded')).toBeInTheDocument();

520

},

521

{ interval: 100 } // Check every 100ms instead of default 50ms

522

);

523

```