Um teste visual que não distingue “a página mudou” de “a captura não é reprodutível” é silenciado em uma semana. A diferença é essa, e ela é determinística por construção: mesmas entradas, mesmo veredicto.
Um gate que não separa os dois vira ruído e alguém desliga. O trabalho todo é manter os dois separados — sem IA no meio: mesma entrada, mesmo veredicto.
diff.png e os recortes das regiões antes de qualquer coisa.git HEAD, a
config de máscara ou o próprio PNG). Refaça a referência de propósito, ou desfaça a mudança —
nunca “rodar de novo”.STALE existe porque o modo clássico de falhar do teste visual não é flakiness: é a
referência re-gerada em silêncio até o teste ficar verde. Amarrar a referência ao git HEAD
e a um hash da config de máscara transforma “refazer a referência” num ato deliberado e revisável.
0 PASS 1 FAIL 2 STALE 3 ERROR
python scripts/visual_gate.py fingerprint baseline/home.png \
--url https://app.exemplo/home --viewport 1280x720 \
--config baseline/home.masks.json
python scripts/visual_gate.py compare baseline/home.png actual/home.png \
--meta baseline/home.png.meta.json --masks baseline/home.masks.json \
--max-diff 0.001 --max-regions 1 --bundle artifacts/home
Com --json sai veredicto, percentual de diferença, número de regiões e a caixa de cada
uma — o suficiente para o CI publicar o que se moveu, em vez de “o teste falhou”.
--max-diff é rede de segurança, não o sinal principalAntialiasing e
hinting mexem alguns milhares de pixels sem mudança real; um componente que se move mexe dezenas de
milhares. Comece em 0.001 (0,1%) e aperte depois das máscaras.--max-regions é o sinal que pega mudança pequena e significativaUma cor
errada num botão de 60×40 é ~0,26% de uma página 1280×720, mas exatamente uma região.
Orçamento: 1.--toleranceDelta por canal (padrão 0) para ruído de captura em
gradiente. Se você precisa de mais de ~8, o problema é a captura.masked_pixels) — ninguém esconde regressão dentro de uma sem que apareça no
relatório.prefers-reduced-motion: reduce e
*{animation:none!important;transition:none!important}.document.fonts.ready e network idle — uma
fonte de fallback difere a página inteira.Math.random, congele antes de capturar.15 testes passando — pytest -q (28,9s)
capturas idênticas -> PASS, diff 0.00000%, 0 regiões
um bloco 60x40 alterado -> FAIL, 1 região nas coordenadas exatas
mesma mudança dentro de máscara -> PASS, masked_pixels >= a área mascarada
mudança fora da máscara -> FAIL (máscara não esconde o resto da página)
duas mudanças com --max-regions 1-> FAIL; com --max-regions 2 -> PASS
referência velha (git HEAD muda) -> STALE, saída 2
config de máscara alterada -> STALE
640x480 contra 1280x720 -> ERROR, "size mismatch" (nunca PASS silencioso)
decodificador contra PIL e ImageMagick -> pixels idênticos em 6 codificações
16-bit -> byte alto igual ao render de 8 bits
bundle de falha -> diff.png re-decodifica, área vermelha >= a região
O teste nos dois sentidos é o ponto: um gate testado só contra “nada mudou” é decoração. Aqui os
dois lados são verificados — inclusive o caso que morde em produção: referência otimizada
(optipng, pngquant, ImageMagick) que um decodificador ingênuo recusa.
E o decodificador de PNG é stdlib (zlib + struct), sem PIL, OpenCV ou ImageMagick no runner: gate de CI que exige biblioteca C em cada máquina é gate que quebra justamente na máquina que importa.
.meta.jsonSem ele o gate não detecta
staleness. Os dois vão juntos no commit.STALE existe para impedir: a diferença está no bundle, revise antes de escrever uma
referência nova.ERROR por desenho: é bug de
captura, não regressão.sweep reporta
MISSING e falha o run — diretório de capturas vazio não é “tudo certo”.A skill Visual Regression Gates traz o gate em stdlib, 15 testes, o template de captura Playwright, o formato das máscaras e os dois guias (determinismo da captura e estratégia de máscara/fingerprint).
Ver a skill Visual Regression Gates Ver todas as skills
Quer isso ligado no seu pipeline? Setup guiado a partir de R$ 1.500, com o primeiro ciclo rodado junto.