# Visual Regression Testing: Detectando Mudanças Visuais

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

---

## 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
- Prevenir "pixel perfect" regressions
- Garantir consistência cross-browser
- Automatizar verificação de design

---

## Abordagens

### 1. Pixel-to-Pixel Comparison

```python
# 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% change
        'change_percentage': change_percentage,
        'different_pixels': different_pixels
    }
```

### 2. DOM-based Comparison

```javascript
// 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)

```python
# 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
// 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

```bash
# Instalação
npm install @percy/cypress --save-dev
```

```javascript
// 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

```javascript
// 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

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

```javascript
// 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}"]`);
});
```

```javascript
// 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

```bash
npm install --save-dev jest-image-snapshot
```

```javascript
// 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(<Button>Click me</Button>);
    
    expect(container.firstChild).toMatchImageSnapshot({
        customSnapshotIdentifier: 'button-default'
    });
});
```

---

## Estratégias de Visual Testing

### 1. Full Page vs Component

```javascript
// 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

```javascript
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

```javascript
// 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

```yaml
# 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% dependendo da tolerância. Testes strict = 0%.

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