or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

act-utilities.mdasync-testing.mdcleanup-management.mderror-handling.mdhook-rendering.mdindex.mdserver-side-rendering.md

async-testing.mddocs/

0

# Async Testing

1

2

Utilities for testing asynchronous hook behavior, including waiting for updates, value changes, and condition fulfillment. These utilities are essential for testing hooks that perform async operations or have delayed effects.

3

4

## Capabilities

5

6

### waitFor

7

8

Waits for a condition to become true, with configurable timeout and polling interval. Useful for testing hooks that have async side effects or delayed updates.

9

10

```typescript { .api }

11

/**

12

* Wait for a condition to become true

13

* @param callback - Function that returns true when condition is met, or void (treated as true)

14

* @param options - Configuration for timeout and polling interval

15

* @returns Promise that resolves when condition is met

16

* @throws TimeoutError if condition is not met within timeout

17

*/

18

waitFor(callback: () => boolean | void, options?: WaitForOptions): Promise<void>;

19

20

interface WaitForOptions {

21

/** Polling interval in milliseconds (default: 50ms, false disables polling) */

22

interval?: number | false;

23

/** Timeout in milliseconds (default: 1000ms, false disables timeout) */

24

timeout?: number | false;

25

}

26

```

27

28

**Usage Examples:**

29

30

```typescript

31

import { renderHook } from "@testing-library/react-hooks";

32

import { useState, useEffect } from "react";

33

34

function useAsyncData(url: string) {

35

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

36

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

37

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

38

39

useEffect(() => {

40

setLoading(true);

41

setError(null);

42

43

fetch(url)

44

.then(response => response.json())

45

.then(data => {

46

setData(data);

47

setLoading(false);

48

})

49

.catch(error => {

50

setError(error);

51

setLoading(false);

52

});

53

}, [url]);

54

55

return { data, loading, error };

56

}

57

58

test("async data loading", async () => {

59

// Mock fetch

60

global.fetch = jest.fn().mockResolvedValue({

61

json: () => Promise.resolve({ id: 1, name: "Test Data" })

62

});

63

64

const { result, waitFor } = renderHook(() => useAsyncData("/api/data"));

65

66

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

67

expect(result.current.data).toBe(null);

68

69

// Wait for loading to complete

70

await waitFor(() => !result.current.loading);

71

72

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

73

expect(result.current.data).toEqual({ id: 1, name: "Test Data" });

74

expect(result.current.error).toBe(null);

75

});

76

77

// Wait for specific condition

78

test("wait for specific value", async () => {

79

const { result, waitFor } = renderHook(() => useAsyncData("/api/data"));

80

81

// Wait for data to be loaded

82

await waitFor(() => result.current.data !== null);

83

84

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

85

});

86

87

// Custom timeout and interval

88

test("custom wait options", async () => {

89

const { result, waitFor } = renderHook(() => useAsyncData("/api/slow"));

90

91

// Wait with custom timeout and interval

92

await waitFor(

93

() => !result.current.loading,

94

{ timeout: 5000, interval: 100 }

95

);

96

97

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

98

});

99

```

100

101

### waitForValueToChange

102

103

Waits for a selected value to change from its current value. Useful when you want to wait for a specific piece of state to update.

104

105

```typescript { .api }

106

/**

107

* Wait for a selected value to change from its current value

108

* @param selector - Function that returns the value to monitor

109

* @param options - Configuration for timeout and polling interval

110

* @returns Promise that resolves when the value changes

111

* @throws TimeoutError if value doesn't change within timeout

112

*/

113

waitForValueToChange(

114

selector: () => unknown,

115

options?: WaitForValueToChangeOptions

116

): Promise<void>;

117

118

interface WaitForValueToChangeOptions {

119

/** Polling interval in milliseconds (default: 50ms, false disables polling) */

120

interval?: number | false;

121

/** Timeout in milliseconds (default: 1000ms, false disables timeout) */

122

timeout?: number | false;

123

}

124

```

125

126

**Usage Examples:**

127

128

```typescript

129

function useCounter() {

130

const [count, setCount] = useState(0);

131

132

const incrementAsync = async () => {

133

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

134

setCount(prev => prev + 1);

135

};

136

137

return { count, incrementAsync };

138

}

139

140

test("wait for value to change", async () => {

141

const { result, waitForValueToChange } = renderHook(() => useCounter());

142

143

expect(result.current.count).toBe(0);

144

145

// Start async operation

146

result.current.incrementAsync();

147

148

// Wait for count to change

149

await waitForValueToChange(() => result.current.count);

150

151

expect(result.current.count).toBe(1);

152

});

153

154

// Wait for nested property changes

155

function useUserProfile(userId: string) {

156

const [profile, setProfile] = useState({ name: "", email: "", loading: true });

157

158

useEffect(() => {

159

fetchUserProfile(userId).then(data => {

160

setProfile({ ...data, loading: false });

161

});

162

}, [userId]);

163

164

return profile;

165

}

166

167

test("wait for nested property change", async () => {

168

const { result, waitForValueToChange } = renderHook(() =>

169

useUserProfile("123")

170

);

171

172

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

173

174

// Wait for loading state to change

175

await waitForValueToChange(() => result.current.loading);

176

177

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

178

expect(result.current.name).toBeTruthy();

179

});

180

```

