or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-control.mdindex.mdmobile-device.mdnetwork-api.mdpage-interaction.mdtesting-framework.mdvisual-debugging.md

visual-debugging.mddocs/

0

# Visual Testing & Debugging

1

2

Screenshot comparison, video recording, tracing, debugging tools, and accessibility testing for comprehensive test development and maintenance.

3

4

## Capabilities

5

6

### Screenshot Testing

7

8

Capture and compare screenshots for visual regression testing.

9

10

```typescript { .api }

11

interface Page {

12

/** Take page screenshot */

13

screenshot(options?: PageScreenshotOptions): Promise<Buffer>;

14

}

15

16

interface Locator {

17

/** Take element screenshot */

18

screenshot(options?: LocatorScreenshotOptions): Promise<Buffer>;

19

}

20

21

interface PageScreenshotOptions {

22

/** Screenshot file path */

23

path?: string;

24

/** Screenshot type */

25

type?: 'png' | 'jpeg';

26

/** Image quality (0-100, JPEG only) */

27

quality?: number;

28

/** Omit default background */

29

omitBackground?: boolean;

30

/** Full page screenshot */

31

fullPage?: boolean;

32

/** Clipping rectangle */

33

clip?: { x: number; y: number; width: number; height: number };

34

/** Screenshot animations */

35

animations?: 'disabled' | 'allow';

36

/** Screenshot caret */

37

caret?: 'hide' | 'initial';

38

/** Screenshot scale */

39

scale?: 'css' | 'device';

40

/** Screenshot mask */

41

mask?: Locator[];

42

/** Screenshot timeout */

43

timeout?: number;

44

}

45

46

interface LocatorScreenshotOptions {

47

/** Screenshot file path */

48

path?: string;

49

/** Screenshot type */

50

type?: 'png' | 'jpeg';

51

/** Image quality (0-100, JPEG only) */

52

quality?: number;

53

/** Omit default background */

54

omitBackground?: boolean;

55

/** Screenshot animations */

56

animations?: 'disabled' | 'allow';

57

/** Screenshot caret */

58

caret?: 'hide' | 'initial';

59

/** Screenshot scale */

60

scale?: 'css' | 'device';

61

/** Screenshot mask */

62

mask?: Locator[];

63

/** Screenshot timeout */

64

timeout?: number;

65

}

66

```

67

68

**Usage Examples:**

69

70

```typescript

71

// Basic page screenshot

72

await page.screenshot({ path: 'page.png' });

73

74

// Full page screenshot

75

await page.screenshot({

76

path: 'full-page.png',

77

fullPage: true

78

});

79

80

// Element screenshot

81

const element = page.locator('.hero-section');

82

await element.screenshot({ path: 'hero.png' });

83

84

// Screenshot with clipping

85

await page.screenshot({

86

path: 'clipped.png',

87

clip: { x: 0, y: 0, width: 800, height: 600 }

88

});

89

90

// Screenshot with masks (hide dynamic content)

91

await page.screenshot({

92

path: 'masked.png',

93

mask: [

94

page.locator('.timestamp'),

95

page.locator('.user-avatar')

96

]

97

});

98

99

// High quality JPEG

100

await page.screenshot({

101

path: 'page.jpg',

102

type: 'jpeg',

103

quality: 90

104

});

105

106

// Screenshot without animations

107

await page.screenshot({

108

path: 'static.png',

109

animations: 'disabled'

110

});

111

```

112

113

### Visual Assertions

114

115

Built-in visual comparison assertions for screenshot testing.

116

117

