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
```