Percy: Integration, Snapshots & Cloud Rendering
Percy (now part of BrowserStack) is a cloud-hosted visual testing service that integrates INTO an existing test suite (Cypress, Playwright, Selenium) rather than replacing it -- functional tests verify behavior, Percy catches the purely-visual regressions those tests structurally can't detect.
Adding Percy to an Existing Suite
// cypress/support/e2e.js
import '@percy/cypress';
// cypress/e2e/dashboard.cy.js -- existing functional test, plus
// visual checks layered in at meaningful UI states
describe('Dashboard', () => {
it('renders correctly for a logged-in user', () => {
cy.login('user@example.com');
cy.visit('/dashboard');
cy.contains('Welcome back').should('be.visible'); // functional assertion
// Visual check -- catches a CSS regression a functional
// assertion structurally can't (button still "exists", but
// might be invisible/overlapping due to a broken style)
cy.percySnapshot('Dashboard - logged in');
});
it('shows the error state on a failed form submit', () => {
cy.visit('/settings');
cy.get('form').submit();
cy.percySnapshot('Settings - validation errors', {
widths: [375, 1280], // one call, multiple viewport comparisons
});
});
});Running with the Percy CLI/Agent
# Wraps the actual test command -- starts a local agent that
# intercepts percySnapshot() calls and uploads the captured data
PERCY_TOKEN=xxx percy exec -- cypress run
# Percy is NOT literally taking a local screenshot -- the SDK
# captures the page's DOM structure + CSS at snapshot time, then
# uploads that to Percy's cloud, which renders it through whichever
# target browsers/widths are configured. This DOM-capture approach
# is exactly what enables ONE snapshot call to produce MULTIPLE
# cloud-rendered comparisons (cross-browser, cross-viewport).Handling Non-Deterministic Content
// Percy captures whatever's in the DOM at snapshot time -- like
// any visual testing tool, eliminating non-determinism is the
// TEST's responsibility, not something Percy auto-detects/ignores.
// BAD: live timestamp, changes every run
cy.visit('/orders');
cy.percySnapshot('Orders list'); // false positive every time
// GOOD: mock data, deterministic snapshot
cy.intercept('GET', '/api/orders', { fixture: 'orders.json' });
cy.visit('/orders');
cy.percySnapshot('Orders list');
// For content outside the team's control (a third-party ad
// embed), MASK the region instead of trying to mock it
cy.percySnapshot('Homepage', {
percyCSS: '.ad-slot { visibility: hidden; }',
});Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free