```typescript { .api }

118

interface PlaywrightAssertions<T> {

119

/** Page screenshot assertion */

120

toHaveScreenshot(options?: PageAssertionsToHaveScreenshotOptions): Promise<void>;

121

toHaveScreenshot(name?: string, options?: PageAssertionsToHaveScreenshotOptions): Promise<void>;

122

123

/** Locator screenshot assertion */

124

toHaveScreenshot(options?: LocatorAssertionsToHaveScreenshotOptions): Promise<void>;

125

toHaveScreenshot(name?: string, options?: LocatorAssertionsToHaveScreenshotOptions): Promise<void>;

126

}

127

128

interface PageAssertionsToHaveScreenshotOptions {

129

/** Screenshot animations */

130

animations?: 'disabled' | 'allow';

131

/** Screenshot caret */

132

caret?: 'hide' | 'initial';

133

/** Clipping rectangle */

134

clip?: { x: number; y: number; width: number; height: number };

135

/** Full page screenshot */

136

fullPage?: boolean;

137

/** Screenshot mask */

138

mask?: Locator[];

139

/** Screenshot mode */

140

mode?: 'light' | 'dark';

141

/** Omit background */

142

omitBackground?: boolean;

143

/** Screenshot scale */

144

scale?: 'css' | 'device';

145

/** Comparison threshold */

146

threshold?: number;

147

/** Screenshot timeout */

148

timeout?: number;

149

/** Update snapshots */

150

updateSnapshots?: 'all' | 'none' | 'missing';

151

}

152

153

interface LocatorAssertionsToHaveScreenshotOptions {

154

/** Screenshot animations */

155

animations?: 'disabled' | 'allow';

156

/** Screenshot caret */

157

caret?: 'hide' | 'initial';

158

/** Screenshot mask */

159

mask?: Locator[];

160

/** Omit background */

161

omitBackground?: boolean;

162

/** Screenshot scale */

163

scale?: 'css' | 'device';

164

/** Comparison threshold */

165

threshold?: number;

166

/** Screenshot timeout */

167

timeout?: number;

168

/** Update snapshots */

169

updateSnapshots?: 'all' | 'none' | 'missing';

170

}

171

```

172

173

**Usage Examples:**

174

175

```typescript

176

import { test, expect } from 'playwright/test';

177

178

test('visual regression test', async ({ page }) => {

179

await page.goto('https://example.com');

180

181

// Page screenshot assertion

182

await expect(page).toHaveScreenshot();

183

await expect(page).toHaveScreenshot('homepage.png');

184

185

// Element screenshot assertion

186

const header = page.locator('header');

187

await expect(header).toHaveScreenshot('header.png');

188

189

// Full page with threshold

190

await expect(page).toHaveScreenshot('full-page.png', {

191

fullPage: true,

192

threshold: 0.1 // Allow 10% difference

193

});

194

195

// Dark mode screenshot

196

await page.emulateMedia({ colorScheme: 'dark' });

197

await expect(page).toHaveScreenshot('dark-mode.png', {

198

mode: 'dark'

199

});

200

201

// Masked screenshot (hide dynamic content)

202

await expect(page).toHaveScreenshot('masked.png', {

203

mask: [

204

page.locator('.timestamp'),

205

page.locator('.live-counter')

206

]

207

});

208

});

209

```

210

211

### Video Recording

212

213

Record browser sessions as video files for debugging and documentation.

214

215

```typescript { .api }

216

interface BrowserContextOptions {

217

/** Video recording options */

218

recordVideo?: {

219

/** Video output directory */

220

dir: string;

221

/** Video dimensions */

222

size?: ViewportSize;

223

/** Video mode */

224

mode?: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';

225

};

226

}

227

228

interface Page {

229

/** Get video recording */

230

video(): Video | null;

231

}

232

233

interface Video {

234

/** Get video file path */

235

path(): Promise<string>;

236

/** Delete video file */

237

delete(): Promise<void>;

238

/** Save video to path */

239

saveAs(path: string): Promise<void>;

240

}

241

```

242

243

**Usage Examples:**

244

245

