Pruebas de frontend
Prueba lo que el usuario ve y hace, no cómo está construido el componente. Combínalo con naki-tdd para el ciclo de desarrollo, naki-e2e-playwright para flujos completos del navegador y naki-accessibility para semántica. Sigue el skill de reglas del proyecto si existe (por ejemplo naki-myapp).
Cuándo usarlo
- Se necesitan pruebas para un nuevo componente, hook o página.
- Una prueba falla después de una refactorización que no cambió el comportamiento (señal de acoplamiento a implementación).
- Las pruebas son inestables, usan
waitForen todas partes o simulan la mitad de la aplicación.
Reglas principales
- Consulta como un usuario. Prioridad:
getByRole(conname),getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. UsagetByTestIdsolo cuando no exista nada accesible. - Interactúa con
userEvent, no confireEvent. Créalo conconst user = userEvent.setup()y espera (await) cada llamada. - Verifica la salida: texto visible, roles, valores de formularios, llamadas a un endpoint de red. Nunca verifiques variables de estado, detalles internos de hooks, nombres de clases CSS o métodos de instancia del componente.
- Simula la red, no tus módulos. Usa manejadores de MSW en la capa HTTP. Simula un módulo solo para límites externos reales (tiempo, aleatoriedad, APIs del navegador que no están en jsdom).
- Usa
findBy*para cosas que aparecen después. ReservawaitForpara verificaciones que no son una consulta, y nunca pongas efectos secundarios dentro dewaitFor. - Un comportamiento por prueba, nombrado como una oración:
muestra un error cuando el servidor devuelve 500. - Cada prueba configura su propio estado. Sin dependencia de orden; restablece manejadores, temporizadores y
almacenamiento en
afterEach. - Proporciona proveedores reales a través de un helper
renderWithProviders(router, query client con reintentos desactivados, tema) en lugar de simularlos. - La accesibilidad es comprobable. Un
getByRole("button", { name: "Guardar" })que falla a menudo significa un error real de accesibilidad. Opcionalmente, añade comprobaciones devitest-axepara pantallas clave. - La cobertura es una señal, no un objetivo. Cubre las ramas que importan: estados de error, estados vacíos, permisos.
Hoja de consultas
| Elemento | Consulta |
|---|---|
| botón | getByRole("button", { name: /guardar/i }) |
| enlace | getByRole("link", { name: "Documentos" }) |
| entrada de texto con etiqueta | getByRole("textbox", { name: "Correo" }) o getByLabelText("Correo") |
| casilla de verificación | getByRole("checkbox", { name: "Recordarme" }) |
| encabezado | getByRole("heading", { level: 2, name: "Alertas" }) |
| fila de tabla | getAllByRole("row") luego within(row) |
| diálogo | getByRole("dialog", { name: "Eliminar alerta" }) |
| ausencia | expect(queryByText("Error")).not.toBeInTheDocument() |
Flujo de trabajo
- Enumera los comportamientos: estados de renderizado (carga, vacío, datos, error) e interacciones.
- Añade manejadores de MSW para los endpoints que llama el componente; por defecto, devuelven éxito.
- Escribe una prueba por comportamiento, sobrescribiendo manejadores con
server.use(...)para casos de error. - Ejecuta
npx vitest run ruta/al/archivo.test.tsx, luego toda la suite. - Rompe la implementación a propósito una vez (comenta una línea) para confirmar que la prueba puede fallar.
Configuración
// 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" hace que una solicitud no manejada falle de forma notoria en lugar de quedarse colgada.
Ejemplo
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 y filtra por severidad", async () => {
server.use(http.get("/api/alerts", () => HttpResponse.json([
{ id: "a1", title: "Ataque de fuerza bruta en vpn", severity: "high" },
{ id: "a2", title: "Nuevo inicio de sesión de administrador", severity: "low" },
])));
const user = userEvent.setup();
render(<AlertList />);
expect(await screen.findByText("Ataque de fuerza bruta en vpn")).toBeInTheDocument();
await user.selectOptions(screen.getByRole("combobox", { name: "Severidad" }), "high");
expect(screen.queryByText("Nuevo inicio de sesión de administrador")).not.toBeInTheDocument();
});
test("muestra un botón de reintento cuando la carga falla", async () => {
server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
render(<AlertList />);
expect(await screen.findByRole("button", { name: /reintentar/i })).toBeInTheDocument();
});
Hooks y tiempo
- Prueba un hook a través de un componente pequeño que lo use, o con
renderHookcuando no tenga UI. - Temporizadores falsos:
vi.useFakeTimers()yuserEvent.setup({ advanceTimers: vi.advanceTimersByTime }); restáuralos convi.useRealTimers()enafterEach. - Congela fechas con
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"))para que las instantáneas y etiquetas sean estables.
Lista de verificación
- Las consultas están basadas en roles o etiquetas.
- Todas las llamadas a
userEventtienen await. - Red simulada con MSW; las solicitudes no manejadas fallan.
- Estados de carga, error y vacío cubiertos.
- Sin verificaciones de detalles internos o nombres de clases.
- Las pruebas pasan solas y en cualquier orden.
- Sin advertencias de
act(...)en la salida.
Errores comunes
| Error | Solución |
|---|---|
getByTestId en todas partes | consultas de rol/etiqueta; añade etiquetas al componente si faltan |
await waitFor(() => getByText("x")) | await findByText("x") |
fireEvent.change para escribir | await user.type(input, "texto") |
Simular el hook useAlerts directamente | simula el endpoint HTTP |
| Instantáneas grandes de páginas completas | verificaciones explícitas sobre lo que importa |
| Query client con reintentos activados, pruebas de error lentas | retry: false en el proveedor de prueba |
Advertencias act ignoradas | espera (await) la actualización de UI asíncrona con findBy* |