<p><strong>Meta Description:</strong> Aprenda visual regression testing: screenshots comparison, ferramentas como Applitools, Percy e Cypress Percy. Como detectar mudanças visuais automaticamente.</p>

<hr>
<h2>O Que É Visual Regression Testing?</h2>

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

<h3>Por Que Importa?</h3>

<ul><li>Detectar mudanças em CSS/HTML</li><ol><li>Prevenir "pixel perfect" regressions</li>
<li>Garantir consistência cross-browser</li>
<li>Automatizar verificação de design</li></ol></ul>

<hr>
<h2>Abordagens</h2>

<h3>1. Pixel-to-Pixel Comparison</h3>

<pre><code># Simple screenshot comparison
import PIL.Image
import numpy as np

<p>def compare_images(baseline_path, current_path):<br>    baseline = PIL.Image.open(baseline_path)<br>    current = PIL.Image.open(current_path)</p>

<p># Convert to numpy arrays<br>    baseline_array = np.array(baseline)<br>    current_array = np.array(current)</p>

<p># Calculate difference<br>    diff = np.abs(baseline_array.astype(float) - current_array.astype(float))</p>

<p># Count different pixels<br>    different_pixels = np.sum(diff &gt; 0)<br>    total_pixels = diff.size</p>

<p>change_percentage = (different_pixels / total_pixels) * 100</p>

<p>return {<br>        'match': change_percentage &lt; 0.1,  # Less than 0.1{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} change<br>        'change_percentage': change_percentage,<br>        'different_pixels': different_pixels<br>    }<br></code></pre></p>

<h3>2. DOM-based Comparison</h3>

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

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

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

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

<hr>
<h2>Ferramentas</h2>

<h3>Applitools (Líder de Mercado)</h3>

<pre><code># pytest + Applitools
from applitools.selenium import Eyes, RunnerOptions
from selenium import webdriver

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

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

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

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

<p>@ExtendWith(ApplitoolsFieldExtension.class)<br>class VisualTests {</p>

<p>@Eyes<br>    Eyes eyes;</p>

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

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

<p>// Submit<br>        driver.findElement(By.id("submit")).click();<br>        eyes.checkWindow("After Submit");<br>    }<br>}<br></code></pre></p>

<h3>Cypress Percy</h3>

<pre><code># Instalação
npm install @percy/cypress --save-dev
</code></pre>

<pre><code>// cypress/integration/visual.spec.js
import { percySnapshot } from '@percy/cypress';

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

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

<p>it('product page with different themes', () =&gt; {<br>        cy.visit('/product/1');</p>

<p>// Light theme<br>        percySnapshot('Product Page - Light');</p>

<p>// Dark theme<br>        cy.get('[data-theme="dark"]').click();<br>        percySnapshot('Product Page - Dark');<br>    });<br>});<br></code></pre></p>

<h3>Playwright + Screenshot Comparison</h3>

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

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

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

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

<p>const screenshot = await page.screenshot();</p>

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

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

<p>expect(match).toBeTruthy();<br>});<br></code></pre></p>

<hr>
<h2>Configuração por Framework</h2>

<h3>Cypress com cypress-image-snapshot</h3>

<pre><code>npm install --save-dev cypress-image-snapshot
</code></pre>

<pre><code>// cypress/support/index.js
const { addMatchImageSnapshotCommand } = require('cypress-image-snapshot/command');
addMatchImageSnapshotCommand({
    failureThreshold: 0.03,
    failureThresholdType: 'percent',
    customSnapshotsDir: './cypress/snapshots'
});

<p>Cypress.Commands.add('getByTestId', (testId) =&gt; {<br>    return cy.get(<code>[data-testid="${testId}"]</code>);<br>});<br></code></pre></p>

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

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

<p>it('button disabled state', () =&gt; {<br>        cy.visit('/button?disabled=true');<br>        cy.getByTestId('primary-button')<br>            .matchImageSnapshot('primary-button-disabled');<br>    });<br>});<br></code></pre></p>

<h3>Jest com jest-image-snapshot</h3>

<pre><code>npm install --save-dev jest-image-snapshot
</code></pre>

<pre><code>// setupTests.js
const { toMatchImageSnapshot } = require('jest-image-snapshot');

<p>expect.extend({ toMatchImageSnapshot });</p>

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

<p>test('Button renders correctly', () =&gt; {<br>    const { container } = render(<Button>Click me</Button>);</p>

<p>expect(container.firstChild).toMatchImageSnapshot({<br>        customSnapshotIdentifier: 'button-default'<br>    });<br>});<br></code></pre></p>

<hr>
<h2>Estratégias de Visual Testing</h2>

<h3>1. Full Page vs Component</h3>

<pre><code>// Full page - everything
cy.visit('/homepage');
cy.percySnapshot('Homepage Full');

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

<p>// Specific element<br>cy.get('.main-content')<br>    .percySnapshotElement('Main Content Only');<br></code></pre></p>

<h3>2. Responsive Testing</h3>

<pre><code>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 }
];

<p>viewports.forEach(viewport =&gt; {<br>    it(<code>renders correctly on ${viewport.name}</code>, () =&gt; {<br>        cy.viewport(viewport.width, viewport.height);<br>        cy.visit('/');<br>        cy.percySnapshot(<code>${viewport.name}-homepage</code>);<br>    });<br>});<br></code></pre></p>

<h3>3. Ignore Dynamic Content</h3>

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

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

<hr>
<h2>CI/CD Integration</h2>

<pre><code># GitHub Actions
name: Visual Regression Tests

<p>on: [push, pull_request]</p>

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

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

<p>- name: Run visual tests<br>        run: npm run test:visual</p>

<p>- name: Upload snapshots<br>        uses: actions/upload-artifact@v4<br>        if: failure()<br>        with:<br>          name: visual-diffs<br>          path: visual-test-results/<br></code></pre></p>

<hr>
<h2>Conclusão</h2>

<p>Visual regression testing detecta mudanças visuais automaticamente. As chaves são:<br><ol><li><strong>Escolher ferramenta</strong> - Applitools, Percy, oubuilt-in</li><br><li><strong>Configurar ignores</strong> - Ignorar conteúdo dinâmico</li><br><li><strong>Testar responsivo</strong> - Múltiplos viewports</li><br><li><strong>Integrar CI</strong> - Executar em cada PR</li><br><li><strong>Revisar diffs</strong> - Aprovar ou corrigir</li><br></ol><br><hr><br><h3>FAQ</h3></p>

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

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

<p><strong>P: Atualizar snapshots automaticamente?</strong>  <br>R: Apenas quando mudanças são intencionais, com code review.</p>