```typescript

246

// Enable video recording for context

247

const context = await browser.newContext({

248

recordVideo: {

249

dir: './videos/',

250

size: { width: 1280, height: 720 }

251

}

252

});

253

254

const page = await context.newPage();

255

await page.goto('https://example.com');

256

await page.click('button');

257

258

// Get video path

259

const video = page.video();

260

if (video) {

261

const videoPath = await video.path();

262

console.log('Video saved to:', videoPath);

263

264

// Save video with custom name

265

await video.saveAs('./custom-video.webm');

266

}

267

268

await context.close();

269

270

// Video recording in test configuration

271

// playwright.config.ts

272

export default defineConfig({

273

use: {

274

video: 'retain-on-failure', // Only save videos for failed tests

275

}

276

});

277

```

278

279

### Tracing & Timeline

280

281

Record detailed execution traces for debugging and performance analysis.

282

283

```typescript { .api }

284

interface BrowserContext {

285

/** Start tracing */

286

tracing: Tracing;

287

}

288

289

interface Tracing {

290

/** Start trace recording */

291

start(options?: TracingStartOptions): Promise<void>;

292

/** Start trace chunk */

293

startChunk(options?: TracingStartChunkOptions): Promise<void>;

294

/** Stop trace chunk */

295

stopChunk(options?: TracingStopChunkOptions): Promise<void>;

296

/** Stop trace recording */

297

stop(options?: TracingStopOptions): Promise<void>;

298

}

299

300

interface TracingStartOptions {

301

/** Trace file name */

302

name?: string;

303

/** Trace title */

304

title?: string;

305

/** Trace screenshots */

306

screenshots?: boolean;

307

/** Trace snapshots */

308

snapshots?: boolean;

309

/** Trace sources */

310

sources?: boolean;

311

}

312

313

interface TracingStopOptions {

314

/** Trace output path */

315

path?: string;

316

}

317

318

interface TracingStartChunkOptions {

319

/** Trace title */

320

title?: string;

321

}

322

323

interface TracingStopChunkOptions {

324

/** Trace output path */

325

path?: string;

326

}

327

```

328

329

**Usage Examples:**

330

331

```typescript

332

// Start tracing

333

await context.tracing.start({

334

screenshots: true,

335

snapshots: true,

336

sources: true

337

});

338

339

// Perform actions

340

await page.goto('https://example.com');

341

await page.click('button');

342

await page.fill('input', 'test');

343

344

// Stop tracing and save

345

await context.tracing.stop({

346

path: 'trace.zip'

347

});

348

349

// Chunked tracing for long tests

350

await context.tracing.start();

351

352

// First chunk

353

await context.tracing.startChunk({ title: 'Login' });

354

await doLogin(page);

355

await context.tracing.stopChunk({ path: 'login-trace.zip' });

356

357

// Second chunk

358

await context.tracing.startChunk({ title: 'Navigation' });

359

await navigateApp(page);

360

await context.tracing.stopChunk({ path: 'navigation-trace.zip' });

361

362

await context.tracing.stop();

363

364

// Tracing in tests

365

test('with tracing', async ({ page, context }) => {

366

await context.tracing.start({ screenshots: true });

367

368

// Test actions

369

await page.goto('/app');

370

await page.click('#start');

371

372

// Auto-save trace on failure via test configuration

373

});

374

```

375

376

### Debug Tools & Inspector

377

378

Browser developer tools integration and debugging utilities.

379

380

```typescript { .api }

381

interface Page {

382

/** Pause execution and open debugger */

383

pause(): Promise<void>;

384

/** Wait for debugger to attach */

385

waitForDebugger(): Promise<void>;

386

/** Bring page to front */

387

bringToFront(): Promise<void>;

388

}

389

390

interface BrowserContext {

391

/** Create CDP session */

392

newCDPSession(page: Page): Promise<CDPSession>;

393

}

394

395

interface CDPSession {

396

/** Send CDP command */

397

send(method: string, params?: any): Promise<any>;

398

/** Detach from target */

399

detach(): Promise<void>;

400

}

401

402

interface LaunchOptions {

403

/** Open browser devtools */

404

devtools?: boolean;

405

/** Run in slow motion */

406

slowMo?: number;

407

}

408

```

409

410

**Usage Examples:**

411

412

