Testes frontend
Teste o que o usuário vê e faz, não como o componente é construído. Combine com naki-tdd para o ciclo,
naki-e2e-playwright para fluxos completos no navegador e naki-accessibility para semântica. Siga a skill de regras do projeto se houver uma (por exemplo naki-myapp).
Quando usar
- Um novo componente, hook ou página precisa de testes.
- Um teste quebra após uma refatoração que não alterou o comportamento (sinal de acoplamento de implementação).
- Testes estão instáveis, usam
waitForem todo lugar ou simulam metade do aplicativo.
Regras principais
- Faça consultas como um usuário. Prioridade:
getByRole(comname),getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. UsegetByTestIdapenas quando nada acessível existir. - Interaja com
userEvent, não comfireEvent. Crie-o comconst user = userEvent.setup()e useawaitem toda chamada. - Afirme sobre a saída: texto visível, funções (roles), valores de formulário, chamadas a um endpoint de rede. Nunca afirme sobre variáveis de estado, detalhes internos de hooks, nomes de classes CSS ou métodos de instância do componente.
- Simule a rede, não seus módulos. Use handlers do MSW na camada HTTP. Simule um módulo apenas para limites verdadeiramente externos (tempo, aleatoriedade, APIs do navegador não presentes no jsdom).
- Use
findBy*para coisas que aparecem depois. ReservewaitForpara asserções que não são uma consulta, e nunca coloque efeitos colaterais dentro dewaitFor. - Um comportamento por teste, nomeado como uma frase:
mostra um erro quando o servidor retorna 500. - Cada teste configura seu próprio estado. Sem dependência de ordem; redefina handlers, temporizadores e armazenamento em
afterEach. - Forneça providers reais através de um helper
renderWithProviders(roteador, query client com retries desativados, tema) em vez de simulá-los. - Acessibilidade é testável. Uma falha em
getByRole("button", { name: "Salvar" })geralmente significa um bug de acessibilidade real. Opcionalmente, adicione verificaçõesvitest-axepara telas principais. - Cobertura é um sinal, não uma meta. Cubra ramos que importam: estados de erro, estados vazios, permissões.
Folha de consulta
| Elemento | Consulta |
|---|---|
| botão | getByRole("button", { name: /salvar/i }) |
| link | getByRole("link", { name: "Docs" }) |
| entrada de texto com rótulo | getByRole("textbox", { name: "Email" }) ou getByLabelText("Email") |
| checkbox | getByRole("checkbox", { name: "Lembrar de mim" }) |
| título | getByRole("heading", { level: 2, name: "Alertas" }) |
| linha de tabela | getAllByRole("row") then within(row) |
| diálogo | getByRole("dialog", { name: "Excluir alerta" }) |
| ausência | expect(queryByText("Erro")).not.toBeInTheDocument() |
Fluxo de trabalho
- Liste os comportamentos: estados de renderização (carregando, vazio, dados, erro) e interações.
- Adicione handlers MSW para os endpoints que o componente chama; padrão para sucesso.
- Escreva um teste por comportamento, sobrescrevendo handlers com
server.use(...)para casos de erro. - Execute
npx vitest run caminho/para/arquivo.test.tsx, depois toda a suíte. - Quebre a implementação de propósito uma vez (comente uma linha) para confirmar que o teste pode falhar.
Configuração
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./src/test/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());
onUnhandledRequest: "error" faz com que um handler ausente falhe de forma ruidosa em vez de travar.
Exemplo
import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../test/server";
import { AlertList } from "./AlertList";
test("lista alertas e filtra por gravidade", async () => {
server.use(http.get("/api/alerts", () => HttpResponse.json([
{ id: "a1", title: "Força bruta na vpn", severity: "high" },
{ id: "a2", title: "Novo login admin", severity: "low" },
])));
const user = userEvent.setup();
render(<AlertList />);
expect(await screen.findByText("Força bruta na vpn")).toBeInTheDocument();
await user.selectOptions(screen.getByRole("combobox", { name: "Gravidade" }), "high");
expect(screen.queryByText("Novo login admin")).not.toBeInTheDocument();
});
test("mostra um botão de repetir quando o carregamento falha", async () => {
server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
render(<AlertList />);
expect(await screen.findByRole("button", { name: /repetir/i })).toBeInTheDocument();
});
Hooks e tempo
- Teste um hook através de um componente pequeno que o usa, ou com
renderHookquando ele não tem IU. - Temporizadores falsos:
vi.useFakeTimers()euserEvent.setup({ advanceTimers: vi.advanceTimersByTime }); restaure comvi.useRealTimers()emafterEach. - Congele datas com
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"))para que snapshots e rótulos sejam estáveis.
Lista de verificação
- Consultas são baseadas em função (role) ou rótulo.
- Todas as chamadas
userEventcomawait. - Rede simulada com MSW; requisições não tratadas falham.
- Estados de carregamento, erro e vazio cobertos.
- Nenhuma asserção sobre detalhes internos ou nomes de classe.
- Testes passam sozinhos e em qualquer ordem.
- Nenhum aviso
act(...)na saída.
Erros comuns
| Erro | Correção |
|---|---|
getByTestId em todo lugar | consultas por função/rótulo; adicione rótulos ao componente se estiver faltando |
await waitFor(() => getByText("x")) | await findByText("x") |
fireEvent.change para digitar | await user.type(input, "texto") |
Simular o hook useAlerts diretamente | simule o endpoint HTTP |
| Snapshots grandes de páginas inteiras | asserções explícitas sobre o que importa |
| Query client com retries ativados, testes de erro lentos | retry: false no provider de teste |
Avisos act ignorados | aguarde a atualização da UI assíncrona com findBy* |