playwright-visual-regression · diff
v1.0.0 to v2.0.0
7 added, 709 removed. Audit A to A.
---
name: playwright-visual-regression
- description: "Visual regression testing using Playwright with toHaveScreenshot(), masking, thresholds, cross-browser testing, and VUDA integration for AI-powered visual analysis"
+ description: Use when setting up visual regression testing with Playwright - toHaveScreenshot baselines, masking dynamic content, handling animations, full-page and element screenshots, cross-browser runs, CI integration, VUDA MCP tooling, or debugging flaky visual diffs
metadata:
author: mte90
- version: "1.0.0"
+ version: 2.0.0
tags:
- playwright
- visual-testing
- regression
- screenshot
- vrt
- automation
- testing
---
# Playwright Visual Regression Testing
Complete guide to visual regression testing using Playwright's built-in `toHaveScreenshot()` API, VUDA MCP integration, and AI-powered visual analysis with vision models.
## Overview
Playwright provides native visual regression testing through the `toHaveScreenshot()` assertion. It captures screenshots, compares them against baselines using pixelmatch, and fails tests when visual differences exceed configured thresholds.
**Key Features:**
- Built-in screenshot comparison (no external dependencies)
- Pixel-by-pixel comparison with configurable thresholds
- Dynamic content masking
- Animation handling
- Cross-browser testing (Chromium, Firefox, WebKit)
- Full-page and element-level screenshots
- Integration with VUDA MCP for AI-powered visual debugging
- Vision model integration for screenshot analysis
## Installation
### Python Installation
```bash
# Install Playwright for Python
pip install playwright
# Install browser drivers
playwright install chromium firefox webkit
# Or install all browsers
playwright install
```
### TypeScript/JavaScript Installation
```bash
# Install Playwright
npm install -D @playwright/test
# Install browsers
npx playwright install chromium firefox webkit
```
## Basic Usage
### Python Example
- Playwright supports Python alongside TypeScript/JavaScript:
-
```python
from playwright.sync_api import sync_playwright, expect
def test_homepage_screenshot():
"""Visual regression test for homepage"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://your-app.com")
# Take screenshot for comparison
expect(page).to_have_screenshot("homepage.png")
browser.close()
def test_element_screenshot():
"""Test specific element instead of full page"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("/pricing")
# Screenshot of specific element
pricing_card = page.locator('[data-testid="pro-plan"]')
expect(pricing_card).to_have_screenshot("pro-plan-card.png")
browser.close()
def test_with_masking():
"""Mask dynamic content before screenshot"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("/dashboard")
# Mask dynamic elements
expect(page).to_have_screenshot("dashboard.png", mask=[
page.locator(".timestamp"),
page.locator(".user-avatar"),
])
browser.close()
```
### Using pytest with Playwright
```python
# conftest.py
import pytest
from playwright.sync_api import sync_playwright, Browser, Page
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
yield p.chromium.launch()
@pytest.fixture
def page(browser: Browser):
page = browser.new_page()
yield page
page.close()
# test_visual.py
def test_login_page(page: Page):
page.goto("/login")
expect(page).to_have_screenshot("login-page.png")
def test_login_with_errors(page: Page):
page.goto("/login")
page.fill("#email", "invalid")
page.click('[data-testid="submit"]')
expect(page).to_have_screenshot("login-error.png")
```
### Pytest Markers for Visual Tests
```python
import pytest
@pytest.mark.visual
def test_visual_regression(page):
"""Run only visual tests with: pytest -m visual"""
page.goto("/")
expect(page).to_have_screenshot("homepage.png")
@pytest.mark.visual
@pytest.mark.parametrize("viewport", [
{"width": 1280, "height": 720}, # Desktop
{"width": 375, "height": 667}, # Mobile
])
def test_responsive_visual(page, viewport):
"""Test different viewports"""
page.set_viewport_size(viewport)
page.goto("/")
name = f"homepage-{viewport['width']}x{viewport['height']}.png"
expect(page).to_have_screenshot(name)
```
### TypeScript/JavaScript Example
- ### Simple Screenshot Test
-
```typescript
import { test, expect } from '@playwright/test';
test('homepage matches baseline', async ({ page }) => {
await page.goto('https://your-app.com');
await expect(page).toHaveScreenshot();
});
```
### First Run Behavior
The first run creates baseline images. Run with `--update-snapshots` to generate baselines:
```bash
npx playwright test --update-snapshots
```
Baselines are stored in `__snapshots__` directories next to test files.
### Named Screenshots
```typescript
test('login form states', async ({ page }) => {
await page.goto('/login');
// Empty state
await expect(page).toHaveScreenshot('login-empty.png');
// Validation error
await page.fill('#email', 'invalid');
await page.click('[data-testid="submit"]');
await expect(page).toHaveScreenshot('login-validation-error.png');
});
```
## Configuration
### Playwright Config
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: 2,
// Cross-browser projects
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile viewports
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
],
// Visual regression settings
expect: {
toHaveScreenshot: {
- // Maximum time to wait for screenshot
timeout: 5000,
- // Animated elements
animations: 'disabled',
},
},
});
```
### Threshold Options
```typescript
// Allow minor pixel differences
await expect(page).toHaveScreenshot('page.png', {
maxDiffPixels: 100, // Maximum pixels that can differ
maxDiffPixelRatio: 0.01, // Maximum 1% pixel difference
threshold: 0.3, // Per-pixel color sensitivity (0-1)
});
```
- ## Masking Dynamic Content
-
- Real applications have dynamic elements (timestamps, avatars, ads). Mask them to avoid false positives.
-
- ### Basic Masking
-
- ```typescript
- await expect(page).toHaveScreenshot('dashboard.png', {
- mask: [
- page.locator('[data-testid="user-avatar"]'),
- page.locator('[data-testid="timestamp"]'),
- page.locator('.live-feed'),
- page.locator('.ad-banner'),
- ],
- maskColor: '#000000', // Custom mask color
- });
- ```
-
- ### Reusable Mask Helper
-
- ```typescript
- // helpers/visual.ts
- import { Page } from '@playwright/test';
-
- export async function maskDynamicContent(page: Page, selectors: string[]) {
- return selectors.map(selector => page.locator(selector));
- }
-
- // Usage
- test('dashboard with masking', async ({ page }) => {
- await page.goto('/dashboard');
-
- const dynamicElements = await maskDynamicContent(page, [
- '.timestamp',
- '.user-avatar',
- '.notification-badge',
- ]);
-
- await expect(page).toHaveScreenshot('dashboard.png', {
- mask: dynamicElements,
- });
- });
- ```
-
- ## Handling Animations
-
- Animations cause flaky tests. Disable them before capturing.
-
- ### CSS-Based Animation Disable
-
- ```typescript
- async function disableAnimations(page: Page) {
- await page.addStyleTag({
- content: `
- *, *::before, *::after {
- animation-duration: 0s !important;
- animation-delay: 0s !important;
- transition-duration: 0s !important;
- transition-delay: 0s !important;
- scroll-behavior: auto !important;
- }
- `,
- });
- }
-
- test('checkout without animations', async ({ page }) => {
- await page.goto('/checkout');
- await disableAnimations(page);
- await expect(page).toHaveScreenshot('checkout.png');
- });
- ```
-
- ### Built-in Animation Option
-
- ```typescript
- await expect(page).toHaveScreenshot('page.png', {
- animations: 'disabled', // Playwright handles this automatically
- });
- ```
-
- ## Full-Page Screenshots
-
- ### Full-Page Capture
-
- ```typescript
- test('full page screenshot', async ({ page }) => {
- await page.goto('/pricing');
-
- await expect(page).toHaveScreenshot('pricing-full.png', {
- fullPage: true,
- });
- });
- ```
-
- ### Handling Lazy-Loaded Content
-
- ```typescript
- test('blog with lazy loading', async ({ page }) => {
- await page.goto('/blog');
-
- // Scroll to trigger lazy loading
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
- await page.waitForTimeout(1000); // Wait for images
- await page.evaluate(() => window.scrollTo(0, 0)); // Scroll back
-
- await expect(page).toHaveScreenshot('blog-full.png', {
- fullPage: true,
- });
- });
- ```
-
- ## Element-Level Testing
-
- Test specific components instead of full pages for more precise results.
-
- ```typescript
- test('navigation component', async ({ page }) => {
- await page.goto('/');
-
- const navbar = page.locator('nav[data-testid="main-nav"]');
- await expect(navbar).toHaveScreenshot('navbar.png');
- });
-
- test('pricing card', async ({ page }) => {
- await page.goto('/pricing');
-
- const card = page.locator('[data-testid="pro-plan"]');
- await expect(card).toHaveScreenshot('pro-plan-card.png', {
- maxDiffPixelRatio: 0.005, // Tighter threshold for components
- });
- });
- ```
-
- ## Cross-Browser Considerations
-
- Different browsers render differently. Each project gets its own baseline:
-
- ```
- tests/
- homepage.spec.ts-snapshots/
- homepage-chromium-linux.png
- homepage-firefox-linux.png
- homepage-webkit-linux.png
- ```
-
- **Important:** Generate baselines in the same environment as CI to avoid OS-level rendering differences.
-
- ```yaml
- # CI using Playwright Docker image
- - uses: docker://mcr.microsoft.com/playwright:v1.50.0-noble
- ```
-
- ## CI/CD Integration
-
- ### GitHub Actions
-
- ```yaml
- name: Visual Regression Tests
- on: [pull_request]
-
- jobs:
- visual-tests:
- runs-on: ubuntu-latest
- container: mcr.microsoft.com/playwright:v1.50.0-noble
- steps:
- - uses: actions/checkout@v4
- - run: npm ci
- - name: Run visual tests
- run: npx playwright test --grep @visual
- - uses: actions/upload-artifact@v4
- if: failure()
- with:
- name: visual-diff-report
- path: playwright-report/
- ```
-
- ### Updating Baselines
-
- ```bash
- # Update snapshots for specific test
- npx playwright test --update-snapshots --grep "homepage"
-
- # Update all snapshots
- npx playwright test --update-snapshots
- ```
-
- ## Debugging Visual Failures
-
- When tests fail, Playwright generates three images in `test-results/`:
-
- 1. **Expected** - the baseline image
- 2. **Actual** - current screenshot
- 3. **Diff** - highlighted differences (red pixels)
-
- ```bash
- # View HTML report with visual diffs
- npx playwright show-report
- ```
-
- ### Common Issues
-
- | Issue | Cause | Solution |
- |-------|-------|----------|
- | Entire screenshot different | Different OS/browser version | Use consistent CI environment |
- | Text differences | Font rendering variance | Use Playwright Docker image |
- | Scattered pixels | Anti-aliasing | Increase `maxDiffPixels` |
- | Specific component changed | Real regression | Investigate CSS change |
-
- ## VUDA Integration (Optional)
-
- VUDA (Visual UI Debug Agent) is an optional MCP server that provides AI-powered visual testing capabilities. It's **not included** in this skill - you need to install and configure it separately.
-
- VUDA goes beyond Playwright's built-in VRT by providing:
- - AI-powered visual analysis without writing tests
- - Automatic visual difference detection
- - DOM inspection with styles
- - User workflow validation
- - Performance metrics
- - Console error monitoring
-
- ### VUDA Installation
-
- ```bash
- # Install globally
- npm install -g visual-ui-debug-agent-mcp
-
- # Or run with npx
- npx visual-ui-debug-agent-mcp
- ```
-
- ### Configure VUDA MCP
-
- Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
-
- ```json
- {
- "mcpServers": {
- "vuda": {
- "command": "npx",
- "args": ["-y", "visual-ui-debug-agent-mcp"]
- }
- }
- }
- ```
-
- VUDA provides these tools for visual testing:
-
- ### Available VUDA Tools
-
- | Tool | Description | Use Case |
- |------|-------------|----------|
- | `screenshot_url` | Capture screenshots of any URL | Quick visual capture without writing tests |
- | `enhanced_page_analyzer` | Comprehensive page analysis with screenshots | Full diagnostic with console + elements |
- | `visual_comparison` | Compare two URLs/pages and highlight differences | Before/after visual regression |
- | `dom_inspector` | Inspect DOM elements with computed styles | Debug styling issues |
- | `ui_workflow_validator` | Test user journeys with validation | Automated E2E testing |
- | `performance_analysis` | Measure Core Web Vitals | Performance regression testing |
- | `console_monitor` | Capture console errors/warnings | JavaScript error detection |
- | `navigation_flow_validator` | Test sequences of user actions | Complex user flows |
- | `batch_screenshot_urls` | Capture multiple URLs in grid | Overview/comparison |
- | `visual_comparison` | Pixel-diff between two URLs | Automated visual diff |
-
- ### 1. screenshot_url - Quick Screenshots
-
- Capture a screenshot without writing any test code:
-
- ```
- screenshot_url(
- url: "https://example.com",
- fullPage: false,
- selector: null, // Optional: capture specific element
- waitTime: 5000
- )
- ```
-
- **Example workflow:**
- 1. Run `screenshot_url` to capture current state
- 2. Make changes to your app
- 3. Run `screenshot_url` again
- 4. Compare visually
-
- ### 2. enhanced_page_analyzer - Full Diagnostic
-
- Comprehensive page analysis combining multiple data sources:
-
- ```
- enhanced_page_analyzer(
- url: "https://example.com",
- includeConsole: true, // Capture console logs
- mapElements: true, // Map interactive elements
- fullPage: false,
- waitForSelector: null,
- device: null
- )
- ```
-
- **Returns:**
- - Screenshot
- - Console logs (errors, warnings, info)
- - List of interactive elements
- - Page metadata
-
- **Use case:** Debug why a page looks broken - get screenshot + console + elements in one call.
-
- ### 3. visual_comparison - Automated Visual Diff
-
- Compare two URLs and automatically highlight differences:
-
- ```
- visual_comparison(
- url1: "https://example.com",
- url2: "https://staging.example.com",
- threshold: 0.1, // Sensitivity (0.0-1.0)
- fullPage: false,
- selector: null
- )
- ```
-
- **Returns:**
- - Side-by-side screenshot
- - Highlighted diff image
- - Percentage of difference
-
- **Use case:** Compare production vs staging, before/after deployments.
-
- ### 4. dom_inspector - Style Debugging
-
- Inspect specific DOM elements with computed styles:
-
- ```
- dom_inspector(
- url: "https://example.com",
- selector: "#login-button",
- includeChildren: false,
- includeStyles: true
- )
- ```
-
- **Returns:**
- - Element HTML
- - Computed CSS styles
- - Computed values (colors, sizes, positions)
-
- **Use case:** Debug why a button looks different - get exact CSS values.
-
- ### 5. ui_workflow_validator - Automated E2E
-
- Test complete user journeys with validation:
-
- ```
- ui_workflow_validator(
- startUrl: "https://example.com",
- taskDescription: "User login flow",
- steps: [
- {
- action: "fill",
- selector: "#email",
- value: "test@example.com"
- },
- {
- action: "fill",
- selector: "#password",
- value: "password123"
- },
- {
- action: "click",
- selector: "[data-testid='login-btn']"
- },
- {
- action: "verifyUrl",
- url: "/dashboard"
- },
- {
- action: "verifyText",
- selector: "h1",
- value: "Dashboard"
- }
- ],
- captureScreenshots: "all"
- )
- ```
-
- **Returns:**
- - Screenshots per step
- - Pass/fail status per step
- - Error details if failed
-
- **Use case:** Automate complex flows without writing Playwright code.
-
- ### 6. performance_analysis - Core Web Vitals
-
- Measure page performance metrics:
-
- ```
- performance_analysis(
- url: "https://example.com",
- iterations: 3,
- waitForNetworkIdle: true,
- device: null
- )
- ```
-
- **Returns:**
- - LCP (Largest Contentful Paint)
- - FID (First Input Delay)
- - CLS (Cumulative Layout Shift)
- - FCP (First Contentful Paint)
- - TTFB (Time to First Byte)
-
- **Use case:** Catch performance regressions before deployment.
-
- ### 7. console_monitor - JavaScript Error Detection
-
- Monitor console output for a page:
-
- ```
- console_monitor(
- url: "https://example.com",
- filterTypes: ["error", "warning"],
- duration: 5000,
- interactionSelector: null
- )
- ```
-
- **Returns:**
- - All console messages
- - Error stack traces
- - Warning details
-
- **Use case:** Detect JavaScript errors during page load.
-
- ### 8. navigation_flow_validator - Multi-Page Flows
-
- Test sequences across multiple pages:
-
- ```
- navigation_flow_validator(
- startUrl: "https://example.com",
- steps: [
- { action: "navigate", url: "/products" },
- { action: "click", selector: ".product:first-child" },
- { action: "click", selector: "[data-testid='add-to-cart']" },
- { action: "navigate", url: "/cart" },
- { action: "screenshot", selector: null }
- ],
- captureScreenshots: true
- )
- ```
-
- ### Complete VUDA Workflow Example
-
- **Scenario:** You deployed a new version and want to verify the homepage looks correct.
-
- ```python
- # Step 1: Capture baseline screenshot
- vuda.screenshot_url(
- url="https://production.example.com",
- fullPage=True
- )
-
- # Step 2: Analyze with console to check for JS errors
- vuda.enhanced_page_analyzer(
- url="https://production.example.com",
- includeConsole=True,
- mapElements=True
- )
-
- # Step 3: Compare with staging
- vuda.visual_comparison(
- url1="https://production.example.com",
- url2="https://staging.example.com",
- threshold=0.05
- )
-
- # Step 4: If issues found, inspect specific element
- vuda.dom_inspector(
- url="https://staging.example.com",
- selector=".hero-title",
- includeStyles=True
- )
- ```
-
- ### VUDA + Playwright Combined
-
- Use VUDA for quick analysis, Playwright for CI/CD:
-
- ```python
- # Quick debug with VUDA
- vuda.enhanced_page_analyzer(
- url="http://localhost:3000",
- includeConsole=True
- )
-
- # Automated regression with Playwright
- def test_homepage_visual_regression(page):
- page.goto("http://localhost:3000")
- expect(page).to_have_screenshot("homepage.png")
- ```
-
- ### Best Practices with VUDA
-
- 1. **Use for debugging** - Quick visual checks without writing tests
- 2. **Use for exploration** - Discover issues on unknown pages
- 3. **Use for comparison** - Before/after, staging/prod
- 4. **Use Playwright for CI** - Automated, reproducible tests
-
- ### Troubleshooting VUDA
-
- | Issue | Solution |
- |-------|----------|
- | No screenshots | Check browser can launch, no headless restrictions |
- | Timeout errors | Increase `waitTime` for slow pages |
- | Missing elements | Page may use client-side rendering, wait longer |
- | Console not captured | Some errors only appear on interaction |
-
- ### VUDA vs Playwright Built-in
-
- | Feature | VUDA | Playwright toHaveScreenshot |
- |---------|------|---------------------------|
- | Setup required | Yes (MCP) | No (built-in) |
- | Test automation | No (manual) | Yes (CI/CD) |
- | AI analysis | Yes | No |
- | Visual diff | Manual comparison | Automatic |
- | Console capture | Yes | No |
- | Performance metrics | Yes | No |
- | Best for | Debugging, exploration | Automated regression |
-
- ## Vision Model Integration (look_at)
-
- Use `look_at` tool with vision-capable models to analyze screenshots after capturing them.
-
- **Important:** `look_at` requires a **direct file path** to the screenshot file (e.g., `/tmp/dashboard.png`). It does not work with virtual files or URLs - you must save the screenshot to disk first.
-
- ### Analyzing Screenshots with AI
-
- ```typescript
- import { test, expect } from '@playwright/test';
- import { readFileSync } from 'fs';
-
- test('analyze screenshot with vision model', async ({ page }) => {
- await page.goto('/dashboard');
-
- // IMPORTANT: Save screenshot to a real file path
- // look_at needs a real filesystem path, not virtual/URL
- await page.screenshot({
- path: '/tmp/dashboard.png', // Direct file path required!
- fullPage: true
- });
-
- // Use look_at tool with the file path:
- // look_at(
- // file_path: '/tmp/dashboard.png',
- // goal: 'Analyze this dashboard screenshot for layout issues, missing elements, color problems'
- // )
-
- // The vision model will identify:
- // - Layout issues
- // - Color consistency problems
- // - Missing elements
- // - Visual anomalies
- });
- ```
-
- ### look_at Requirements
-
- ```typescript
- // ❌ WRONG - look_at does NOT work with:
- // - URLs
- // - Virtual files
- // - Base64 strings
- // - File descriptors
-
- // ✅ CORRECT - look_at needs:
- look_at(
- file_path: '/absolute/path/to/screenshot.png', // Must be real file on disk
- goal: 'What to analyze in the screenshot'
- )
-
- // Recommended: Save to /tmp for temporary screenshots
- await page.screenshot({ path: '/tmp/my-screenshot.png' });
- look_at(file_path: '/tmp/my-screenshot.png', goal: 'Find any visual bugs');
- ```
-
- ### Complete Workflow Example
-
- ```typescript
- test('capture and analyze screenshot', async ({ page }) => {
- await page.goto('/checkout');
-
- // Step 1: Capture screenshot to real file path
- const screenshotPath = '/tmp/checkout-page.png';
- await page.screenshot({
- path: screenshotPath,
- fullPage: true
- });
-
- // Step 2: Use look_at for AI analysis
- // In your prompt, call look_at with:
- // {
- // file_path: '/tmp/checkout-page.png',
- // goal: 'Identify any visual issues: layout shifts, color mismatches, missing text, overlapping elements'
- // }
-
- // The model analyzes the image and returns:
- // - List of detected visual issues
- // - Screenshots of problematic areas
- // - Specific recommendations
- });
- ```
+ ## Deep Dives
- ### Best Practices for look_at
+ For detailed guidance on specific topics, load these reference files on demand:
- 1. **Always use absolute paths** - `/tmp/screenshot.png` not `screenshot.png`
- 2. **Save to /tmp** - For temporary test screenshots
- 3. **Use descriptive goals** - "Find UI bugs" is better than "Analyze this"
- 4. **Check file exists** - Ensure screenshot was saved before calling look_at
+ - **Screenshot Techniques** (`references/screenshot-techniques.md`) - Masking dynamic content, handling animations, full-page screenshots, element-level testing, cross-browser considerations
+ - **VUDA AI Tools** (`references/vuda-ai.md`) - VUDA MCP integration, vision model `look_at` usage
+ - **CI & Debugging** (`references/ci-debugging.md`) - CI/CD integration, debugging visual failures, advanced patterns, troubleshooting flaky tests
## Best Practices
### 1. Test Organization
```typescript
// Tag visual tests for selective execution
test('homepage visual @visual', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
// Run only visual tests
npx playwright test --grep @visual
```
### 2. Component Over Full Page
```typescript
- // Prefer component screenshots
// ✅ Good: Targeted, fast, precise
await expect(page.locator('.product-card')).toHaveScreenshot('product-card.png');
// ⚠️ Full page: Slower, more noise, harder to diagnose failures
await expect(page).toHaveScreenshot('page.png');
```
### 3. Consistent Environments
```bash
# Always use Docker for consistent rendering
docker run --rm -v $(pwd):/app mcr.microsoft.com/playwright:v1.50.0-noble
```
### 4. Threshold Guidelines
| Type | Recommended Threshold |
|------|----------------------|
| Simple components | `maxDiffPixelRatio: 0.001` (0.1%) |
| Text-heavy pages | `maxDiffPixelRatio: 0.01` (1%) |
| Full-page with fonts | `maxDiffPixelRatio: 0.02` (2%) |
### 5. Handle Dynamic Content
```typescript
// Always mask:
test('feed with masking', async ({ page }) => {
await page.goto('/feed');
await expect(page).toHaveScreenshot('feed.png', {
mask: [
page.locator('.timestamp'),
page.locator('.user-avatar'),
page.locator('.ad-slot'),
page.locator('[data-testid="like-count"]'),
],
});
});
```
- ## Advanced Patterns
-
- ### Data-Driven Visual Testing
-
- ```typescript
- import { test, expect } from '@playwright/test';
- import testData from './visual-scenarios.json';
-
- testData.forEach(({ name, url, maskSelectors, threshold }) => {
- test(`visual: ${name}`, async ({ page }) => {
- await page.goto(url);
-
- const mask = maskSelectors?.map(sel => page.locator(sel));
-
- await expect(page).toHaveScreenshot(`${name}.png`, {
- mask,
- maxDiffPixelRatio: threshold || 0.01,
- });
- });
- });
- ```
-
- ### Page Object Model
-
- ```typescript
- // page-objects/VisualPage.ts
- import { type Page, expect } from '@playwright/test';
-
- export class VisualPage {
- constructor(private page: Page) {}
-
- async assertMatches(name: string, options = {}) {
- await expect(this.page).toHaveScreenshot(name, options);
- }
-
- async assertElementMatches(selector: string, name: string, options = {}) {
- const element = this.page.locator(selector);
- await expect(element).toHaveScreenshot(name, options);
- }
- }
-
- // Usage
- test('dashboard visual', async ({ page }) => {
- const visualPage = new VisualPage(page);
- await page.goto('/dashboard');
- await visualPage.assertMatches('dashboard.png', { mask: [...] });
- });
- ```
-
- ## Troubleshooting
-
- ### Flaky Tests
-
- **Causes:**
- 1. Animations not disabled
- 2. Dynamic content not masked
- 3. Network not idle before capture
- 4. Viewport inconsistencies
-
- **Solutions:**
-
- ```typescript
- test('stable screenshot', async ({ page }) => {
- await page.goto('/');
-
- // Disable animations
- await page.addStyleTag({ content: '*, *::before, *::after { animation: none !important; }' });
-
- // Wait for network idle
- await page.waitForLoadState('networkidle');
-
- // Disable animations in screenshot options
- await expect(page).toHaveScreenshot('stable.png', {
- animations: 'disabled',
- mask: [page.locator('.dynamic')],
- });
- });
- ```
-
- ### Baseline Mismatch
-
- 1. **Different OS** - Use Playwright Docker image in CI
- 2. **Different browser version** - Pin Playwright version
- 3. **Font rendering** - Bundle fonts or use web fonts
-
## Test Pyramid for Visual Testing
```
/\
/ \ E2E Visual Tests (5-10%)
/----\ - Critical user flows
/ \ - Full-page regression
/--------\ Component Tests (20-30%)
/ \ - Individual components
/------------\- UI component variants
/ \ Element Tests (60-70%)
/________________\ - Smallest UI units
- Buttons, inputs, cards
```
## Resources
- [Playwright Visual Regression Docs](https://playwright.dev/docs/test-snapshots)
- [VUDA MCP GitHub](https://github.com/samihalawa/visual-ui-debug-agent-mcp)
- [BrowserStack Visual Testing Guide](https://www.browserstack.com/guide/visual-regression-testing-using-playwright)
- [CSS-Tricks Visual Testing Guide](https://css-tricks.com/automated-visual-regression-testing-with-playwright/)
---
**When to Use What:**
| Need | Solution |
|------|----------|
| Basic VRT | `toHaveScreenshot()` - built into Playwright |
| AI-powered analysis | VUDA MCP tools |
- | Vision model analysis | `look_at` tool with screenshots |
| Vision model analysis | `look_at` tool with screenshots |
| Cross-browser at scale | Run against multiple browsers in CI |
| Component testing | Element-level screenshots |