```typescript

413

// Launch with devtools open

414

const browser = await chromium.launch({

415

headless: false,

416

devtools: true

417

});

418

419

// Slow motion for debugging

420

const browserSlow = await chromium.launch({

421

headless: false,

422

slowMo: 1000 // 1 second delay between actions

423

});

424

425

// Pause execution for debugging

426

test('debug test', async ({ page }) => {

427

await page.goto('https://example.com');

428

429

// Pause here - opens browser with debugger

430

await page.pause();

431

432

await page.click('button');

433

});

434

435

// CDP access for advanced debugging

436

const cdpSession = await context.newCDPSession(page);

437

438

// Enable runtime events

439

await cdpSession.send('Runtime.enable');

440

441

// Set breakpoint

442

await cdpSession.send('Debugger.enable');

443

await cdpSession.send('Debugger.setBreakpointByUrl', {

444

lineNumber: 10,

445

url: 'https://example.com/script.js'

446

});

447

448

// Get performance metrics

449

const metrics = await cdpSession.send('Performance.getMetrics');

450

console.log('Performance metrics:', metrics);

451

```

452

453

### Accessibility Testing

454

455

Built-in accessibility testing and reporting capabilities.

456

457

```typescript { .api }

458

interface Page {

459

/** Get accessibility tree */

460

accessibility: Accessibility;

461

}

462

463

interface Accessibility {

464

/** Get accessibility snapshot */

465

snapshot(options?: AccessibilitySnapshotOptions): Promise<AccessibilityNode | null>;

466

}

467

468

interface AccessibilitySnapshotOptions {

469

/** Include interesting nodes only */

470

interestingOnly?: boolean;

471

/** Root element */

472

root?: ElementHandle;

473

}

474

475

interface AccessibilityNode {

476

/** Node role */

477

role?: string;

478

/** Node name */

479

name?: string;

480

/** Node value */

481

value?: string | number;

482

/** Node description */

483

description?: string;

484

/** Keyboard shortcut */

485

keyshortcuts?: string;

486

/** Role description */

487

roledescription?: string;

488

/** Node orientation */

489

orientation?: string;

490

/** Auto-complete attribute */

491

autocomplete?: string;

492

/** Has popup */

493

haspopup?: string;

494

/** Hierarchical level */

495

level?: number;

496

/** Disabled state */

497

disabled?: boolean;

498

/** Expanded state */

499

expanded?: boolean;

500

/** Focused state */

501

focused?: boolean;

502

/** Modal state */

503

modal?: boolean;

504

/** Multiline state */

505

multiline?: boolean;

506

/** Multiselectable state */

507

multiselectable?: boolean;

508

/** Readonly state */

509

readonly?: boolean;

510

/** Required state */

511

required?: boolean;

512

/** Selected state */

513

selected?: boolean;

514

/** Checked state */

515

checked?: boolean | 'mixed';

516

/** Pressed state */

517

pressed?: boolean | 'mixed';

518

/** Current value */

519

valuemin?: number;

520

/** Maximum value */

521

valuemax?: number;

522

/** Current value */

523

valuenow?: number;

524

/** Value text */

525

valuetext?: string;

526

/** Child nodes */

527

children?: AccessibilityNode[];

528

}

529

```

530

531

**Usage Examples:**

532

533

