Contract testing: Storybook test runner, addon a11y, Chromatic visual
8 min de leitura
Sua DS tem <Button>, <Card>, tokens, MDX,
Changesets, e o consumidor instalou. Mas tem um
buraco: como você garante que o <Button>
que você publicou é o mesmo <Button> que o
consumidor está vendo? Mudou o border e ninguém
percebeu. Mudou a posição do ícone e o consumidor
reclamou. O axe-core flagou um aria-label
faltando e o PR passou sem review de a11y.
O "contrato" de um componente é a combinação de:
- API - props, eventos, tipos (coberto por TS).
- Comportamento - interações, estados,
callbacks (coberto por testes -
playfunction). - Visual - aparência em diferentes estados (coberto por visual regression - Chromatic).
- Acessibilidade - ARIA, foco, keyboard (coberto por axe no Storybook).
"Contract testing" no contexto de DS é garantir
esses 4 aspectos automaticamente, em CI, pra
cada PR. Não substitui testes de unidade (que
são da DS - cobertos pela testing-frontend), mas
complementa com foco em componente isolado
vs fluxo de usuário.
Nota sobre o nome: "Contract testing" em arquitetura de microsserviços significa outra coisa (Pact - contrato HTTP entre serviços). Aqui significa "contrato da API do componente" (props + eventos + visual). A trilha usa o nome da issue #46 mas explicita o significado no editorial-decisions.
O essencial 🟢
Storybook test runner - testes das stories em
browser real. O @storybook/test-runner é
Playwright embarcado (config zero) que roda
cada story como teste:
pnpm add -D @storybook/test-runner
// package.json
{
"scripts": {
"test-storybook": "test-storybook"
}
}
# Buildar Storybook primeiro
pnpm storybook build
# Rodar test-runner contra o build
pnpm test-storybook
Sem mais config, ele:
- Sobe o Storybook estático (do
build). - Pra cada story em cada
*.stories.ts:- Renderiza.
- Roda a
playfunction (se houver). - Verifica que renderizou sem erro.
- Reporta falhas.
A mágica é que stories viram testes sem
escrever teste separado. Sua story
"MyButton com loading" garante que a story
renderiza, e se você adicionar play: async ({ canvasElement }) => { userEvent.click(...) },
vira teste de interação dentro do Storybook.
play function - testes de interação. A
função play é executada pelo test runner
(também manualmente, no "play" da story). Use
pra simular usuário e verificar estado:
import { expect, fn, userEvent, within } from "@storybook/test";
export const Clickable: Story = {
args: {
children: "Salvar",
onClick: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const botao = canvas.getByRole("button", { name: /salvar/i });
// Verifica estado inicial
expect(botao).toBeEnabled();
// Simula clique
await userEvent.click(botao);
// Verifica que onClick foi chamado
await expect(args.onClick).toHaveBeenCalledTimes(1);
},
};
@storybook/test (vem com Storybook 8+) re-exporta
@testing-library/jest-dom e user-event. Você
pode usar toda a API do Testing Library dentro
de uma story (visto na testing-frontend).
O test runner roda todas as stories. Cada
export const X: Story vira um teste. Story com
play é teste de interação. Story sem play
é teste de render (apenas verifica que renderiza
sem erro).
// Esta story vira um teste "renders without crashing"
export const Primary: Story = {
args: { variant: "primary", children: "Salvar" },
};
// Esta story vira um teste de clique + assertion
export const Clickable: Story = {
args: { onClick: fn() },
play: async ({ canvasElement, args }) => {
// ...
},
};
Em uma DS com 50 stories, são 50 testes "automáticos" sem escrever código de teste extra.
Addon a11y: axe-core em cada story. O
@storybook/addon-a11y adiciona um painel
de acessibilidade que roda axe-core na story
ativa. Você vê em tempo real se a story viola
WCAG:
pnpm add -D @storybook/addon-a11y
// .storybook/main.ts
export default {
addons: [
"@storybook/addon-essentials",
"@storybook/addon-a11y",
],
};
Agora cada story mostra um painel "Accessibility" com:
- Quantidade de violações por severidade.
- Lista de regras violadas.
aria-*attributes esperados vs atuais.- "Highlight" dos elementos com problema.
Em CI, o test runner também roda axe-core
automaticamente em cada story (via
addon-a11y). Story com aria-invalid faltando
= teste falhando. Sem código extra de teste.
// Customizar regras do axe
export const Primary: Story = {
parameters: {
a11y: {
// Aplica só regras WCAG 2 AA
config: {
rules: [
{ id: "color-contrast", enabled: true },
{ id: "label", enabled: true },
],
},
},
},
};
O padrão recomendado é: rodar todas as
regras WCAG 2 AA por default, e marcar
disableRules pontual com comentário explicando
por quê.
Chromatic: visual regression do DS. Chromatic é a ferramenta hosted de visual regression do time do Storybook. A cada PR, ele:
- Builda o Storybook.
- Tira screenshot de cada story em cada viewport configurado.
- Compara com a baseline (aprovada em PR anterior).
- Comenta no PR com o diff visual inline.
- Você aprova ou pede mudanças.
pnpm add -D chromatic
// package.json
{
"scripts": {
"chromatic": "chromatic --project-token=<token>"
}
}
# .github/workflows/chromatic.yml
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm exec chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }} --auto-accept-changes main
Cada PR recebe um bot do Chromatic que posta:
🟡 UI Test (12 stories, 3 viewports)
12 stories, 3 viewports checked.
1 change detected:
- Button.stories > Primary > Desktop
[View changes] [Approve] [Request changes]
Você clica em "View changes" e vê o diff visual (browser-like). Aprovou? Merge. Rejeitou? DS atualiza a story (ou corrige o bug).
Baseline visual e cross-browser. A força do Chromatic:
- Baseline versionada - cada PR aprovado vira a nova baseline. Mudou a história = tem que aprovar de novo.
- Multi-viewport - testa em 3-4 viewports (desktop, tablet, mobile).
- Multi-browser - opcional, roda em Chromium, Firefox, WebKit.
- Histórico - cada PR tem um diff. Você pode voltar e ver "quando essa cor mudou?"
Em time de 5 devs, isso economiza horas por PR de "teste visual manual" - e pega mudanças que dev não vê (pixel-level em botão cinza vs preto).
O que Chromatic NÃO é. Pra evitar confusão:
- Não substitui testes de comportamento - use o test runner pra isso.
- Não substitui testes E2E da app - a
testing-frontendcobre isso. - Não substitui review humano - o Chromatic mostra o diff, mas a decisão ("essa mudança é boa?") é humana.
Chromatic captura mudanças visuais. A decisão é do time.
Testes cross-browser no Storybook. O
@storybook/test-runner aceita config de
Playwright (que tem cross-browser nativo):
// .storybook/test-runner.ts
import { getStorybookConfig } from "@storybook/test-runner";
export default {
...getStorybookConfig(),
// Adiciona Playwright projects
playwright: {
projects: [
{ name: "chromium", use: { browserName: "chromium" } },
{ name: "firefox", use: { browserName: "firefox" } },
{ name: "webkit", use: { browserName: "webkit" } },
],
},
};
Agora cada story roda em 3 browsers. Mais lento, mas pega bug específico de browser (Safari é notório pra isso).
Snapshot de DOM vs visual. Em DS, evite snapshot de DOM (Jest/Vitest snapshot do HTML gerado). Razões:
- Quebra a cada mudança estrutural irrelevante (classe CSS, ordem de atributos).
- Não pega mudança visual (cor mudou, classe é a mesma).
- É "theater" (time aceita tudo sem ler).
Use visual regression (Chromatic) pra detectar mudança visual. Use **test runner
- axe** pra detectar mudança de comportamento e a11y. Snapshot de DOM é redundante com os dois.
Aprofundamento 🟡
O "contrato" completo de um componente. Pra cada componente, o contrato é:
- API (TypeScript):
- Props com tipo, descrição, default.
- Eventos (onClick, onChange, etc).
- Refs (forwardRef).
- Genéricos (para componente de list/select).
- Comportamento (test runner):
- Renderiza sem erro.
- Interação (play function) - clique, digitação, navegação por teclado.
- Estados extremos (loading, disabled, error, empty).
- Edge cases (texto muito longo, ícone sem label, children null).
- Visual (Chromatic):
- Aparência em cada state.
- Aparência em cada viewport.
- Hover, focus, active, disabled (CSS state).
- Light/dark.
- Acessibilidade (addon a11y):
- ARIA correto.
- Keyboard navigation.
- Foco visível.
- Screen reader (axe + manual).
Quanto mais desses você automatiza em CI, menos dependência de review manual. O "contrato" vira código, e PR que quebra o contrato é bloqueado.
Configurar Chromatic pra cross-browser. Por default, Chromatic roda em Chromium (Chrome). Pra rodar em Firefox e WebKit (Safari):
pnpm exec chromatic --project-token=... --browsers=chromium,firefox,webkit
Custo: cada browser dobra o número de screenshots. Para DS pequeno (50 stories), é 3-5x mais screenshots = 3-5x mais tempo. Em DS grande (200+ stories), é proibitivo.
A regra: Chromium only em CI rápido, e cross-browser em release/milestone (manual ou via trigger diferente). Em time grande, publica uma versão preview cross-browser antes do release final.
Combinar test runner com CI do npm publish. Em DS com Changesets + GitHub Actions:
- PR abre.
- CI roda:
pnpm lintpnpm typecheckpnpm test:run(Vitest unit)pnpm test-storybook(test runner)pnpm exec chromatic(visual review)
- Se tudo verde, autor mergea.
- Versão PR (ou commit) gera CHANGELOG + bump
- publish no npm.
O test-storybook é o equivalente em DS do que vitest + axe + playwright é em app. É a "rede de segurança" que pega regressão de componente antes de chegar ao consumidor.
parameters.test em stories. Pra organizar
quais stories rodam em que modo:
export const Experimental: Story = {
parameters: {
// Não roda no test runner (manual só)
test: { disable: true },
},
};
export const AllVariants: Story = {
// Roda em CI, mas com timeout maior (renderiza 10 buttons)
parameters: {
test: { timeout: 10000 },
},
};
Útil pra "stories só pra visual review" (não viram teste) e stories lentas (timeout custom).
Snapshot de interação (play function + expect). Pra testes mais ricos:
import { expect, userEvent, within, fn } from "@storybook/test";
export const FormSubmission: Story = {
args: {
onSubmit: fn(),
},
render: () => <Form onSubmit={...} />,
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Preenche form
await userEvent.type(canvas.getByLabelText(/email/i), "ana@x.com");
await userEvent.type(canvas.getByLabelText(/senha/i), "12345678");
// Submete
await userEvent.click(canvas.getByRole("button", { name: /entrar/i }));
// Verifica callback
await expect(args.onSubmit).toHaveBeenCalledWith({
email: "ana@x.com",
senha: "12345678",
});
},
};
A play function roda Playwright + Testing
Library, com await. É a "rede de segurança"
mais forte que existe: simula o usuário, verifica
resultado. Tudo isso dentro de uma story.
Custom matcher pra a11y. Pra ter regras de axe customizadas por componente:
// .storybook/preview.ts
export default {
parameters: {
a11y: {
// Para componentes interativos: regras WCAG 2.1 AA
config: {
runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] },
},
},
},
};
Sem customização, axe roda todas as regras (80+), incluindo algumas experimentais. Em DS, o padrão é WCAG 2.1 AA (o mínimo legal em muitos países). Mais restrito = menos ruído.
Chromatic + percy (comparação). Chromatic é a recomendação default (time Storybook). Percy (BrowserStack) é alternativa com SDK similar. Comparação:
- Chromatic - integrado com Storybook, Turbopack/Vite/Webpack. Self-hostable opcional. Pago (free tier limitado).
- Percy - SDK pra várias plataformas (Playwright, Cypress, Selenium). Cross-browser real (não Chromium só). Pago.
Em DS React, Chromatic é a escolha. A integração é nativa.
Pra quem quer ir além 🔴
Storybook test runner vs Vitest + Testing
Library. A testing-frontend cobre Vitest +
Testing Library. O test runner do Storybook
faz o mesmo mas dentro do Storybook.
Quando usar um vs outro?
- Vitest + Testing Library (sem Storybook) - para componentes internos da app, que não viram DS. Lógica de negócio, hooks complexos, integração com API.
- Storybook test runner (com Storybook) - para componentes da DS (que viram DS). Cobertura visual, a11y, cross-browser.
Em DS, o test runner é a escolha - porque você já tem Storybook, e a story já é o "ambiente de teste" do componente.
@storybook/test-runner + play function =
a "rede de segurança" do DS. Em DS maduro, o
test runner é bloqueador de merge. PR que:
- Quebra render de uma story = fail.
- Quebra play function = fail.
- Adiciona violação de a11y = fail.
- Muda visual sem aprovação Chromatic = block.
O custo: test runner com 200 stories, 3 browsers = 5-10 min de CI. Em DS grande, vale o investimento (pouca coisa escapa). Em DS pequeno (< 50 stories), 1-2 min, ainda vale.
Chromatic publish preview - workflow de review visual. Em time com designer:
- Dev abre PR com mudança no
<Button>. - CI roda tests + Chromatic.
- Chromatic posta bot no PR com "1 change detected".
- Designer revisa o diff visual, aprova ou pede mudança.
- Dev mergeia.
- Chromatic atualiza baseline.
Designer como reviewer de PR visual - sem o dev precisar tirar screenshot, anexar, pedir review. Muda a cultura do time: visual review é parte do PR, não "depois a gente vê".
@storybook/blocks pra doc rica em MDX. Em
DS, MDX usa @storybook/blocks (Canvas, Controls,
ArgsTable) pra render interativo dentro da doc
(visto no nó 5). O test runner pode ser
configurado pra rodar as stories referenciadas
no MDX - garante que o exemplo no MDX
funciona.
Leitura recomendada:
- Storybook - Test runner - a doc oficial.
- Storybook - Accessibility addon - axe no Storybook.
- Chromatic - Visual testing - o serviço hosted de visual review.
Dica: o erro mais comum no começo é pular test runner e Chromatic porque "testes de unidade cobrem". Não cobrem. Unit test da DS testa a função do componente (callback, render), não o visual (cor mudou?) nem a a11y (axe flagou?). Os 3 juntos (unit + test runner + Chromatic) é a rede de segurança completa.
No próximo nó (e último), vamos unir tudo no projeto final: 5+ componentes com tokens, theming, Storybook, test runner + a11y, Changesets, e publicação no npm.
// Quiz
Qual a relacao entre Storybook test runner e Chromatic visual review?