or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

compat.mdcomponents.mdcontext.mdcore.mddevtools.mdhooks.mdindex.mdjsx-runtime.mdtesting.md

testing.mddocs/

0

# Testing Utilities

1

2

Testing helpers for managing component rendering, effects flushing, and test environment setup. These utilities ensure reliable testing of Preact components with proper lifecycle management.

3

4

## Capabilities

5

6

### Test Environment Setup

7

8

Functions for initializing and managing the test environment.

9

10

```typescript { .api }

11

/**

12

* Sets up the test environment and returns a rerender function

13

* @returns Function to trigger component re-renders in tests

14

*/

15

function setupRerender(): () => void;

16

17

/**

18

* Wraps test code to ensure all effects and updates are flushed

19

* @param callback - Test code to execute

20

* @returns Promise that resolves when all updates are complete

21

*/

22

function act(callback: () => void | Promise<void>): Promise<void>;

23

24

/**

25

* Resets the test environment and cleans up Preact state

26

* Should be called after each test to ensure clean state

27

*/

28

function teardown(): void;

29

```

30

31

**Usage Examples:**

32

33

```typescript

34

import { setupRerender, act, teardown, render, createElement } from "preact/test-utils";

35

import { useState } from "preact/hooks";

36

37

// Basic test setup

38

describe('Component Tests', () => {

39

let rerender: () => void;

40

41

beforeEach(() => {

42

rerender = setupRerender();

43

});

44

45

afterEach(() => {

46

teardown();

47

});

48

49

test('component renders correctly', async () => {

50

function Counter() {

51

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

52

53

return createElement("div", null,

54

createElement("span", { "data-testid": "count" }, count),

55

createElement("button", {

56

onClick: () => setCount(c => c + 1),

57

"data-testid": "increment"

58

}, "Increment")

59

);

60

}

61

62

// Render component

63

const container = document.createElement('div');

64

document.body.appendChild(container);

65

66

await act(() => {

67

render(createElement(Counter), container);

68

});

69

70

const countElement = container.querySelector('[data-testid="count"]');

71

const buttonElement = container.querySelector('[data-testid="increment"]');

72

73

expect(countElement?.textContent).toBe('0');

74

75

// Test interaction

76

await act(() => {

77

buttonElement?.click();

78

});

79

80

expect(countElement?.textContent).toBe('1');

81

82

// Cleanup

83

document.body.removeChild(container);

84

});

85

});

86

87

// Testing with async effects

88

test('component with async effects', async () => {

89

const rerender = setupRerender();

90

91

function AsyncComponent({ userId }: { userId: number }) {

92

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

93

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

94

95

useEffect(() => {

96

const fetchUser = async () => {

97

setLoading(true);

98

// Mock async operation

99

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

100

setUser({ id: userId, name: `User ${userId}` });

101

setLoading(false);

102

};

103

104

fetchUser();

105

}, [userId]);

106

107

if (loading) return createElement("div", null, "Loading...");

108

if (!user) return createElement("div", null, "No user");

109

110

return createElement("div", null, user.name);

111

}

112

113

const container = document.createElement('div');

114

document.body.appendChild(container);

115

116

await act(async () => {

117

render(createElement(AsyncComponent, { userId: 1 }), container);

118

});

119

120

// Initially loading

121

expect(container.textContent).toBe('Loading...');

122

123

// Wait for async effect to complete

124

await act(async () => {

125

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

126

});

127

128

expect(container.textContent).toBe('User 1');

129

130

document.body.removeChild(container);

131

teardown();

132

});

133

```

134

135

### Component Testing Patterns

136

137

Common patterns for testing Preact components effectively.

138

139

**Usage Examples:**

140

141