```typescript

534

// Get accessibility snapshot

535

test('accessibility test', async ({ page }) => {

536

await page.goto('https://example.com');

537

538

// Full accessibility tree

539

const snapshot = await page.accessibility.snapshot();

540

console.log('Accessibility tree:', JSON.stringify(snapshot, null, 2));

541

542

// Interesting nodes only

543

const interestingNodes = await page.accessibility.snapshot({

544

interestingOnly: true

545

});

546

547

// Check specific element accessibility

548

const button = page.locator('button');

549

const buttonSnapshot = await page.accessibility.snapshot({

550

root: await button.elementHandle()

551

});

552

553

// Verify accessibility properties

554

expect(buttonSnapshot?.role).toBe('button');

555

expect(buttonSnapshot?.name).toBeTruthy();

556

557

// Check for accessibility violations

558

const violations = findA11yViolations(snapshot);

559

expect(violations).toHaveLength(0);

560

});

561

562

// Custom accessibility checker

563

function findA11yViolations(node: AccessibilityNode | null): string[] {

564

const violations: string[] = [];

565

566

if (!node) return violations;

567

568

// Check for missing alt text on images

569

if (node.role === 'img' && !node.name) {

570

violations.push('Image missing alt text');

571

}

572

573

// Check for missing form labels

574

if (node.role === 'textbox' && !node.name) {

575

violations.push('Input missing label');

576

}

577

578

// Check for insufficient color contrast

579

if (node.role === 'button' && !node.name) {

580

violations.push('Button missing accessible name');

581

}

582

583

// Recursively check children

584

if (node.children) {

585

for (const child of node.children) {

586

violations.push(...findA11yViolations(child));

587

}

588

}

589

590

return violations;

591

}

592

```

593

594

### Console & Error Monitoring

595

596

Monitor browser console messages, JavaScript errors, and page events.

597

598

```typescript { .api }

599

interface Page {

600

/** Listen for console messages */

601

on(event: 'console', listener: (message: ConsoleMessage) => void): void;

602

/** Listen for page errors */

603

on(event: 'pageerror', listener: (error: Error) => void): void;

604

/** Listen for dialog events */

605

on(event: 'dialog', listener: (dialog: Dialog) => void): void;

606

/** Listen for download events */

607

on(event: 'download', listener: (download: Download) => void): void;

608

/** Listen for file chooser events */

609

on(event: 'filechooser', listener: (fileChooser: FileChooser) => void): void;

610

}

611

612

interface ConsoleMessage {

613

/** Message type */

614

type(): 'log' | 'debug' | 'info' | 'error' | 'warning' | 'dir' | 'dirxml' | 'table' | 'trace' | 'clear' | 'startGroup' | 'startGroupCollapsed' | 'endGroup' | 'assert' | 'profile' | 'profileEnd' | 'count' | 'timeEnd';

615

/** Message text */

616

text(): string;

617

/** Message arguments */

618

args(): JSHandle[];

619

/** Message location */

620

location(): { url: string; lineNumber: number; columnNumber: number };

621

/** Get page */

622

page(): Page;

623

}

624

625

interface Dialog {

626

/** Dialog message */

627

message(): string;

628

/** Dialog type */

629

type(): 'alert' | 'beforeunload' | 'confirm' | 'prompt';

630

/** Default prompt value */

631

defaultValue(): string;

632

/** Accept dialog */

633

accept(promptText?: string): Promise<void>;

634

/** Dismiss dialog */

635

dismiss(): Promise<void>;

636

/** Get page */

637

page(): Page;

638

}

639

```

640

641

**Usage Examples:**

642

643

```typescript

644

// Monitor console messages

645

const consoleMessages: ConsoleMessage[] = [];

646

page.on('console', message => {

647

consoleMessages.push(message);

648

console.log(`Console ${message.type()}: ${message.text()}`);

649

});

650

651

// Monitor JavaScript errors

652

const pageErrors: Error[] = [];

653

page.on('pageerror', error => {

654

pageErrors.push(error);

655

console.error('Page error:', error.message);

656

});

657

658

// Handle dialogs automatically

659

page.on('dialog', async dialog => {

660

console.log('Dialog:', dialog.type(), dialog.message());

661

662

if (dialog.type() === 'confirm') {

663

await dialog.accept();

664

} else if (dialog.type() === 'prompt') {

665

await dialog.accept('test input');

666

} else {

667

await dialog.dismiss();

668

}

669

});

670

671

// Monitor downloads

672

page.on('download', async download => {

673

console.log('Download started:', download.suggestedFilename());

674

const path = await download.path();

675

console.log('Download completed:', path);

676

});

677

678

// Test error handling

679

test('error monitoring', async ({ page }) => {

680

const errors: Error[] = [];

681

page.on('pageerror', error => errors.push(error));

682

683

await page.goto('https://example.com');

684

await page.click('#trigger-error');

685

686

// Verify no JavaScript errors occurred

687

expect(errors).toHaveLength(0);

688

689

// Or verify specific error was caught

690

const errorMessages = errors.map(e => e.message);

691

expect(errorMessages).not.toContain('Uncaught TypeError');

692

});

693

```

