Gotchas
- Não gerar testes sem detectar a pilha primeiro — Detecte primeiro o framework do projeto (React/Vue/Svelte/Flutter/HTML etc.). Não crie testes Playwright para um projeto Flutter.
- Não aplicar critérios fora do WCAG 2.2 AA — Testes de acessibilidade devem usar WCAG 2.2 AA. Taxa de contraste 4.5:1 (texto normal), 3:1 (texto grande/UI não textual), alvos de toque 24×24px (AA) / 44×44px (AAA). Fonte: design-kit design-audit Gotcha #3
- Verificar a versão do ruleset do axe-core — Ao usar
@axe-core/playwrightouaxe-core, verifique o ruleset da versão instalada no projeto. Há diferenças de regras entre versões principais. - Não configurar threshold para testes de regressão visual — Comparação pixel a pixel causa falsos positivos devido a anti-aliasing e diferenças de renderização de fonte. É obrigatório um threshold como
maxDiffPixelRatio: 0.01. - Definir claramente o escopo de detecção de valores hardcoded na validação de tokens — Valores brutos como
#ff0000,16px,400em CSS não significam necessariamente FALHA. Código de reset/normalize CSS e bibliotecas externas devem ser excluídos. - Verificar a origem dos breakpoints em testes responsivos — Use os breakpoints definidos no sistema de design do projeto. Não insira 320/768/1024 arbitrariamente.
- Não omitir testes de modo escuro — Se os tokens de design tiverem mapeamento de modo escuro, teste em ambos os modos. Use
prefers-color-scheme: darkmedia query ou alternância de classe de token. - Verificar dependência do Storybook — Não presuma Storybook para testes de regressão visual. Se não houver Storybook, use screenshots de página do Playwright como alternativa.
- Algoritmo de contraste APCA é informativo — O critério de julgamento é WCAG 2.2 AA (fórmula 4.5:1 WCAG2). APCA Lc deve ser incluído apenas como informação adicional e não usado como critério PASS/FAIL. Fonte: design-guide Gotcha #10
- Não relatar primeira execução com baseline como "teste aprovado" (controle negativo obrigatório) — O Playwright, quando não há snapshot, grava automaticamente a tela real como baseline e marca essa execução como aprovada. Portanto, um green logo após
--update-snapshotsnão prova nada — não foi comparado com nada (falha na validação de evidência 2 ativada). Após criar o baseline, realize obrigatoriamente um controle negativo: aplique uma modificação intencional no elemento alvo (ex.: alterar a cor de fundo em 1 passo) e execute o teste para confirmar que falha, depois reverta e confirme que passa. Não relate que o teste de regressão visual está funcionando antes de citar a saída dessas duas execuções como evidência. Fonte: Playwright Visual Comparisons (primeira execução: "A snapshot doesn't exist ... writing actual"),harness/docs/guides/qa-evaluation-guide.md§Evidence Validity Gate checks 2·3. - Snapshot vazio/página vazia não são evidência de PASS — Se a renderização falhar e uma tela em branco for capturada, o teste de regressão visual ainda passará se for igual ao baseline. Testes visuais devem incluir uma asserção de existência de conteúdo (elemento principal visível, número de itens na lista ≥ 1, altura do contêiner > 0). Um arquivo de teste com apenas comparação de snapshot permitirá que defeitos como collapse de altura ilimitada passem — é um caso real de acidente. Detalhes:
../../references/visual-change-protocol.md§3. - Validação de mudança parcial com snapshots escopados — detectar alterações em áreas não intencionais — Ao verificar "apenas a borda mudou", usar apenas um snapshot de página inteira não permitirá distinguir se o fundo também mudou. Crie snapshots por locator do elemento alvo e snapshots da área circundante separadamente, julgando se o alvo mudou e a área circundante não. Se o snapshot da área circundante falhar, é uma regressão. Detalhes:
../../references/visual-change-protocol.md§2. prefers-reduced-motion: reducenão significa "remover animação" — Essa configuração indica que movimentos que causam vertigem (escala, pan etc.) devem ser substituídos por alternativas mais suaves, não que todas as transições devem ser removidas. Portanto, não afirme "número de animações = 0" em testes de reduced-motion. Verifique se animações baseadas emtransform: scale/translateforam removidas ou substituídas por opacity/dissolve. Fonte: MDN prefers-reduced-motion (Baseline 2020-01).- Teste de propagação de decisão apenas com golden é FAIL — Cobertura do Manifesto de Propagação de Decisão — Quais superfícies devem refletir uma decisão de design aprovada é listado por
.design/decisions.yaml(decision_id→required_surfaces[]→ golden + assertions). Se este manifesto existir, gere testes seguindo a regra de cobertura 4. O ponto chave é: se houver apenas golden e nenhuma asserção de visible / count / height dos elementos principais, é FAIL — uma tela em branco também passaria se igual ao baseline, então um snapshot sozinho não prova que "o usuário vê" (mesma raiz do Gotcha 11). Superfícies ausentes emexcluded_surfacessem justificativa não são revisadas, mas lacunas de cobertura. Esquema, regras e verificador executável:../../references/visual-change-protocol.md§6 Decision Propagation Manifest. - Nomeie os canais de evidência — existência de snapshot não é observação do usuário —
artifact_snapshot/dom_snapshot/browser_user_visible/device_user_visibletêm intensidades diferentes. Uma declaração PASS deve conter os 5 elementos: viewport · route/state · visible locator · count/height · screenshot/golden id. Se faltar algum, é[não verificado]. Definições de canal:../../references/visual-change-protocol.md§7 Evidence Channels. O protocolo de prioridade quando o usuário relata uma falha é o padrão emharness/docs/guides/skill-design-guide.md§3.8 — não conteste com base em teste aprovado.
Processo
Passo 0: Detecção do Projeto
Detecte a pilha de UI e o sistema de design na raiz do projeto:
| Alvo de Detecção | Pilha | Ferramenta de Teste |
|---|---|---|
package.json + React | React | Playwright + axe-core + Storybook (opcional) |
package.json + Vue | Vue | Playwright + axe-core |
package.json + Svelte | Svelte | Playwright + axe-core |
package.json + Angular | Angular | Playwright + axe-core |
pubspec.yaml + Flutter | Flutter | → redirecionar para skill flutter-test |
*.html (site estático) | HTML/CSS | Playwright + axe-core + pa11y |
Detecções adicionais:
- Tokens de design:
**/tokens/**,**/theme/**,**/design-system/**, propriedades personalizadas CSS (--) - Storybook:
.storybook/,*.stories.* - Tailwind:
tailwind.config.* - CSS-in-JS: styled-components, emotion, vanilla-extract
Passo 1: Análise do Alvo
Analise o alvo e o tipo de teste a partir de $ARGUMENTS.
Inferência automática quando o tipo não for especificado:
| Característica do Alvo | Tipo de Teste |
|---|---|
| Arquivo de token, configuração de tema | token (validação de consistência) |
| Componente, página | a11y (acessibilidade) + visual (regressão visual) |
| Layout, grade | responsive (responsivo) |
| Projeto inteiro | Todos os tipos |
Passo 2: Exploração de Padrões Existentes
- Testes de acessibilidade existentes:
*.a11y.test.*, import axe-core - Testes visuais existentes:
*.visual.test.*,__snapshots__/,.loki/ - Configuração Playwright:
playwright.config.* - Testes Storybook: existência de
*.stories.* - Validação de tokens de design: regras lint existentes, configuração stylelint
Passo 3: Geração de Teste de Consistência de Tokens
Analise arquivos de token de design para gerar um teste de consistência.
// tests/design/tokens.test.ts
import { describe, it, expect } from 'vitest'
import fs from 'fs'
describe('Consistência de Token de Design', () => {
// Analisar propriedades personalizadas CSS
const cssContent = fs.readFileSync('src/styles/tokens.css', 'utf-8')
const tokens = [...cssContent.matchAll(/--([^:]+):\s*([^;]+)/g)]
it('todos os tokens de cor usam formato consistente (hex/oklch/hsl)', () => {
const colorTokens = tokens.filter(([, name]) => name.includes('color'))
const formats = new Set(
colorTokens.map(([, , value]) => {
if (value.trim().startsWith('#')) return 'hex'
if (value.trim().startsWith('oklch')) return 'oklch'
if (value.trim().startsWith('hsl')) return 'hsl'
return 'other'
})
)
expect(formats.size).toBeLessThanOrEqual(1)
})
it('tokens de espaçamento seguem escala (base * multiplicador)', () => {
const spacingTokens = tokens
.filter(([, name]) => name.includes('spacing'))
.map(([, , value]) => parseFloat(value))
.sort((a, b) => a - b)
for (let i = 1; i < spacingTokens.length; i++) {
const ratio = spacingTokens[i] / spacingTokens[i - 1]
expect(ratio).toBeGreaterThan(1)
}
})
it('nenhum valor hardcoded em arquivos de componente', () => {
const componentCss = fs.readFileSync('src/components/Button.css', 'utf-8')
const rawColors = [...componentCss.matchAll(/#[0-9a-fA-F]{3,8}/g)]
const rawPx = [...componentCss.matchAll(/:\s*\d+px/g)]
// Arquivos de reset, normalize, bibliotecas externas são excluídos da verificação
expect(rawColors).toEqual([])
expect(rawPx).toEqual([])
})
it('tokens de modo escuro cobrem todos os tokens de modo claro', () => {
const lightNames = tokens
.filter(([, name]) => !name.includes('dark'))
.map(([, name]) => name.replace('light-', ''))
const darkNames = tokens
.filter(([, name]) => name.includes('dark'))
.map(([, name]) => name.replace('dark-', ''))
const missing = lightNames.filter(n => !darkNames.includes(n))
expect(missing).toEqual([])
})
})
Passo 4: Geração de Teste de Acessibilidade
// tests/design/a11y.test.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
const pages = [
{ name: 'Home', path: '/' },
{ name: 'Configurações', path: '/settings' },
// Lista de rotas detectadas no Passo 1
]
for (const page of pages) {
test.describe(`${page.name} Acessibilidade`, () => {
test('deve passar axe-core WCAG 2.2 AA', async ({ page: p }) => {
await p.goto(page.path)
const results = await new AxeBuilder({ page: p })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze()
expect(results.violations).toEqual([])
})
test('todos os elementos interativos têm nomes acessíveis', async ({ page: p }) => {
await p.goto(page.path)
const buttons = await p.locator('button, [role="button"]').all()
for (const btn of buttons) {
const name = await btn.getAttribute('aria-label')
?? await btn.textContent()
expect(name?.trim()).toBeTruthy()
}
})
test('contraste de cor atende proporção 4.5:1', async ({ page: p }) => {
await p.goto(page.path)
const results = await new AxeBuilder({ page: p })
.withRules(['color-contrast'])
.analyze()
expect(results.violations).toEqual([])
})
test('alvos de toque têm pelo menos 24x24px', async ({ page: p }) => {
await p.setViewportSize({ width: 375, height: 812 })
await p.goto(page.path)
const targets = await p.locator(
'button, a, input, select, [role="button"], [role="link"]'
).all()
for (const target of targets) {
const box = await target.boundingBox()
if (box) {
expect(box.width).toBeGreaterThanOrEqual(24)
expect(box.height).toBeGreaterThanOrEqual(24)
}
}
})
})
}
// Acessibilidade do modo escuro (quando tokens têm modo escuro)
test('modo escuro passa contraste de cor', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' })
await page.goto('/')
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze()
expect(results.violations).toEqual([])
})
Passo 5: Geração de Teste de Regressão Visual
// tests/design/visual-regression.test.ts
import { test, expect } from '@playwright/test'
const viewports = [
{ name: 'mobile', width: 375, height: 812 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1440, height: 900 },
]
const pages = [
{ name: 'Home', path: '/' },
// Páginas principais detectadas no Passo 1
]
for (const vp of viewports) {
for (const pg of pages) {
test(`${pg.name} - ${vp.name} snapshot visual`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await page.goto(pg.path)
await page.waitForLoadState('networkidle')
// Gotcha 11 — Asserção de existência de conteúdo para evitar que renderização vazia passe ao coincidir com o baseline.
// Deve ser executada "antes" da comparação de snapshot para também evitar que uma tela em branco se fixe como baseline.
const main = page.locator('main, [role="main"]').first()
await expect(main).toBeVisible()
const box = await main.boundingBox()
expect(box?.height ?? 0).toBeGreaterThan(0)
await expect(page).toHaveScreenshot(
`${pg.name}-${vp.name}.png`,
{ maxDiffPixelRatio: 0.01 }
)
})
}
}
// Quando houver Storybook — teste visual por componente
// Esta seção é gerada apenas quando Storybook for detectado
Validação de mudança parcial (escopada) — Ao verificar uma alteração que muda apenas uma propriedade específica de um elemento, separe o alvo e a área circundante (Gotcha 12). Se o snapshot da área circundante falhar, significa que uma área não intencional mudou, portanto é tratado como regressão.
// tests/design/scoped-change.test.ts
import { test, expect } from '@playwright/test'
test.describe('Mudança visual escopada — alvo muda, área circundante imutável', () => {
test('Snapshot do elemento alvo (mudança permitida)', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('target-card'))
.toHaveScreenshot('target-card.png', { maxDiffPixelRatio: 0.01 })
})
// A área circundante "não deve" mudar, portanto use um threshold mais rigoroso que o do alvo.
// No entanto, conforme Gotcha 4, não use 0 — sempre falhará devido a diferenças de anti-aliasing e renderização de fonte.
test('Snapshot da área circundante (mudança proibida — falha indica regressão)', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('surrounding-panel'))
.toHaveScreenshot('surrounding-panel.png', { maxDiffPixelRatio: 0.001 })
})
})
Passo 5-b: Geração de Teste de Propagação de Decisão (apenas quando .design/decisions.yaml existir)
Se o manifesto não existir, pule esta etapa, mas relate como "ausência de manifesto" e não como "não se aplica" (da mesma forma que o verificador retorna NO_MANIFEST + exit 3 — zero alvos é diferente de aprovado).
Se o manifesto existir, para cada decision_id com status: approved, percorra required_surfaces[] e gere 1 teste por superfície. Decisões com status: superseded (substituída) não geram testes — a posição dessa superfície é assumida pela decisão approved apontada por superseded_by. O esquema e a regra de cobertura 4 são definidos em ../../references/visual-change-protocol.md §6 e não são redefinidos aqui. As regras de geração são três:
- Copie
route_or_entry·state·viewport_or_containerda superfície diretamente para a configuração do teste. - Execute
assertions[]antes da comparação golden — para evitar que uma renderização vazia se fixe como baseline. - Use
selectors[]para o snapshot do locator alvo e o exterior para o snapshot da área circundante (emparelhado com Gotcha 12).
// tests/design/decision-DEC-20260813-001.test.ts — gerado a partir do manifesto
import { test, expect } from '@playwright/test'
test('DEC-20260813-001 → dashboard.desktop.main', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 }) // viewport_or_container
await page.goto('/dashboard') // route_or_entry
// assertions[] — antes da comparação golden (Gotcha 11·14)
const main = page.locator('main').first()
await expect(main).toBeVisible()
await expect(page.locator("[data-surface='group-list'] > *")).not.toHaveCount(0)
expect((await main.boundingBox())?.height ?? 0).toBeGreaterThan(0)
// golden — só tem significado após passar pelas asserções acima
await expect(main).toHaveScreenshot(
'DEC-20260813-001/dashboard.desktop.main.png', { maxDiffPixelRatio: 0.01 }
)
})
Após a geração, execute o verificador de cobertura (§6) e verifique o código de saída 0. Código de saída 2 (SCHEMA_ERROR) significa que a forma da entrada está incorreta e não foi possível julgar; mesmo que violations=0 seja exibido, não deve ser lido como aprovado. Código de saída 3 (NO_SURFACE · NO_DECISION) relata zero alvos, como NO_MANIFEST. Se houver uma superfície com golden apenas e assertions vazios, é FAIL, e o fato de o arquivo de teste ter sido criado não substitui a cobertura.
Superfícies listadas em excluded_surfaces não geram testes, mas devem ser enumeradas no relatório com a justificativa.
Passo 6: Geração de Teste de Layout Responsivo
// tests/design/responsive.test.ts
import { test, expect } from '@playwright/test'
// Usar breakpoints do sistema de design do projeto
const breakpoints = [
{ name: 'sm', width: 640 },
{ name: 'md', width: 768 },
{ name: 'lg', width: 1024 },
{ name: 'xl', width: 1280 },
]
test.describe('Layout Responsivo', () => {
for (const bp of breakpoints) {
test(`sem overflow horizontal em ${bp.name} (${bp.width}px)`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: 900 })
await page.goto('/')
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth)
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth)
…
[truncado para tradução]
Nota: O conteúdo foi truncado conforme solicitado.