Como escrever testes de regressão visual com Percy ou Chromatic

1. Fundamentos dos testes de regressão visual

Testes de regressão visual são uma categoria de testes automatizados que comparam capturas de tela da interface do usuário (UI) para detectar mudanças visuais não intencionais. Diferentemente dos testes funcionais, que verificam comportamento e lógica (por exemplo, "ao clicar no botão, a função X é chamada"), os testes visuais verificam a aparência: cores, espaçamentos, alinhamentos, fontes e responsividade.

A principal diferença está no tipo de bug que cada abordagem captura. Testes unitários não detectam um botão que mudou de cor ou um padding que foi alterado acidentalmente. Os testes visuais preenchem exatamente essa lacuna. Eles são críticos para times que mantêm sistemas de design, bibliotecas de componentes ou aplicações com interfaces complexas.

Quando utilizar Percy vs. Chromatic:

  • Percy (da BrowserStack): Ideal para equipes que já utilizam ferramentas de teste E2E (Cypress, Playwright) e desejam adicionar verificação visual a fluxos completos. Suporta qualquer framework e integra-se com pipelines CI/CD genéricos.
  • Chromatic (da Storybook): Melhor para projetos que já adotam Storybook como ferramenta de desenvolvimento de componentes. Oferece integração nativa com histórias (stories), gerenciamento de baselines por branch e revisão visual colaborativa.

A escolha depende do ecossistema existente. Se seu time já documenta componentes com Storybook, Chromatic é a escolha natural. Se você precisa de testes visuais em páginas completas ou fluxos E2E, Percy oferece mais flexibilidade.

2. Configuração inicial do ambiente de teste

Integração Percy com GitHub Actions

# .github/workflows/visual-tests.yml
name: Visual Tests
on: [pull_request]
jobs:
  percy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci
      - run: npx percy exec -- npm test
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

Configuração Chromatic para Storybook

# Instalação
npm install --save-dev @chromatic-com/storybook

# Comando no package.json
{
  "scripts": {
    "chromatic": "npx chromatic --project-token=CHROMATIC_PROJECT_TOKEN"
  }
}
# .storybook/main.js
export default {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@chromatic-com/storybook'
  ],
  staticDirs: ['../public'],
};

Estrutura de diretórios recomendada:

src/
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.test.tsx
│   │   └── Button.stories.tsx
│   └── Card/
│       ├── Card.tsx
│       ├── Card.test.tsx
│       └── Card.stories.tsx
visual-snapshots/
└── __snapshots__/
    └── components/

3. Criação de snapshots de referência (baselines)

Estratégias para capturar estados de componentes

// Button.stories.tsx
import { Button } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    chromatic: { 
      delay: 300, // Aguarda animações finalizarem
      pauseAnimationAtEnd: true 
    },
  },
};

export const Default = {
  args: { label: 'Clique aqui', variant: 'primary' },
};

export const Hover = {
  args: { label: 'Clique aqui', variant: 'primary' },
  parameters: {
    pseudo: { hover: true }, // Simula estado hover
  },
};

export const Error = {
  args: { label: 'Erro', variant: 'danger', disabled: false },
};

export const Loading = {
  args: { label: 'Carregando...', variant: 'primary', loading: true },
};

Viewports responsivos

// .storybook/preview.js
export const parameters = {
  chromatic: {
    viewports: [375, 768, 1280], // Mobile, Tablet, Desktop
  },
};

Lidando com elementos dinâmicos

// Card.stories.tsx
export const WithDate = {
  args: { 
    date: new Date('2024-01-15'), // Data fixa, não new Date()
  },
};

// Para dados aleatórios, use funções determinísticas
function generateStableId(index: number): string {
  return `item-${index}`; // Sempre o mesmo valor para o mesmo índice
}

4. Integração com frameworks de componentes e Storybook

Stories otimizadas para Chromatic

// UserCard.stories.tsx
export const FullUserCard = {
  args: {
    name: 'Maria Silva',
    email: 'maria@exemplo.com',
    avatar: '/static/avatar-test.png', // Asset estável
  },
  parameters: {
    chromatic: { 
      diffThreshold: 0.2, // Tolerância para antialiasing
      disable: false 
    },
  },
};

Percy para projetos React

// cypress/e2e/percy-snapshots.cy.js
describe('Testes visuais com Percy', () => {
  it('Captura página inicial', () => {
    cy.visit('/');
    cy.wait(1000); // Aguarda renderização completa
    cy.percySnapshot('Página Inicial - Desktop', {
      widths: [1280],
      minHeight: 2000,
    });
  });

  it('Captura formulário de login', () => {
    cy.visit('/login');
    cy.get('[data-testid="email"]').type('teste@exemplo.com');
    cy.get('[data-testid="password"]').type('senha123');
    cy.percySnapshot('Formulário de Login Preenchido');
  });
});

Automação com decorators

// .storybook/preview.js
export const decorators = [
  (Story) => (
    <div style={{ padding: '20px', backgroundColor: '#f5f5f5' }}>
      <Story />
    </div>
  ),
];

