Cypress does not include visual testing out of the box, but several plugins and services integrate seamlessly. The options range from free, local pixel comparison (cypress-image-diff) to cloud-based AI-powered platforms (Applitools, Percy). This lesson covers the setup and usage of each approach so you can choose the right tool for your team’s budget, infrastructure, and accuracy requirements.
Visual Testing Tools — Setup and Usage
Each tool follows the same pattern: capture a screenshot at a specific point in your test, compare it to a baseline, and report differences.
// ── TOOL 1: cypress-image-diff (free, local, pixel comparison) ──
// Install: npm install --save-dev cypress-image-diff-js
// cypress.config.ts
/*
import { defineConfig } from 'cypress';
import getCompareSnapshotsPlugin from 'cypress-image-diff-js/plugin';
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
getCompareSnapshotsPlugin(on, config);
},
},
});
*/
// cypress/support/e2e.ts
// import 'cypress-image-diff-js';
// Usage in tests:
it('should match the login page visually', () => {
cy.visit('/');
cy.compareSnapshot('login-page', 0.1); // name, threshold (0.1% tolerance)
});
it('should match the product card component', () => {
// Wait for data to load, then snapshot
cy.intercept('GET', '/api/products').as('products');
cy.visit('/shop');
cy.wait('@products');
cy.get('.product-card').first().compareSnapshot('product-card-default', 0.1);
});
// First run: creates baseline in cypress-image-diff-screenshots/baseline/
// Subsequent runs: compares to baseline, saves diff if mismatch
// ── TOOL 2: Percy by BrowserStack (cloud-based) ──
// Install: npm install --save-dev @percy/cli @percy/cypress
// Usage:
/*
it('should match the checkout page', () => {
cy.visit('/checkout');
cy.percySnapshot('Checkout Page'); // Captured and compared in Percy cloud
});
it('should match responsive layouts', () => {
cy.visit('/shop');
cy.percySnapshot('Shop Page', {
widths: [375, 768, 1280], // Test three viewport widths
});
});
*/
// Run: npx percy exec -- npx cypress run
// Review: Percy dashboard at https://percy.io shows diffs with approval UI
// ── TOOL 3: Applitools Eyes (AI-based) ──
// Install: npm install --save-dev @applitools/eyes-cypress
// cypress.config.ts
/*
import { defineConfig } from 'cypress';
import eyesPlugin from '@applitools/eyes-cypress';
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
eyesPlugin(on, config);
},
},
});
*/
// Usage:
/*
it('should match using AI visual comparison', () => {
cy.eyesOpen({ appName: 'ShopApp', testName: 'Product Grid' });
cy.visit('/shop');
cy.eyesCheckWindow({ tag: 'Product Grid', fully: true }); // Full page
cy.eyesClose();
});
*/
// ── Tool comparison ──
const TOOL_COMPARISON = {
'cypress-image-diff': {
cost: 'Free (open source)',
comparison: 'Pixel-by-pixel with configurable threshold',
baselines: 'Local files in your repo',
cross_browser: 'No — screenshots vary by OS/browser rendering',
best_for: 'Small teams, budget-conscious, local development',
},
'Percy (BrowserStack)': {
cost: 'Free tier (5000 snapshots/month), paid plans available',
comparison: 'Pixel comparison with visual diffing dashboard',
baselines: 'Cloud-stored with approval workflow',
cross_browser: 'Yes — renders in multiple browsers in the cloud',
best_for: 'Teams needing responsive testing, PR-based review workflow',
},
'Applitools Eyes': {
cost: 'Free tier (100 checkpoints/month), paid plans available',
comparison: 'AI-powered — understands layout vs content vs style changes',
baselines: 'Cloud-stored with AI-assisted approval',
cross_browser: 'Yes — Ultrafast Grid renders across browsers/viewports',
best_for: 'Teams needing low false-positive rates, large-scale visual testing',
},
};
Object.entries(TOOL_COMPARISON).forEach(([tool, info]) => {
console.log(`\n${tool}:`);
Object.entries(info).forEach(([key, val]) => {
console.log(` ${key}: ${val}`);
});
});
cy.compareSnapshot('name', threshold) in cypress-image-diff takes a screenshot of the current viewport (or a specific element if chained from cy.get()) and compares it to a stored baseline with the same name. The threshold (0.1 = 0.1% pixel tolerance) determines how many pixels can differ before the test fails. On the first run, no baseline exists, so the tool creates one and the test passes. On subsequent runs, it compares against that baseline. Update baselines by deleting the old image and re-running.cy.get('[data-cy="header"]').compareSnapshot('header'). Component-level snapshots are less brittle than full-page snapshots because changes to unrelated parts of the page do not trigger false positives. A footer redesign should not fail the header snapshot. This targeted approach reduces baseline update frequency dramatically.Common Mistakes
Mistake 1 — Generating baselines on a different OS than CI
❌ Wrong: Developer creates baselines on macOS; CI runs on Linux — font rendering differences cause every snapshot to fail.
✅ Correct: Generating baselines inside the same Docker container that CI uses: docker run cypress/included npx cypress run --env updateSnapshots=true. Baselines match CI rendering exactly.
Mistake 2 — Snapshotting pages with dynamic content
❌ Wrong: cy.compareSnapshot('dashboard') on a page showing the current date, random recommendations, and live user counts — fails every run.
✅ Correct: Stubbing all dynamic data with fixtures, replacing dates with fixed values, and hiding live counters before snapshotting.