toHaveScreenshot() cria um screenshot de baseline na primeira execução e falha em qualquer execução seguinte onde a diferença de pixels ultrapassar um limite. A falha mais comum na primeira execução no CI não tem nada a ver com uma regressão real. Baselines gerados na máquina macOS do desenvolvedor não casam com o que o Linux do CI renderiza, porque hinting de fontes e sub-pixel rendering diferem entre plataformas.
O que é teste de regressão visual
Um teste de regressão visual captura um screenshot de uma página ou elemento, armazena como baseline, e compara cada execução futura contra esse baseline pixel a pixel. Se a diferença ultrapassar um threshold configurável, o teste falha e mostra exatamente quais pixels mudaram.
A distinção de um screenshot comum é importante. Tirar um screenshot com page.screenshot() apenas salva um arquivo. Nunca falha. Não diz nada sobre se a página está correta visualmente. Testes de regressão visual precisam de uma referência (a imagem acordada como "deve ser assim") e uma comparação automatizada contra essa referência em cada execução.
O apelo é real. Você pega regressões de layout que nenhuma asserção funcional jamais detectaria. Uma mudança de CSS que desloca um modal cinco pixels para a esquerda. Um bug de z-index que esconde um dropdown atrás de um banner. Uma implementação de dark mode que inverte acidentalmente um logo. São os tipos de bug que passam pelo code review porque os revisores focam em lógica, não em pixels.
O desafio também é real. Screenshots são sensíveis. Uma diferença de um pixel de anti-aliasing entre macOS e Linux, um timestamp dinâmico na página, um anúncio com conteúdo rotativo: tudo isso gera falhas falsas. Gerenciar esse ruído é a maior parte do trabalho prático em testes de regressão visual.
toHaveScreenshot(): a asserção integrada
A asserção visual do Playwright é expect(locator).toHaveScreenshot() ou expect(page).toHaveScreenshot(). Você pode capturar a página inteira ou limitar a qualquer locator.
// tests/visual/homepage.spec.ts
import { test, expect } from '@playwright/test';
test('homepage bate com o baseline', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Screenshot da página inteira
await expect(page).toHaveScreenshot('homepage.png');
});
test('botão de login bate com o baseline', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Screenshot limitado a um elemento específico
const loginButton = page.getByRole('button', { name: 'Login' });
await expect(loginButton).toHaveScreenshot('login-button.png');
});O argumento de nome ('homepage.png') é opcional. Se você omitir, o Playwright gera um nome automaticamente a partir do título do teste e de um contador. Fornecer um nome explícito facilita encontrar e entender os arquivos de baseline depois.
Na primeira execução, não há baseline para comparar. O Playwright cria um.
Gerando screenshots de baseline na primeira execução
Rode seus testes pela primeira vez e você vai ver erros assim:
Error: A snapshot doesn't exist at tests/visual/homepage.spec.ts-snapshots/homepage-chromium-darwin.png, writing actual.Isso é esperado. O Playwright está dizendo que escreveu o arquivo de baseline e pedindo para você revisar e fazer commit. O teste falha na primeira execução por design. O Playwright não cria um baseline silenciosamente sem que você saiba.
Após a primeira execução, o projeto vai ter um diretório de snapshots:
tests/
visual/
homepage.spec.ts
homepage.spec.ts-snapshots/
homepage-chromium-darwin.png
homepage-chromium-linux.png
login-button-chromium-darwin.pngRevise essas imagens. Se parecerem corretas, faça commit delas. Agora são o baseline. Cada execução de teste seguinte compara contra esses arquivos commitados.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Onde os arquivos de snapshot são armazenados. Padrão: ao lado do arquivo spec.
snapshotDir: './tests/__snapshots__',
use: {
baseURL: 'https://lab.becomeqa.com',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});Você pode centralizar todos os snapshots em um único diretório com snapshotDir na config, o que alguns times preferem para organização mais limpa do repositório.
Atualizando baselines com --update-snapshots
A aplicação muda. O design muda. Quando uma mudança visual é intencional, você precisa atualizar o baseline. Rode:
npx playwright test --update-snapshotsIsso substitui todos os snapshots existentes com novos screenshots. Cada teste que rodou agora tem seu estado atual como novo baseline.
Para atualizar snapshots de apenas um arquivo de teste:
npx playwright test tests/visual/homepage.spec.ts --update-snapshotsOu para um teste específico pelo nome:
npx playwright test --update-snapshots -g "homepage bate com o baseline"--update-snapshots com o mesmo cuidado que você dá ao git push --force. Rodar descuidadamente vai sobrescrever baselines legítimos com estados quebrados. Sempre revise as imagens atualizadas antes de fazer commit. No CI, a flag nunca deve ser definida automaticamente. Deve rodar apenas em resposta a uma ação deliberada do desenvolvedor.Após atualizar, você vai commitar os arquivos .png alterados. O diff no code review vai mostrar as imagens antes e depois, que é exatamente o lugar certo para pegar mudanças visuais não intencionais.
Configurando thresholds de comparação
Comparação pixel a pixel funciona perfeitamente em ambiente controlado e gera ruído constante em qualquer outro lugar. O Playwright oferece três opções de threshold para gerenciar a sensibilidade.
test('card de produto bate com o baseline', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/products');
const productCard = page.locator('.product-card').first();
await expect(productCard).toHaveScreenshot('product-card.png', {
// Número máximo de pixels que podem diferir
maxDiffPixels: 100,
// Razão máxima de pixels diferentes (0–1). 0.01 = 1% de todos os pixels
maxDiffPixelRatio: 0.01,
// Threshold de diferença de cor por pixel (0–1). Maior = mais tolerante
threshold: 0.2,
});
});threshold controla o quanto um único pixel precisa ser diferente para contar como "diferente". O padrão é 0.2, que trata pequenas diferenças de anti-aliasing e sub-pixel rendering. Aumente para 0.3 ou 0.4 em componentes com muitas curvas ou gradientes onde a renderização varia levemente entre plataformas.
maxDiffPixels é uma contagem absoluta. Use para componentes pequenos e delimitados onde você sabe que alguns pixels podem variar (renderização de ícones, border radius) mas um deslocamento de 50 pixels deve sempre falhar.
maxDiffPixelRatio é uma porcentagem do total de pixels. Use para screenshots de página inteira onde a contagem total de pixels é grande. maxDiffPixels: 100 em uma página de 1920x1080 é extremamente estrito, mas maxDiffPixelRatio: 0.001 dá uma tolerância razoável.
Você pode definir padrões no playwright.config.ts para não repetir os mesmos thresholds em cada teste:
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
threshold: 0.2,
},
},
});Testes individuais ainda podem sobrescrever esses valores quando precisam de sensibilidade diferente.
Mascarando conteúdo dinâmico
Conteúdo dinâmico é a maior fonte de falhas falsas em testes de regressão visual. Um timestamp que atualiza a cada segundo, um avatar de usuário puxado de um CDN, um banner com anúncio rotativo: qualquer um desses vai gerar um diff em cada execução.
A opção mask do Playwright aceita um array de locators. Essas regiões são pintadas com uma cor sólida antes da comparação.
test('dashboard bate com o baseline', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
// Mascara o timestamp "Última atualização" no cabeçalho
page.locator('[data-testid="last-updated-timestamp"]'),
// Mascara o avatar do usuário. Diferente para cada usuário.
page.locator('[data-testid="user-avatar"]'),
// Mascara containers de anúncios de terceiros
page.locator('.ad-container'),
],
// Personaliza a cor da máscara (padrão é magenta)
maskColor: '#FF00FF',
});
});As regiões mascaradas aparecem na comparação como um bloco sólido de cor. A comparação ainda roda em todo o screenshot. As áreas mascaradas simplesmente sempre casam consigo mesmas, porque tanto o screenshot atual quanto o esperado têm a mesma máscara aplicada.
data-testid a conteúdos dinâmicos especificamente para que possam ser mascarados de forma confiável em testes visuais. Selecionar por nome de classe funciona, mas nomes de classe mudam. Um data-testid="user-avatar" é estável e comunica claramente seu propósito para qualquer pessoa lendo o teste.Para animações, use animations: 'disabled' para parar animações CSS antes do screenshot:
test('seção hero animada bate com o baseline', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await expect(page).toHaveScreenshot('hero.png', {
animations: 'disabled',
});
});Isso congela transições e animações CSS no estado inicial, tornando componentes animados determinísticos. Para animações controladas por JavaScript que não usam transições CSS, talvez seja necessário aguardar a animação terminar ou adicionar um waitForLoadState('networkidle') antes da asserção.
Nomenclatura de snapshots e organização multiplataforma
Olhe o nome de arquivo que o Playwright gera: homepage-chromium-darwin.png. O browser e o sistema operacional estão embutidos no nome. Não é por acaso.
A mesma página renderizada no Chromium no macOS versus Chromium no Linux produz pixels sutilmente diferentes. Hinting de fontes, sub-pixel rendering e pequenas diferenças em como o OS compõe gráficos significam que você não pode compartilhar um único arquivo de baseline entre plataformas. O Playwright trata isso criando baselines separados para cada combinação browser/OS.
tests/__snapshots__/
homepage.spec.ts/
homepage-chromium-darwin.png (macOS Chrome)
homepage-chromium-linux.png (Linux Chrome)
homepage-firefox-linux.png (Linux Firefox)
homepage-webkit-darwin.png (macOS Safari)Você controla o padrão de nomenclatura através da opção snapshotPathTemplate no playwright.config.ts:
// playwright.config.ts
export default defineConfig({
snapshotPathTemplate:
'{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}-{projectName}-{platform}{ext}',
});Os tokens disponíveis são:
{arg}: o nome que você passou paratoHaveScreenshot(){projectName}: o nome do projeto na config (ex.:chromium,firefox){platform}: o OS (darwin,linux,win32){testFileName}: o nome do arquivo spec sem extensão{snapshotDir}: o diretório base de snapshots
Mantenha {platform} no template. Removê-lo e tentar compartilhar um baseline entre OSes é o erro mais comum que os times cometem ao configurar testes visuais pela primeira vez. Gera falhas falsas constantes no CI.
Rodando testes visuais no CI
Rodar testes visuais no CI revela o problema de OS imediatamente. Seus baselines foram gerados na máquina macOS do desenvolvedor. O pipeline de CI roda no Linux. Os snapshots não casam.
A solução mais limpa é gerar baselines dentro do mesmo container Docker que o CI usa. O Playwright fornece imagens Docker oficiais:
# .github/workflows/visual-tests.yml
name: Visual Tests
on: [push, pull_request]
jobs:
visual:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.44.0-jammy
steps:
- uses: actions/checkout@v4
- name: Instalar dependências
run: npm ci
- name: Rodar testes visuais
run: npx playwright test tests/visual/
- name: Upload do relatório de diff em caso de falha
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-visual-report
path: playwright-report/
retention-days: 7Quando os testes falham no CI, o relatório enviado contém o screenshot atual, o baseline esperado e uma imagem de diff que destaca exatamente quais pixels mudaram. É assim que você distingue uma regressão visual real de uma diferença de ambiente.
Para gerar baselines Linux a partir de uma máquina macOS sem mudar para Linux, rode o container Docker do Playwright localmente:
# Gera baselines compatíveis com Linux do seu Mac
docker run --rm \
-v "$(pwd):/work" \
-w /work \
mcr.microsoft.com/playwright:v1.44.0-jammy \
npx playwright test tests/visual/ --update-snapshotsIsso escreve novos arquivos de snapshot *-linux.png que vão casar com o que o CI produz. Faça commit desses arquivos e as falhas de CI causadas por diferenças de plataforma desaparecem.
Um padrão comum no CI é rodar testes visuais em um job separado, após a suite de testes funcionais passar primeiro. Testes visuais são mais lentos e suas falhas são mais ruidosas, então mantê-los em um step dedicado evita que bloqueiem o feedback rápido sobre regressões funcionais:
// playwright.config.ts
export default defineConfig({
projects: [
// Testes funcionais rodam primeiro
{
name: 'functional',
testMatch: 'tests/functional/**/*.spec.ts',
},
// Testes visuais rodam depois dos funcionais
{
name: 'visual',
testMatch: 'tests/visual/**/*.spec.ts',
dependencies: ['functional'],
},
],
});Playwright integrado vs Applitools e Percy
O teste visual integrado do Playwright cobre bastante coisa. Mas ferramentas comerciais como Applitools Eyes e Percy existem por razões que vale entender.
A limitação central da abordagem integrada é o gerenciamento de snapshots. Cada imagem de baseline vive no repositório. Um projeto com 50 testes visuais, 3 browsers e 2 plataformas gera 300 arquivos PNG. Adicione mais casos de teste e o repo cresce. Revisar mudanças visuais em um pull request significa olhar diffs de imagens na interface do GitHub, que funciona mas não é ótimo para imagens grandes ou mudanças sutis.
Applitools e Percy resolvem isso com armazenamento em nuvem para baselines e UIs dedicadas para review de diffs visuais. Oferecem comparação inteligente baseada em AI que entende deslocamentos de layout versus mudanças de conteúdo, e fluxos de trabalho de equipe para aprovar ou rejeitar mudanças visuais.
O trade-off é direto:
| | Playwright integrado | Applitools / Percy |
|--|---------------------|-------------------|
| Custo | Gratuito | Pago (tier gratuito disponível) |
| Setup | Minutos | Minutos + chave de API |
| Armazenamento de baselines | Repositório Git | Nuvem |
| UI de review de diffs | Relatório HTML do Playwright | UI dedicada na nuvem |
| Comparação com AI | Não | Sim (Applitools) |
| Baselines cross-browser | Arquivos separados por browser/OS | Unificado com normalização |
| Snapshots no CI | Requer imagem Docker compatível | Gerenciado pelo serviço |
Para projeto solo ou time pequeno, a abordagem integrada é o ponto de partida certo. É gratuita, rápida de configurar e trata bem os casos comuns. O fluxo com Docker gerencia o problema cross-OS adequadamente após a primeira configuração.
Para times maiores, especialmente onde várias pessoas precisam revisar e aprovar mudanças visuais, o atrito de gerenciar arquivos PNG no git e revisar diffs no GitHub se torna real. É quando um serviço dedicado começa a justificar o custo. Você está pagando pelo fluxo de review tanto quanto pela tecnologia de comparação.
O Applitools também oferece uma integração com Playwright que substitui toHaveScreenshot() pelas chamadas eyes.check() do Applitools. A migração é atualizar um import e mudar a chamada de asserção, não reescrever os testes.
FAQ
Como rodo apenas testes visuais sem rodar a suite inteira?Use a flag --grep ou organize os testes visuais em um diretório próprio e aponte o Playwright para ele: npx playwright test tests/visual/. Se você usa projects na config, npx playwright test --project=visual roda só o projeto visual.
Aguarde o spinner desaparecer antes da asserção: await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' }). Como alternativa, mascare-o. A abordagem de máscara é mais resiliente. Se o timing mudar, uma máscara ainda trata isso, mas um waitFor com timeout curto pode não tratar.
toHaveScreenshot() para testar viewport mobile?
Sim. Defina o viewport na config do projeto ou no próprio teste: await page.setViewportSize({ width: 375, height: 812 }). O Playwright vai tratar screenshots mobile e desktop como baselines separados se forem capturados em testes ou projects separados.
Menos do que você imagina. Testes visuais são mais adequados para componentes e páginas onde a saída visual é genuinamente parte da especificação. Exemplos: estados de botões de um design system, uma visualização de dados, um preview de exportação de PDF. Tentar cobrir cada página visualmente cria uma carga de manutenção que os times geralmente abandonam em poucos meses.
Posso capturar snapshot de um componente em isolamento sem navegar para uma página?Não diretamente com o Playwright. É uma ferramenta baseada em browser que opera em páginas completas. Para testes visuais de componentes em isolamento, Storybook com Chromatic (Percy para Storybook) é a ferramenta mais adequada. Testes visuais com Playwright funcionam melhor no nível de integração: páginas reais em um browser real.
→ Veja também: Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Arquivo de Configuração do Playwright Explicado: Todas as Opções | Depurando Testes Instáveis: Um Guia Prático | Testes de Regressão Visual com IA: Além das Capturas Pixel-Perfeitas | Testes Cross-Browser com Playwright: Chrome, Firefox, Safari