Percy Visual Testing with Cypress and Playwright: Complete Tutorial

Percy Visual Testing with Cypress and Playwright: Complete Tutorial

Percy is a visual testing platform from BrowserStack that integrates directly with Cypress and Playwright. You add a single command to your tests; Percy captures screenshots, compares them against approved baselines, and flags visual differences in a pull request review workflow.

This tutorial covers setup for both Cypress and Playwright, baseline management, the Percy review workflow, and GitHub Actions integration.

How Percy Works

Percy operates as a cloud service. Your test runner captures DOM snapshots (not raw screenshots) and sends them to Percy's servers. Percy renders them server-side across multiple browsers and screen sizes, then compares the renders against the approved baseline.

The key insight: Percy doesn't just diff pixel images — it renders the DOM fresh each time. This makes it more reliable across browsers and less sensitive to anti-aliasing differences.

When diffs are found, Percy creates a visual review dashboard where your team approves or rejects changes. Approved changes become the new baseline.

Setup with Cypress

Install

npm install --save-dev @percy/cli @percy/cypress

Configure Percy token

Get your project token from the Percy dashboard and add it to your environment:

export PERCY_TOKEN=your_percy_token_here

For CI, add it as a secret. Never commit the token.

Add Percy to your Cypress support file

// cypress/support/e2e.js
import '@percy/cypress';

Capture snapshots in tests

// cypress/e2e/homepage.cy.js
describe('Homepage Visual Tests', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('renders the hero section correctly', () => {
    cy.get('.hero').should('be.visible');
    cy.percySnapshot('Homepage - Hero Section');
  });

  it('renders the product grid', () => {
    cy.get('.product-grid').should('be.visible');
    cy.percySnapshot('Homepage - Product Grid', {
      widths: [375, 768, 1280]  // capture at multiple widths
    });
  });

  it('renders the navigation menu open state', () => {
    cy.get('.hamburger-btn').click();
    cy.get('.nav-menu').should('be.visible');
    cy.percySnapshot('Navigation - Mobile Menu Open');
  });
});

Run with Percy

# Run Cypress with Percy
npx percy <span class="hljs-built_in">exec -- cypress run

<span class="hljs-comment"># Run specific spec
npx percy <span class="hljs-built_in">exec -- cypress run --spec <span class="hljs-string">"cypress/e2e/homepage.cy.js"

Percy only captures snapshots when you run through percy exec. Running cypress run directly bypasses Percy entirely.

Setup with Playwright

Install

npm install --save-dev @percy/cli @percy/playwright

Configure in your test

// tests/visual/homepage.spec.js
import { test, expect } from '@playwright/test';
import percySnapshot from '@percy/playwright';

test.describe('Homepage Visual Tests', () => {
  test('hero section renders correctly', async ({ page }) => {
    await page.goto('/');
    await page.waitForSelector('.hero');
    await percySnapshot(page, 'Homepage - Hero Section');
  });

  test('product grid renders at multiple viewports', async ({ page }) => {
    await page.goto('/products');
    await page.waitForSelector('.product-grid');

    await percySnapshot(page, 'Products - Desktop', {
      widths: [1280]
    });

    await page.setViewportSize({ width: 375, height: 812 });
    await percySnapshot(page, 'Products - Mobile');
  });

  test('checkout form renders correctly', async ({ page }) => {
    await page.goto('/checkout');
    await page.waitForLoadState('networkidle');
    await percySnapshot(page, 'Checkout Form');
  });
});

Run with Percy

npx percy exec -- playwright <span class="hljs-built_in">test tests/visual/

Snapshot Naming Strategy

Snapshot names become your visual changelog. Name them precisely:

// Bad — hard to trace in the review dashboard
cy.percySnapshot('Test 1');
cy.percySnapshot('homepage');

// Good — describes what's being captured and why
cy.percySnapshot('Checkout - Step 2: Shipping Address (empty state)');
cy.percySnapshot('Checkout - Step 2: Shipping Address (validation errors)');
cy.percySnapshot('Dashboard - Pro plan: weekly chart');
cy.percySnapshot('Dashboard - Free plan: upgrade prompt');