694

695

## Types

696

697

### Clock & Time Manipulation

698

699

Control time and date for testing time-dependent functionality.

700

701

```typescript { .api }

702

interface BrowserContext {

703

/** Get clock interface */

704

clock: Clock;

705

}

706

707

interface Clock {

708

/** Install fake timers */

709

install(options?: ClockInstallOptions): Promise<void>;

710

/** Fast forward time */

711

fastForward(ticksOrTime: number | string): Promise<void>;

712

/** Pause clock */

713

pauseAt(time: number | string | Date): Promise<void>;

714

/** Resume clock */

715

resume(): Promise<void>;

716

/** Set fixed time */

717

setFixedTime(time: number | string | Date): Promise<void>;

718

/** Set system time */

719

setSystemTime(time: number | string | Date): Promise<void>;

720

/** Restore real timers */

721

restore(): Promise<void>;

722

/** Run pending timers */

723

runFor(ticks: number | string): Promise<void>;

724

}

725

726

interface ClockInstallOptions {

727

/** Time to install fake timers at */

728

time?: number | string | Date;

729

/** Timer APIs to fake */

730

toFake?: ('setTimeout' | 'clearTimeout' | 'setInterval' | 'clearInterval' | 'Date' | 'performance')[];

731

}

732

```

733

734

**Usage Examples:**

735

736

```typescript

737

test('time-dependent test', async ({ page, context }) => {

738

// Install fake timers at specific time

739

await context.clock.install({ time: new Date('2024-01-01T00:00:00Z') });

740

741

await page.goto('/dashboard');

742

743

// Fast forward 5 minutes

744

await context.clock.fastForward('05:00');

745

746

// Check time-dependent UI updates

747

await expect(page.locator('.time-display')).toHaveText('00:05:00');

748

749

// Set fixed time

750

await context.clock.setFixedTime('2024-01-01T12:00:00Z');

751

await page.reload();

752

753

await expect(page.locator('.date-display')).toHaveText('Jan 1, 2024');

754

755

// Restore real timers

756

await context.clock.restore();

757

});

758

```

759

760

### Visual Configuration Types

761

762

```typescript { .api }

763

interface ViewportSize {

764

width: number;

765

height: number;

766

}

767

768

interface WebError {

769

/** Error name */

770

name?: string;

771

/** Error message */

772

message?: string;

773

/** Error stack */

774

stack?: string;

775

}

776

777

interface Download {

778

/** Cancel download */

779

cancel(): Promise<void>;

780

/** Delete downloaded file */

781

delete(): Promise<void>;

782

/** Get download failure reason */

783

failure(): string | null;

784

/** Get download page */

785

page(): Page;

786

/** Get download path */

787

path(): Promise<string>;

788

/** Save download as */

789

saveAs(path: string): Promise<void>;

790

/** Get suggested filename */

791

suggestedFilename(): string;

792

/** Get download URL */

793

url(): string;

794

}

795

796

interface FileChooser {

797

/** Get associated element */

798

element(): ElementHandle;

799

/** Check if multiple files allowed */

800

isMultiple(): boolean;

801

/** Get page */

802

page(): Page;

803

/** Set files */

804

setFiles(files: string | string[] | { name: string; mimeType: string; buffer: Buffer } | { name: string; mimeType: string; buffer: Buffer }[], options?: FileChooserSetFilesOptions): Promise<void>;

805

}

806

807

interface FileChooserSetFilesOptions {

808

/** No wait after setting files */

809

noWaitAfter?: boolean;

810

/** Timeout */

811

timeout?: number;

812

}

813

```