Communitygithub.com

joom6077/design-test

Analisa código UI e tokens de design para gerar automaticamente testes de qualidade de design.

O que é design-test?

Esta skill automatiza a geração de testes de qualidade de design para projetos front-end, detectando a pilha de tecnologia (React, Vue, Svelte, Angular, HTML estático) e o sistema de design (tokens, Storybook, Tailwind, CSS-in-JS) do projeto. Ela produz testes de consistência de tokens, acessibilidade (WCAG 2.2 AA com axe-core), regressão visual (Playwright com snapshots), testes responsivos (usando breakpoints do projeto) e testes de modo escuro. A skill inclui validações rigorosas para evitar falsos positivos: exige thresholds de snapshots, verificação de conteúdo existente antes da comparação visual, e testes de controle negativo para confirmar que a regressão visual está funcionando. Também gera testes de propagação de decisão se um manifesto `.design/decisions.yaml` estiver presente, garantindo que as decisões de design aprovadas sejam refletidas em todas as superfícies necessárias. Suporta os frameworks Playwright + axe-core para testes de acessibilidade e Playwright para testes visuais e responsivos.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/joo6077/claude-plugins/tree/HEAD/design-kit/skills/design-test

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Gotchas

  1. 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.
  2. 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
  3. Verificar a versão do ruleset do axe-core — Ao usar @axe-core/playwright ou axe-core, verifique o ruleset da versão instalada no projeto. Há diferenças de regras entre versões principais.
  4. 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.
  5. Definir claramente o escopo de detecção de valores hardcoded na validação de tokens — Valores brutos como #ff0000, 16px, 400 em CSS não significam necessariamente FALHA. Código de reset/normalize CSS e bibliotecas externas devem ser excluídos.
  6. 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.
  7. 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: dark media query ou alternância de classe de token.
  8. 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.
  9. 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
  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-snapshots nã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.
  11. 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.
  12. 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.
  13. prefers-reduced-motion: reduce nã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 em transform: scale/translate foram removidas ou substituídas por opacity/dissolve. Fonte: MDN prefers-reduced-motion (Baseline 2020-01).
  14. 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 em excluded_surfaces sem 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.
  15. 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_visible tê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 em harness/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çãoPilhaFerramenta de Teste
package.json + ReactReactPlaywright + axe-core + Storybook (opcional)
package.json + VueVuePlaywright + axe-core
package.json + SvelteSveltePlaywright + axe-core
package.json + AngularAngularPlaywright + axe-core
pubspec.yaml + FlutterFlutter→ redirecionar para skill flutter-test
*.html (site estático)HTML/CSSPlaywright + 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 AlvoTipo de Teste
Arquivo de token, configuração de tematoken (validação de consistência)
Componente, páginaa11y (acessibilidade) + visual (regressão visual)
Layout, graderesponsive (responsivo)
Projeto inteiroTodos 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:

  1. Copie route_or_entry · state · viewport_or_container da superfície diretamente para a configuração do teste.
  2. Execute assertions[] antes da comparação golden — para evitar que uma renderização vazia se fixe como baseline.
  3. 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.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Habilidades Relacionadas