5. Análise e aprovação de mudanças visuais

Fluxo de revisão no Percy

// Configuração de tolerância no Percy
// percy.config.js
module.exports = {
  version: 2,
  snapshot: {
    widths: [375, 768, 1280],
    minHeight: 1024,
    percyCSS: `
      /* Ignora diferenças mínimas em sombras */
      .shadow-box {
        box-shadow: none !important;
      }
      /* Remove animações que causam falso positivo */
      * {
        animation-duration: 0s !important;
        transition-duration: 0s !important;
      }
    `,
  },
};

Thresholds de tolerância no Chromatic

// Component.stories.tsx
export const WithTolerance = {
  parameters: {
    chromatic: {
      diffThreshold: 0.2, // 20% de tolerância para diferenças de pixel
      diffIncludeAntiAliasing: false, // Ignora antialiasing
    },
  },
};

Aprovação automática em CI

# .github/workflows/chromatic.yml
name: Chromatic
on: push
jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: npm ci
      - uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          exitZeroOnChanges: true # Não bloqueia CI se houver mudanças
          onlyChanged: true # Apenas componentes alterados

6. Estratégias de manutenção e prevenção de falsos positivos

Lidando com fontes e assets externos

// .storybook/preview.js
export const loaders = [
  async () => {
    // Pré-carrega fontes para evitar flash de texto invisível
    await document.fonts.ready;
    return {};
  },
];

// Percy CSS para ignorar fontes específicas
percyCSS: `
  @font-face {
    font-family: 'DynamicFont';
    src: local('Arial');
  }
`

Ignorando elementos voláteis

// No componente
<div data-percy-ignore>
  {Math.random()} {/* Elemento ignorado nos snapshots */}
</div>

// No Percy config
percyCSS: `
  [data-percy-ignore] {
    visibility: hidden;
  }
`

Atualização programática de baselines

# Script para atualizar baselines após mudanças intencionais
# scripts/update-baselines.sh
#!/bin/bash
echo "Atualizando baselines visuais..."
npx chromatic --force-rebuild
npx percy exec -- npx cypress run --spec "cypress/e2e/percy-snapshots.cy.js" --env PERCY_BRANCH=main

7. Integração com testes E2E e pipelines de deploy

Combinando Percy com Cypress

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {
      // Configuração adicional
    },
  },
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
  },
});
// cypress/e2e/fluxo-completo.cy.js
describe('Fluxo de checkout', () => {
  it('Captura visual do fluxo completo', () => {
    cy.visit('/products');
    cy.percySnapshot('Lista de Produtos');

    cy.get('[data-testid="product-1"]').click();
    cy.percySnapshot('Detalhes do Produto');

    cy.get('[data-testid="add-to-cart"]').click();
    cy.percySnapshot('Carrinho Atualizado');

    cy.get('[data-testid="checkout"]').click();
    cy.percySnapshot('Página de Checkout');
  });
});

Gate de qualidade no deploy

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  visual-tests:
    uses: ./.github/workflows/visual-tests.yml
  deploy:
    needs: visual-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Verificar aprovação visual
        run: |
          APPROVED=$(curl -s "https://percy.io/api/v1/builds/latest?branch=${{ github.ref_name }}" \
            -H "Authorization: Token ${{ secrets.PERCY_TOKEN }}" \
            | jq -r '.data.attributes.approved')
          if [ "$APPROVED" != "true" ]; then
            echo "Build visual não aprovada. Cancelando deploy."
            exit 1
          fi
      - run: npm run build && npm run deploy

8. Boas práticas avançadas e resolução de problemas comuns

Otimização de performance com paralelismo

# percy.config.js - Snapshot em lote
module.exports = {
  version: 2,
  snapshot: {
    widths: [1280],
    parallelTotal: 4, // 4 snapshots simultâneos
    parallelNonce: process.env.PARALLEL_NONCE || 'default',
  },
};
# Execução paralela no GitHub Actions
jobs:
  percy-parallel:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npx percy exec --parallel --nonce=${{ github.run_id }} -- npx cypress run --spec "cypress/e2e/shard-${{ matrix.shard }}.cy.js"

Debugging de diffs inesperados

# Script para investigar diferenças
# scripts/debug-diff.sh
#!/bin/bash
echo "Baixando snapshots para comparação local..."
npx percy download-build $BUILD_ID --directory ./debug-snapshots
echo "Comparando imagens localmente..."
compare ./debug-snapshots/baseline.png ./debug-snapshots/current.png ./debug-snapshots/diff.png
echo "Diff salva em ./debug-snapshots/diff.png"

Versionamento de snapshots e rollback

# Script de rollback
# scripts/rollback-baselines.sh
#!/bin/bash
echo "Revertendo baselines para versão anterior..."
git checkout HEAD~1 -- visual-snapshots/__snapshots__
git commit -m "Rollback de baselines visuais para versão estável"
git push origin main
npx chromatic --force-rebuild
npx percy exec -- npx cypress run --spec "cypress/e2e/percy-snapshots.cy.js"

Referências