Include the component, state, and any relevant variant in the name. You'll thank yourself when reviewing 50 snapshots in a PR.

Baseline Management

The first time you run Percy on a new snapshot name, it creates a baseline without requiring approval. All subsequent runs are compared to that baseline.

Approving changes

When a visual diff appears:

  1. Percy creates a review build on its dashboard
  2. Team members review each diff and click "Approve" or "Request changes"
  3. Once all snapshots are approved, the Percy CI check passes
  4. The approved screenshots become the new baseline

Auto-approve non-visual changes

Configure Percy to ignore specific CSS properties that change legitimately:

# .percy.yml
version: 2
snapshot:
  percyCSS: |
    * { animation-duration: 0s !important; transition-duration: 0s !important; }
    .dynamic-timestamp { visibility: hidden; }
    .user-avatar { filter: blur(5px); }

Branch baselines

Percy uses your git branch to determine which baseline to compare against. Feature branches compare against the main branch baseline. This means visual changes in a PR are clearly scoped to that PR's changes.

Handling Dynamic Content

Dynamic content (dates, user names, ads, animations) causes false positives. Handle it before capturing:

// Freeze animations
await page.addStyleTag({
  content: `
    *, *::before, *::after {
      animation-duration: 0s !important;
      animation-delay: 0s !important;
      transition-duration: 0s !important;
    }
  `
});

// Replace dynamic content
await page.evaluate(() => {
  // Mask timestamps
  document.querySelectorAll('.timestamp').forEach(el => {
    el.textContent = '2026-01-01 00:00:00';
  });
  // Mask user-specific data
  document.querySelectorAll('.user-email').forEach(el => {
    el.textContent = 'user@example.com';
  });
});

await percySnapshot(page, 'Dashboard - Stable State');

Percy also provides a percyCSS option to inject CSS that hides or stabilises elements before capture.

GitHub Actions Integration

name: Visual Tests

on: [push, pull_request]

jobs:
  visual-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install chromium

      - name: Run Percy visual tests
        run: npx percy exec -- playwright test tests/visual/
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

      # Percy automatically posts status to GitHub PRs
      # when PERCY_TOKEN is configured

Percy integrates with GitHub's commit status API — a PR that has unapproved visual diffs will show a failing Percy check, blocking merge until the changes are reviewed.

Configuring Percy

# .percy.yml
version: 2

# Browsers to render in
browsers:
  - chrome
  - firefox

# Default snapshot widths
snapshot:
  widths: [375, 1280]
  minHeight: 600

# Ignore specific elements
snapshot:
  percyCSS: |
    iframe { display: none !important; }
    .ad-container { visibility: hidden; }

Percy Limits and Pricing

Percy's free tier includes 5,000 screenshots per month. Each snapshot at each width counts as one screenshot.

To optimise usage:

  • Only run Percy snapshots on your visual test suite, not in every test
  • Use --include patterns to run visual tests separately from functional tests
  • Capture only the components that matter, not full-page screenshots of every page

What Percy Catches (and Doesn't)

Percy catches:

  • Layout shifts (elements moving unexpectedly)
  • Color changes
  • Font changes or missing web fonts
  • Image failures
  • CSS regression from global style changes
  • Responsive layout breakage

Percy doesn't catch:

  • Functional bugs (wrong data, broken interactions)
  • Performance regressions
  • Accessibility issues
  • SEO changes

Percy is a visual layer on top of your functional test suite, not a replacement for it.

Next Steps

With Percy running:

  • Add visual tests to your PR template checklist: "Percy review approved"
  • Configure percy CLI for storybook integration: percy storybook ./storybook-static
  • Use Percy's API to programmatically approve snapshots in your CD pipeline for specific low-risk changes

Read more

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Atlantis automates Terraform plan and apply through pull requests. But Atlantis itself needs testing: workflow configuration, plan output validation, policy enforcement, and server health checks. This guide covers testing Atlantis workflows locally with atlantis-local, validating plan outputs with custom scripts, enforcing Terraform policies with OPA and Conftest, and monitoring Atlantis

By HelpMeTest