Visual Regression Testing: Detectando Mudanças Visuais

Testes Automatizados · 6 de julho de 2026

📖 6 min de leitura

O Que É Visual Regression Testing?

Visual regression testing captura screenshots de interfaces e os compara com baselines para detectar mudanças visuais não intencionais (regressões).

Por Que Importa?

  • Detectar mudanças em CSS/HTML
    1. Prevenir “pixel perfect” regressions
    2. Garantir consistência cross-browser
    3. Automatizar verificação de design

Abordagens

1. Pixel-to-Pixel Comparison

# Simple screenshot comparison
import PIL.Image
import numpy as np

def compare_images(baseline_path, current_path):
baseline = PIL.Image.open(baseline_path)
current = PIL.Image.open(current_path)

# Convert to numpy arrays
baseline_array = np.array(baseline)
current_array = np.array(current)

# Calculate difference
diff = np.abs(baseline_array.astype(float) - current_array.astype(float))

# Count different pixels
different_pixels = np.sum(diff > 0)
total_pixels = diff.size

change_percentage = (different_pixels / total_pixels) * 100

return {
'match': change_percentage < 0.1, # Less than 0.1{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} change
'change_percentage': change_percentage,
'different_pixels': different_pixels
}

2. DOM-based Comparison

// Playwright com screenshot comparison
const { chromium } = require('playwright');

async function compareScreenshots(page, url, selector) {
// Baseline
await page.goto(url);
await page.waitForSelector(selector);
const baseline = await page.locator(selector).screenshot();

// Current
await page.goto(url);
await page.waitForSelector(selector);
const current = await page.locator(selector).screenshot();

// Compare
return await page.evaluate(([b, c]) => {
// Canvas comparison
const canvas = document.createElement('canvas');
// ... comparison logic
}, [baseline, current]);
}


Ferramentas

Applitools (Líder de Mercado)

# pytest + Applitools
from applitools.selenium import Eyes, RunnerOptions
from selenium import webdriver

@pytest.fixture
def eyes():
eyes = Eyes(RunnerOptions().test_name("Login Page"))
eyes.api_key = "YOUR_API_KEY"
eyes.force_full_page_screenshot = True
yield eyes

@pytest.fixture
def driver(eyes):
driver = webdriver.Chrome()
driver.get("https://example.com")
yield driver
eyes.abort_async()
driver.quit()

def test_login_page_visual(driver, eyes):
eyes.open(driver, "MyApp", "Login Page Test")
driver.get("https://example.com/login")
eyes.check_window("Login Form")
eyes.close_async()

// Java/Selenium + Applitools
import com.applitools.eyes.selenium.Eyes;
import org.openqa.selenium.WebDriver;

@ExtendWith(ApplitoolsFieldExtension.class)
class VisualTests {

@Eyes
Eyes eyes;

@Test
void loginPageTest(WebDriver driver) {
driver.get("https://example.com/login");
eyes.checkWindow("Login Form");

// Fill form
driver.findElement(By.id("email")).sendKeys("test@example.com");
eyes.checkWindow("Form Filled");

// Submit
driver.findElement(By.id("submit")).click();
eyes.checkWindow("After Submit");
}
}

Cypress Percy

# Instalação
npm install @percy/cypress --save-dev
// cypress/integration/visual.spec.js
import { percySnapshot } from '@percy/cypress';

describe('Visual Regression', () => {
it('homepage looks correct', () => {
cy.visit('/');
percySnapshot('Homepage');
});

it('login page', () => {
cy.visit('/login');
percySnapshot('Login Page', { widths: [768, 1280, 1920] });
});

it('product page with different themes', () => {
cy.visit('/product/1');

// Light theme
percySnapshot('Product Page - Light');

// Dark theme
cy.get('[data-theme="dark"]').click();
percySnapshot('Product Page - Dark');
});
});

Playwright + Screenshot Comparison

// playwright.config.js
const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
testDir: './visual-tests',
screenshotDir: './screenshots',
updateSnapshots: process.argv.includes('--update-snapshots')
});

// visual-tests/homepage.spec.js
const { test, expect } = require('@playwright/test');