```typescript

142

import { setupRerender, act, teardown, render, createElement } from "preact/test-utils";

143

import { useState, useEffect } from "preact/hooks";

144

145

// Testing component state changes

146

function testComponentState() {

147

const rerender = setupRerender();

148

149

function StatefulComponent() {

150

const [state, setState] = useState({ count: 0, name: '' });

151

152

const updateCount = () => setState(prev => ({ ...prev, count: prev.count + 1 }));

153

const updateName = (name: string) => setState(prev => ({ ...prev, name }));

154

155

return createElement("div", null,

156

createElement("div", { "data-testid": "count" }, state.count),

157

createElement("div", { "data-testid": "name" }, state.name),

158

createElement("button", {

159

onClick: updateCount,

160

"data-testid": "count-btn"

161

}, "Count++"),

162

createElement("input", {

163

value: state.name,

164

onChange: (e) => updateName((e.target as HTMLInputElement).value),

165

"data-testid": "name-input"

166

})

167

);

168

}

169

170

const container = document.createElement('div');

171

document.body.appendChild(container);

172

173

act(() => {

174

render(createElement(StatefulComponent), container);

175

});

176

177

const countEl = container.querySelector('[data-testid="count"]') as HTMLElement;

178

const nameEl = container.querySelector('[data-testid="name"]') as HTMLElement;

179

const countBtn = container.querySelector('[data-testid="count-btn"]') as HTMLButtonElement;

180

const nameInput = container.querySelector('[data-testid="name-input"]') as HTMLInputElement;

181

182

// Initial state

183

expect(countEl.textContent).toBe('0');

184

expect(nameEl.textContent).toBe('');

185

186

// Test count update

187

act(() => {

188

countBtn.click();

189

});

190

191

expect(countEl.textContent).toBe('1');

192

193

// Test name update

194

act(() => {

195

nameInput.value = 'John';

196

nameInput.dispatchEvent(new Event('input', { bubbles: true }));

197

});

198

199

expect(nameEl.textContent).toBe('John');

200

201

document.body.removeChild(container);

202

teardown();

203

}

204

205

// Testing component with context

206

function testComponentWithContext() {

207

const rerender = setupRerender();

208

209

const TestContext = createContext({ value: 'default' });

210

211

function ContextConsumer() {

212

const { value } = useContext(TestContext);

213

return createElement("div", { "data-testid": "context-value" }, value);

214

}

215

216

function TestWrapper({ contextValue, children }: {

217

contextValue: string;

218

children: ComponentChildren;

219

}) {

220

return createElement(TestContext.Provider, {

221

value: { value: contextValue }

222

}, children);

223

}

224

225

const container = document.createElement('div');

226

document.body.appendChild(container);

227

228

act(() => {

229

render(

230

createElement(TestWrapper, { contextValue: 'test-value' },

231

createElement(ContextConsumer)

232

),

233

container

234

);

235

});

236

237

const valueEl = container.querySelector('[data-testid="context-value"]') as HTMLElement;

238

expect(valueEl.textContent).toBe('test-value');

239

240

document.body.removeChild(container);

241

teardown();

242

}

243

244

// Testing component lifecycle

245

function testComponentLifecycle() {

246

const rerender = setupRerender();

247

const mountSpy = jest.fn();

248

const unmountSpy = jest.fn();

249

const updateSpy = jest.fn();

250

251

function LifecycleComponent({ value }: { value: string }) {

252

useEffect(() => {

253

mountSpy();

254

return () => unmountSpy();

255

}, []);

256

257

useEffect(() => {

258

updateSpy(value);

259

}, [value]);

260

261

return createElement("div", null, value);

262

}

263

264

const container = document.createElement('div');

265

document.body.appendChild(container);

266

267

// Mount

268

act(() => {

269

render(createElement(LifecycleComponent, { value: 'initial' }), container);

270

});

271

272

expect(mountSpy).toHaveBeenCalledTimes(1);

273

expect(updateSpy).toHaveBeenCalledWith('initial');

274

275

// Update

276

act(() => {

277

render(createElement(LifecycleComponent, { value: 'updated' }), container);

278

});

279

280

expect(updateSpy).toHaveBeenCalledWith('updated');

281

282

// Unmount

283

act(() => {

284

render(null, container);

285

});

286

287

expect(unmountSpy).toHaveBeenCalledTimes(1);

288

289

document.body.removeChild(container);

290

teardown();

291

}

292

```

