Overview
Percy is a visual testing and review platform (now part of BrowserStack) that captures screenshots of your UI across different browsers and screen sizes, then compares them against approved baselines to detect visual regressions. It integrates into your existing test suite and CI/CD pipeline, providing a visual diff review workflow similar to code review.
Percy supports Cypress, Playwright, Puppeteer, Selenium, Storybook, static sites, and custom SDK integrations. It renders pages in real browsers (Chrome and Firefox), captures responsive snapshots at configurable widths, and provides a web dashboard for reviewing and approving visual changes with your team.
Installation
# Percy CLI
npm install -D @percy/cli
# Cypress integration
npm install -D @percy/cypress
# Playwright integration
npm install -D @percy/playwright
# Puppeteer integration
npm install -D @percy/puppeteer
# Selenium (Node.js)
npm install -D @percy/selenium-webdriver
# Storybook integration
npm install -D @percy/storybook
# Static site snapshots
npm install -D @percy/cli
Core Commands
| Command | Description |
|---|
percy snapshot | Take snapshots of static pages |
percy storybook | Snapshot Storybook stories |
percy exec | Run a command with Percy enabled |
percy upload | Upload a directory of images |
percy config:validate | Validate Percy configuration |
percy build:wait | Wait for a build to complete |
Basic Usage
With Cypress
// cypress/e2e/visual.cy.js
describe("Visual Tests", () => {
it("should match homepage", () => {
cy.visit("/");
cy.percySnapshot("Homepage");
});
it("should match dashboard", () => {
cy.visit("/dashboard");
cy.percySnapshot("Dashboard", {
widths: [375, 768, 1280],
minHeight: 1024,
});
});
});
# Run Cypress with Percy
export PERCY_TOKEN=your_token_here
npx percy exec -- npx cypress run
With Playwright
const { test } = require("@playwright/test");
const percySnapshot = require("@percy/playwright");
test("homepage visual test", async ({ page }) => {
await page.goto("https://example.com");
await percySnapshot(page, "Homepage");
});
test("responsive visual test", async ({ page }) => {
await page.goto("https://example.com/pricing");
await percySnapshot(page, "Pricing Page", {
widths: [375, 768, 1280],
});
});
npx percy exec -- npx playwright test
With Storybook
# Snapshot all stories
npx percy storybook http://localhost:6006
# Snapshot from a built Storybook
npx percy storybook ./storybook-static
# With filtering
npx percy storybook --include "Button*" http://localhost:6006
npx percy storybook --exclude "*Dark*" http://localhost:6006
Static Site Snapshots
# Snapshot a list of URLs
npx percy snapshot snapshots.yml
# Snapshot a directory of HTML files
npx percy snapshot ./public
# snapshots.yml
- name: Homepage
url: https://example.com/
widths: [375, 768, 1280]
- name: About Page
url: https://example.com/about
waitForSelector: ".content-loaded"
- name: Dashboard
url: https://example.com/dashboard
execute: |
await page.click('.load-data');
await page.waitForSelector('.data-loaded');
Configuration
# .percy.yml
version: 2
snapshot:
widths: [375, 768, 1280]
minHeight: 1024
percyCSS: |
.dynamic-content { visibility: hidden; }
.timestamp { display: none; }
discovery:
allowedHostnames:
- "cdn.example.com"
- "fonts.googleapis.com"
networkIdleTimeout: 500
concurrency: 5
storybook:
include: ["**"]
exclude: ["**/internal/**"]
args: ["--no-open"]
Snapshot Options
| Option | Description |
|---|
widths | Array of viewport widths (default: [375, 1280]) |
minHeight | Minimum screenshot height in pixels |
percyCSS | CSS to inject before snapshot |
enableJavaScript | Enable JavaScript rendering |
waitForSelector | Wait for a CSS selector before snapshot |
waitForTimeout | Wait N milliseconds before snapshot |
scope | CSS selector to scope the snapshot |
enableLayout | Enable layout snapshot mode |
// Cypress example with all options
cy.percySnapshot("Complex Page", {
widths: [375, 768, 1024, 1280, 1920],
minHeight: 1024,
percyCSS: ".ad-banner { display: none; }",
enableJavaScript: true,
scope: "#main-content",
});
Advanced Usage
Handling Dynamic Content
# .percy.yml - Hide dynamic elements
version: 2
snapshot:
percyCSS: |
[data-percy-hide], .ad-slot, .live-chat {
visibility: hidden !important;
}
.dynamic-date {
color: transparent;
}
.animated-element {
animation: none !important;
transition: none !important;
}
// Freeze animations and dynamic content in tests
cy.percySnapshot("Stable Page", {
percyCSS: `
*, *::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
`,
});
Responsive Testing Strategy
// Test critical breakpoints
const breakpoints = {
mobile: 375,
tablet: 768,
desktop: 1280,
wide: 1920,
};
cy.percySnapshot("Responsive Layout", {
widths: Object.values(breakpoints),
});
Upload Directory of Images
# Upload pre-rendered images for comparison
npx percy upload ./screenshots
# With custom naming
npx percy upload --strip-extensions ./screenshots
CI/CD Integration
# GitHub Actions
name: Visual Tests
on: [pull_request]
jobs:
percy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx percy exec -- npx cypress run
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
# Environment variables
export PERCY_TOKEN="your_project_token"
export PERCY_BRANCH="feature-branch" # Auto-detected in CI
export PERCY_TARGET_BRANCH="main" # Baseline branch
export PERCY_PARALLEL_TOTAL=4 # Parallel shards
export PERCY_PARALLEL_NONCE="unique-build-id" # Group parallel builds
Parallel Builds
# Shard 1
PERCY_PARALLEL_TOTAL=3 PERCY_PARALLEL_NONCE=$BUILD_ID \
npx percy exec -- npx cypress run --spec "cypress/e2e/shard1/**"
# Shard 2
PERCY_PARALLEL_TOTAL=3 PERCY_PARALLEL_NONCE=$BUILD_ID \
npx percy exec -- npx cypress run --spec "cypress/e2e/shard2/**"
# Shard 3
PERCY_PARALLEL_TOTAL=3 PERCY_PARALLEL_NONCE=$BUILD_ID \
npx percy exec -- npx cypress run --spec "cypress/e2e/shard3/**"
Troubleshooting
| Issue | Solution |
|---|
| No snapshots captured | Verify PERCY_TOKEN is set; check percy exec wraps test command |
| Dynamic content diffs | Add percyCSS to hide timestamps, ads, animations |
| Missing assets (CSS/fonts) | Add CDN hostnames to discovery.allowedHostnames |
| Snapshot timeout | Increase discovery.networkIdleTimeout; add waitForSelector |
| Build not finalizing | Use percy build:finalize or check parallel nonce settings |
| Too many diffs | Review baseline; approve intentional changes in dashboard |
| Storybook stories missing | Check include/exclude patterns; verify Storybook URL |
| Slow builds | Reduce widths; limit snapshot count; increase discovery.concurrency |