test('homepage visual', async ({ page }) => {
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');

const screenshot = await page.screenshot();

// Compare with baseline
const baseline = await page.goto('https://example.com');
const baselineScreenshot = await baseline.screenshot();

const match = await page.evaluate(
([current, baseline]) => {
// Simple diff percentage
return current.length === baseline.length;
},
[screenshot, baselineScreenshot]
);

expect(match).toBeTruthy();
});


Configuração por Framework

Cypress com cypress-image-snapshot

npm install --save-dev cypress-image-snapshot
// cypress/support/index.js
const { addMatchImageSnapshotCommand } = require('cypress-image-snapshot/command');
addMatchImageSnapshotCommand({
    failureThreshold: 0.03,
    failureThresholdType: 'percent',
    customSnapshotsDir: './cypress/snapshots'
});

Cypress.Commands.add('getByTestId', (testId) => {
return cy.get([data-testid="${testId}"]);
});

// cypress/integration/button.spec.js
describe('Button Component', () => {
    it('primary button renders correctly', () => {
        cy.visit('/button');
        cy.getByTestId('primary-button')
            .matchImageSnapshot('primary-button-default');
    });

it('button hover state', () => {
cy.visit('/button');
cy.getByTestId('primary-button')
.trigger('mouseover')
.matchImageSnapshot('primary-button-hover');
});

it('button disabled state', () => {
cy.visit('/button?disabled=true');
cy.getByTestId('primary-button')
.matchImageSnapshot('primary-button-disabled');
});
});

Jest com jest-image-snapshot

npm install --save-dev jest-image-snapshot
// setupTests.js
const { toMatchImageSnapshot } = require('jest-image-snapshot');

expect.extend({ toMatchImageSnapshot });

// Component.test.jsx
import { render } from '@testing-library/react';
import Button from './Button';

test('Button renders correctly', () => {
const { container } = render();

expect(container.firstChild).toMatchImageSnapshot({
customSnapshotIdentifier: 'button-default'
});
});


Estratégias de Visual Testing

1. Full Page vs Component

// Full page - everything
cy.visit('/homepage');
cy.percySnapshot('Homepage Full');

// Specific component
cy.visit('/homepage');
cy.percySnapshot('Homepage Header', {
blackout: ['.ads', '.sidebar'] // Ignore dynamic content
});

// Specific element
cy.get('.main-content')
.percySnapshotElement('Main Content Only');

2. Responsive Testing

const viewports = [
    { name: 'mobile', width: 375, height: 667 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'desktop', width: 1280, height: 720 },
    { name: 'large', width: 1920, height: 1080 }
];

viewports.forEach(viewport => {
it(renders correctly on ${viewport.name}, () => {
cy.viewport(viewport.width, viewport.height);
cy.visit('/');
cy.percySnapshot(${viewport.name}-homepage);
});
});

3. Ignore Dynamic Content

// Applitools - Ignore regions
eyes.checkWindow('Dashboard', {
    ignore: [
        { element: '#dynamic-widget-1' },
        { element: '#ads-banner' },
        { region: { left: 0, top: 0, width: 200, height: 100 } }
    ]
});

// Percy - Blackout
cy.percySnapshot('Dashboard', {
blackout: ['[data-testid="ad-banner"]', '.cookie-consent']
});


CI/CD Integration

# GitHub Actions
name: Visual Regression Tests

on: [push, pull_request]

jobs:
visual-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run Percy
uses: browser-actions/run-percy@latest
with:
percy-token: ${{ secrets.PERCY_TOKEN }}

- name: Run visual tests
run: npm run test:visual

- name: Upload snapshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-diffs
path: visual-test-results/


Conclusão

Visual regression testing detecta mudanças visuais automaticamente. As chaves são:

  1. Escolher ferramenta – Applitools, Percy, oubuilt-in
  2. Configurar ignores – Ignorar conteúdo dinâmico
  3. Testar responsivo – Múltiplos viewports
  4. Integrar CI – Executar em cada PR
  5. Revisar diffs – Aprovar ou corrigir


FAQ

P: Como lidar com conteúdo dinâmico?
R: Use blackout/ignore para elementos variáveis.

P: Qual threshold usar?
R: 0.1-3{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} dependendo da tolerância. Testes strict = 0{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442}.

P: Atualizar snapshots automaticamente?
R: Apenas quando mudanças são intencionais, com code review.