Cypress Visual Regression Testing: A Practical Guide

Cypress Visual Regression Testing: A Practical Guide

Cypress is the most popular JavaScript end-to-end testing framework. Out of the box, it doesn't include visual regression — but adding it is straightforward, and you have several good options depending on your needs.

This guide covers three approaches: the lightweight cypress-image-snapshot plugin, Percy's cloud-based visual testing, and Applitools Eyes.

Why Visual Regression Matters in Cypress

Cypress tests verify behavior: clicks navigate correctly, forms submit, API calls return expected data. They don't catch visual regressions:

  • A button that exists but is hidden behind another element
  • Text that overflows its container after a font change
  • A layout that breaks on specific viewport sizes
  • Colors that change after a CSS refactor

Visual regression fills this gap. Instead of asserting that button.submit exists, you assert that the entire page looks correct compared to a known-good baseline.

Option 1: cypress-image-snapshot (Local, Zero Cost)

cypress-image-snapshot stores snapshots in your repository and diffs them on every run. It's free, works offline, and integrates natively with Cypress.

Installation

npm install --save-dev cypress-image-snapshot

Register the plugin in cypress/plugins/index.js (Cypress <10) or cypress.config.js (Cypress 10+):

// cypress.config.js (Cypress 10+)
const { defineConfig } = require('cypress')
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin')

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      addMatchImageSnapshotPlugin(on, config)
      return config
    }
  }
})

Add the command in cypress/support/commands.js:

import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command'
addMatchImageSnapshotCommand()

Writing Visual Tests

describe('Homepage', () => {
  it('matches visual baseline', () => {
    cy.visit('/')
    cy.matchImageSnapshot('homepage')
  })

  it('hero section looks correct', () => {
    cy.visit('/')
    cy.get('[data-testid="hero"]').matchImageSnapshot('hero-section')
  })
})

First run creates the baseline in cypress/snapshots/. Subsequent runs compare against it.

Configuring Thresholds

Some pixel-level differences are acceptable (anti-aliasing, subpixel rendering). Configure tolerance:

cy.matchImageSnapshot('homepage', {
  failureThreshold: 0.03,       // 3% pixel difference allowed
  failureThresholdType: 'percent',
  customDiffConfig: { threshold: 0.1 }
})

Updating Baselines

When a visual change is intentional, update the snapshots:

cypress run --env updateSnapshots=true

Or for interactive mode:

cypress open --env updateSnapshots=true

CI Considerations

Snapshots must be consistent across machines. Use a Docker image to ensure identical rendering:

# .github/workflows/visual-tests.yml
jobs:
  visual-tests:
    runs-on: ubuntu-latest
    container:
      image: cypress/browsers:latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npx cypress run
      - uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: snapshot-diffs
          path: cypress/snapshots

Commit the baseline snapshots to your repository. If CI snapshots differ from local ones, the fix is usually the Docker image — not the snapshots.

Option 2: Percy + Cypress

Percy is a cloud-based visual testing service that handles baseline management, browser-level rendering, and team review workflows. It's free up to 5,000 snapshots/month.

Installation

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

Import Percy in cypress/support/commands.js:

import '@percy/cypress'

Writing Percy Tests

describe('Pricing page', () => {
  it('renders correctly', () => {
    cy.visit('/pricing')
    cy.percySnapshot('Pricing page')
  })

  it('mobile layout', () => {
    cy.viewport(375, 812)
    cy.visit('/pricing')
    cy.percySnapshot('Pricing page - mobile')
  })
})

Running with Percy

npx percy exec -- cypress run

Percy uploads screenshots to its dashboard where your team reviews and approves changes.

Percy in CI

- name: Run Cypress with Percy
  env:
    PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
  run: npx percy exec -- cypress run

Percy integrates with GitHub, GitLab, and Bitbucket to post PR status checks. Reviewers see a visual diff before merging.

Option 3: Applitools Eyes + Cypress

Applitools uses AI to filter rendering noise and flag only meaningful visual differences. It handles cross-browser testing through the Ultrafast Grid — running visual checks on 80+ browser/OS/device combinations from a single test run.

Installation

npm install --save-dev @applitools/eyes-cypress
npx eyes-setup

Writing Eyes Tests

import '@applitools/eyes-cypress/commands'

describe('Dashboard', () => {
  it('visual check', () => {
    cy.visit('/dashboard')
    cy.eyesOpen({
      appName: 'My App',
      testName: 'Dashboard visual check',
    })
    cy.eyesCheckWindow({
      tag: 'Dashboard',
      target: 'window',
      fully: true
    })
    cy.eyesClose()
  })
})

Cross-Browser Configuration

// applitools.config.js
module.exports = {
  testConcurrency: 5,
  browser: [
    { width: 1280, height: 800, name: 'chrome' },
    { width: 1280, height: 800, name: 'firefox' },
    { width: 1280, height: 800, name: 'safari' },
    { deviceName: 'iPhone 12', screenOrientation: 'portrait' }
  ]
}

A single Cypress run tests all configured browsers simultaneously.

Choosing the Right Approach

Approach Cost Setup Best For
cypress-image-snapshot Free 15 min Small teams, offline, simple comparisons
Percy Free tier + paid 30 min Teams needing review workflows, PR integration
Applitools Paid 45 min Cross-browser coverage, AI noise filtering

For most projects starting out, cypress-image-snapshot is the right choice. Move to Percy or Applitools when you need review workflows, cross-browser coverage, or better handling of dynamic content.

Organizing Visual Tests

Keep visual regression tests separate from functional tests:

cypress/
  e2e/
    functional/     # click, form, navigation tests
    visual/         # snapshot tests
  snapshots/        # committed baselines (for cypress-image-snapshot)

Run functional and visual tests on separate schedules. Functional tests on every commit; visual regression on PR merge or nightly.

Common Pitfalls

Dynamic content: Timestamps, user avatars, ads, and animations cause false positives. Either mask these elements or use a tool like Applitools that handles them automatically.

cy.matchImageSnapshot('homepage', {
  blackout: ['[data-testid="timestamp"]', '.advertisement']
})

Font loading: Tests that run before fonts load produce inconsistent snapshots. Add a wait:

cy.document().its('fonts.ready').then(() => {
  cy.matchImageSnapshot('homepage')
})

Scroll position: Ensure the page is fully scrolled into position before snapping. cy.scrollTo('top') before snapshots.

Viewport size: Always set an explicit viewport. Different viewport sizes produce different snapshots and both might be valid baselines.

Next Steps

Once visual regression is running in CI:

  1. Add snapshots for every major page at both desktop and mobile viewports
  2. Add snapshots for key states: empty states, error states, loading states
  3. Configure Percy/Applitools PR integration so visual changes require explicit approval before merging
  4. Review and update baselines as part of your design review process — not as an afterthought

Read more

Start now free