293

294

### Advanced Testing Utilities

295

296

Helper functions and patterns for complex testing scenarios.

297

298

**Usage Examples:**

299

300

```typescript

301

import { setupRerender, act, teardown } from "preact/test-utils";

302

303

// Custom testing utilities

304

class TestRenderer {

305

container: HTMLElement;

306

rerender: () => void;

307

308

constructor() {

309

this.container = document.createElement('div');

310

document.body.appendChild(this.container);

311

this.rerender = setupRerender();

312

}

313

314

render(element: ComponentChildren) {

315

return act(() => {

316

render(element, this.container);

317

});

318

}

319

320

update(element: ComponentChildren) {

321

return this.render(element);

322

}

323

324

findByTestId(testId: string): HTMLElement | null {

325

return this.container.querySelector(`[data-testid="${testId}"]`);

326

}

327

328

findAllByTestId(testId: string): NodeListOf<HTMLElement> {

329

return this.container.querySelectorAll(`[data-testid="${testId}"]`);

330

}

331

332

findByText(text: string): HTMLElement | null {

333

const walker = document.createTreeWalker(

334

this.container,

335

NodeFilter.SHOW_TEXT,

336

null,

337

false

338

);

339

340

let node;

341

while (node = walker.nextNode()) {

342

if (node.textContent?.includes(text)) {

343

return node.parentElement;

344

}

345

}

346

return null;

347

}

348

349

cleanup() {

350

document.body.removeChild(this.container);

351

teardown();

352

}

353

}

354

355

// Mock async operations

356

function mockAsyncOperation<T>(result: T, delay = 100): Promise<T> {

357

return new Promise(resolve => {

358

setTimeout(() => resolve(result), delay);

359

});

360

}

361

362

// Testing hook-based components

363

function TestHookComponent({ hook }: { hook: () => any }) {

364

const result = hook();

365

366

return createElement("div", {

367

"data-testid": "hook-result"

368

}, JSON.stringify(result));

369

}

370

371

function testCustomHook(hook: () => any) {

372

const renderer = new TestRenderer();

373

374

renderer.render(createElement(TestHookComponent, { hook }));

375

376

const getResult = () => {

377

const element = renderer.findByTestId('hook-result');

378

return element ? JSON.parse(element.textContent || '{}') : null;

379

};

380

381

return {

382

result: { current: getResult() },

383

rerender: (newHook?: () => any) => {

384

renderer.update(createElement(TestHookComponent, { hook: newHook || hook }));

385

},

386

cleanup: () => renderer.cleanup()

387

};

388

}

389

390

// Example: Testing custom hook

391

function useCounter(initialValue = 0) {

392

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

393

394

const increment = () => setCount(c => c + 1);

395

const decrement = () => setCount(c => c - 1);

396

const reset = () => setCount(initialValue);

397

398

return { count, increment, decrement, reset };

399

}

400

401

test('useCounter hook', () => {

402

const { result, rerender, cleanup } = testCustomHook(() => useCounter(5));

403

404

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

405

406

act(() => {

407

result.current.increment();

408

});

409

410

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

411

412

act(() => {

413

result.current.decrement();

414

});

415

416

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

417

418

act(() => {

419

result.current.reset();

420

});

421

422

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

423

424

cleanup();

425

});

426

427

// Testing error boundaries

428

function TestErrorBoundary({

429

children,

430

onError

431

}: {

432

children: ComponentChildren;

433

onError?: (error: Error) => void;

434

}) {

435

return createElement(ErrorBoundary, {

436

onError,

437

fallback: createElement("div", { "data-testid": "error" }, "Error occurred")

438

}, children);

439

}

440

441

function ErrorThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {

442

if (shouldThrow) {

443

throw new Error('Test error');

444

}

445

446

return createElement("div", { "data-testid": "success" }, "No error");

447

}

448

449

test('error boundary handling', () => {

450

const renderer = new TestRenderer();

451

const errorSpy = jest.fn();

452

453

// Render without error

454

renderer.render(

455

createElement(TestErrorBoundary, { onError: errorSpy },

456

createElement(ErrorThrowingComponent, { shouldThrow: false })

457

)

458

);

459

460

expect(renderer.findByTestId('success')).toBeTruthy();

461

expect(renderer.findByTestId('error')).toBeFalsy();

462

463

// Render with error

464

act(() => {

465

renderer.update(

466

createElement(TestErrorBoundary, { onError: errorSpy },

467

createElement(ErrorThrowingComponent, { shouldThrow: true })

468

)

469

);

470

});

471

472

expect(renderer.findByTestId('error')).toBeTruthy();

473

expect(renderer.findByTestId('success')).toBeFalsy();

474

expect(errorSpy).toHaveBeenCalledWith(expect.any(Error));

475

476

renderer.cleanup();

477

});

478

```