181

182

### waitForNextUpdate

183

184

Waits for the next hook update/re-render, regardless of what causes it. This is useful when you know an update will happen but don't know exactly what will change.

185

186

```typescript { .api }

187

/**

188

* Wait for the next hook update/re-render

189

* @param options - Configuration for timeout

190

* @returns Promise that resolves on the next update

191

* @throws TimeoutError if no update occurs within timeout

192

*/

193

waitForNextUpdate(options?: WaitForNextUpdateOptions): Promise<void>;

194

195

interface WaitForNextUpdateOptions {

196

/** Timeout in milliseconds (default: 1000ms, false disables timeout) */

197

timeout?: number | false;

198

}

199

```

200

201

**Usage Examples:**

202

203

```typescript

204

function useWebSocket(url: string) {

205

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

206

const [connected, setConnected] = useState(false);

207

208

useEffect(() => {

209

const ws = new WebSocket(url);

210

211

ws.onopen = () => setConnected(true);

212

ws.onmessage = (event) => setData(JSON.parse(event.data));

213

ws.onclose = () => setConnected(false);

214

215

return () => ws.close();

216

}, [url]);

217

218

return { data, connected };

219

}

220

221

test("websocket updates", async () => {

222

const mockWebSocket = {

223

onopen: null,

224

onmessage: null,

225

onclose: null,

226

close: jest.fn()

227

};

228

229

global.WebSocket = jest.fn(() => mockWebSocket);

230

231

const { result, waitForNextUpdate } = renderHook(() =>

232

useWebSocket("ws://localhost:8080")

233

);

234

235

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

236

237

// Simulate connection

238

mockWebSocket.onopen();

239

240

// Wait for next update (connection state change)

241

await waitForNextUpdate();

242

243

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

244

245

// Simulate message

246

mockWebSocket.onmessage({ data: JSON.stringify({ message: "Hello" }) });

247

248

// Wait for next update (data change)

249

await waitForNextUpdate();

250

251

expect(result.current.data).toEqual({ message: "Hello" });

252

});

253

254

// With custom timeout

255

test("wait for update with timeout", async () => {

256

const { waitForNextUpdate } = renderHook(() => useWebSocket("ws://test"));

257

258

// Wait with longer timeout

259

await waitForNextUpdate({ timeout: 2000 });

260

});

261

```

262

263

### Error Handling in Async Utils

264

265

All async utilities can throw `TimeoutError` when operations don't complete within the specified timeout:

266

267

```typescript

268

import { renderHook } from "@testing-library/react-hooks";

269

270

function useNeverUpdating() {

271

const [value] = useState("static");

272

return value;

273

}

274

275

test("timeout error handling", async () => {

276

const { result, waitForValueToChange } = renderHook(() => useNeverUpdating());

277

278

// This will throw TimeoutError after 100ms

279

await expect(

280

waitForValueToChange(() => result.current, { timeout: 100 })

281

).rejects.toThrow("Timed out");

282

});

283

284

test("no timeout with false", async () => {

285

const { result, waitFor } = renderHook(() => useNeverUpdating());

286

287

// This would wait forever, but we'll resolve it manually

288

const waitPromise = waitFor(() => false, { timeout: false });

289

290

// In real tests, you'd trigger the condition to become true

291

// For this example, we'll just verify the promise doesn't reject immediately

292

const timeoutPromise = new Promise((_, reject) =>

293

setTimeout(() => reject(new Error("Should not timeout")), 100)

294

);

295

296

// The wait should not timeout

297

await expect(Promise.race([waitPromise, timeoutPromise])).rejects.toThrow("Should not timeout");

298

});

299

```

300

301

### Combining Async Utilities

302

303

You can combine multiple async utilities for complex testing scenarios:

304

305

```typescript

306

function useComplexAsync() {

307

const [step, setStep] = useState(1);

308

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

309

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

310

311

useEffect(() => {

312

if (step === 1) {

313

setTimeout(() => setStep(2), 100);

314

} else if (step === 2) {

315

fetchData()

316

.then(setData)

317

.catch(setError)

318

.finally(() => setStep(3));

319

}

320

}, [step]);

321

322

return { step, data, error };

323

}

324

325

test("complex async flow", async () => {

326

const { result, waitFor, waitForValueToChange, waitForNextUpdate } =

327

renderHook(() => useComplexAsync());

328

329

expect(result.current.step).toBe(1);

330

331

// Wait for step to change to 2

332

await waitForValueToChange(() => result.current.step);

333

expect(result.current.step).toBe(2);

334

335

// Wait for data or error

336

await waitFor(() =>

337

result.current.data !== null || result.current.error !== null

338

);

339

340

// Wait for final step

341

await waitForValueToChange(() => result.current.step);

342

expect(result.current.step).toBe(3);

343

});

344

```

345

346

## Types

347

348

```typescript { .api }

349

class TimeoutError extends Error {

350

constructor(util: Function, timeout: number);

351

}

352

```