479

480

### Integration Testing Patterns

481

482

Patterns for testing component integration and complex interactions.

483

484

**Usage Examples:**

485

486

```typescript

487

// Testing form components

488

function testFormIntegration() {

489

const renderer = new TestRenderer();

490

491

function ContactForm() {

492

const [formData, setFormData] = useState({

493

name: '',

494

email: '',

495

message: ''

496

});

497

const [submitted, setSubmitted] = useState(false);

498

499

const handleSubmit = (e: Event) => {

500

e.preventDefault();

501

setSubmitted(true);

502

};

503

504

const updateField = (field: string, value: string) => {

505

setFormData(prev => ({ ...prev, [field]: value }));

506

};

507

508

if (submitted) {

509

return createElement("div", { "data-testid": "success" }, "Form submitted!");

510

}

511

512

return createElement("form", { onSubmit: handleSubmit },

513

createElement("input", {

514

"data-testid": "name",

515

value: formData.name,

516

onChange: (e) => updateField('name', (e.target as HTMLInputElement).value),

517

placeholder: "Name"

518

}),

519

createElement("input", {

520

"data-testid": "email",

521

value: formData.email,

522

onChange: (e) => updateField('email', (e.target as HTMLInputElement).value),

523

placeholder: "Email"

524

}),

525

createElement("textarea", {

526

"data-testid": "message",

527

value: formData.message,

528

onChange: (e) => updateField('message', (e.target as HTMLTextAreaElement).value),

529

placeholder: "Message"

530

}),

531

createElement("button", {

532

type: "submit",

533

"data-testid": "submit"

534

}, "Submit")

535

);

536

}

537

538

act(() => {

539

renderer.render(createElement(ContactForm));

540

});

541

542

const nameInput = renderer.findByTestId('name') as HTMLInputElement;

543

const emailInput = renderer.findByTestId('email') as HTMLInputElement;

544

const messageInput = renderer.findByTestId('message') as HTMLTextAreaElement;

545

const submitButton = renderer.findByTestId('submit') as HTMLButtonElement;

546

547

// Fill form

548

act(() => {

549

nameInput.value = 'John Doe';

550

nameInput.dispatchEvent(new Event('input', { bubbles: true }));

551

552

emailInput.value = 'john@example.com';

553

emailInput.dispatchEvent(new Event('input', { bubbles: true }));

554

555

messageInput.value = 'Hello world';

556

messageInput.dispatchEvent(new Event('input', { bubbles: true }));

557

});

558

559

// Submit form

560

act(() => {

561

submitButton.click();

562

});

563

564

expect(renderer.findByTestId('success')).toBeTruthy();

565

566

renderer.cleanup();

567

}

568

```