# Regla: Testing — Next.js

Probar Next.js con App Router requiere estrategia porque existen tres tipos de
código distintos: Server Components (async, sin estado de React), Client Components
(con hooks y eventos) y Server Actions (funciones del servidor invocadas desde el cliente).
Esta regla define cómo cubrir cada uno.

---

## Stack de testing

| Herramienta | Propósito |
|-------------|-----------|
| **Vitest** | Test runner para unit tests y componentes. Reemplaza Jest en proyectos nuevos. |
| **React Testing Library** | Renderizado y aserción de componentes. |
| **Playwright** | Tests end-to-end con browser real. |
| **MSW (Mock Service Worker)** | Mock de API en tests de integración. |

- Configuración de Vitest con soporte de React:
  ```ts
  // vitest.config.ts
  import { defineConfig } from 'vitest/config'
  import react from '@vitejs/plugin-react'

  export default defineConfig({
    plugins: [react()],
    test: {
      environment: 'jsdom',
      globals: true,
      coverage: {
        provider: 'v8',
        thresholds: { lines: 80, functions: 80, branches: 80 },
      },
    },
  })
  ```

---

## Cobertura mínima: 80%

```bash
vitest run --coverage
```

- CI falla si algún umbral (líneas, funciones, ramas) cae por debajo del 80%.
- La cobertura aplica sobre `components/`, `lib/`, `hooks/` y `actions/`.
  No aplica sobre `app/**/page.tsx` ni `app/**/layout.tsx` (solo orquestan).

---

## Testing de Server Components

Los Server Components son funciones `async` que retornan JSX. Se testean
renderizando el resultado de la función directamente:

```tsx
// components/ResumenFactura.tsx
export async function ResumenFactura({ facturaId }: { facturaId: number }) {
  const factura = await obtenerFactura(facturaId)
  return (
    <article>
      <h2>{factura.folio}</h2>
      <p>Total: ${factura.total}</p>
    </article>
  )
}

// components/ResumenFactura.test.tsx
import { render, screen } from '@testing-library/react'
import { ResumenFactura } from './ResumenFactura'

vi.mock('@/lib/facturas', () => ({
  obtenerFactura: vi.fn().mockResolvedValue({
    id: 1,
    folio: 'FAC-00001',
    total: 1160.0,
  }),
}))

it('muestra el folio y total de la factura', async () => {
  // Arrange + Act
  const componente = await ResumenFactura({ facturaId: 1 })
  render(componente)

  // Assert
  expect(screen.getByText('FAC-00001')).toBeInTheDocument()
  expect(screen.getByText('Total: $1160')).toBeInTheDocument()
})
```

---

## Testing de Client Components con React Testing Library

```tsx
// components/FormularioLogin.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FormularioLogin } from './FormularioLogin'

describe('FormularioLogin', () => {
  it('muestra error cuando el email tiene formato inválido', async () => {
    // Arrange
    const usuario = userEvent.setup()
    render(<FormularioLogin />)

    // Act
    await usuario.type(screen.getByLabelText('Correo electrónico'), 'no-es-email')
    await usuario.click(screen.getByRole('button', { name: /iniciar sesión/i }))

    // Assert
    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent('Correo inválido')
    })
  })
})
```

---

## MSW para mock de API en integración

```ts
// tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/facturas', () => {
    return HttpResponse.json({
      data: [{ id: 1, folio: 'FAC-001', total: 580.0 }],
      meta: { total: 1, page: 1, per_page: 20 },
    })
  }),
]

// tests/setup.ts
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'

export const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```

---

## Playwright para E2E

```ts
// e2e/facturas.spec.ts
import { test, expect } from '@playwright/test'

test('usuario puede emitir una factura', async ({ page }) => {
  // Arrange — iniciar sesión
  await page.goto('/login')
  await page.fill('[name="email"]', 'admin@empresa.com')
  await page.fill('[name="password"]', 'password-de-test')
  await page.click('button[type="submit"]')

  // Act
  await page.goto('/facturas/nueva')
  await page.selectOption('[name="clienteId"]', '1')
  await page.click('button:has-text("Emitir factura")')

  // Assert
  await expect(page.locator('[data-testid="folio-factura"]')).toBeVisible()
  await expect(page.locator('[data-testid="estatus-factura"]')).toHaveText('Emitida')
})
```

---

## Snapshot testing: solo componentes estables

- Los snapshots de componentes se desactualizan con cada cambio de UI y generan
  ruido en los diffs. Usar solo para componentes de diseño muy estables.
- Preferir aserciones explícitas con `getByRole`, `getByText`, `getByLabelText`.
- Si se usa snapshot: revisar el diff antes de hacer `--updateSnapshot`. No aceptar
  ciegamente.

---

## Checklist de testing antes de merge

- [ ] `vitest run --coverage` pasa con umbral 80%
- [ ] Server Components testeados con mock de sus dependencias de datos
- [ ] Client Components testeados con React Testing Library y `userEvent`
- [ ] MSW configurado para mocks de API en tests de integración
- [ ] Al menos 1 test E2E con Playwright por flujo crítico nuevo
- [ ] Sin `setTimeout` ni `sleep` en tests (usar `waitFor` de RTL)
- [ ] Snapshot tests solo en componentes explícitamente marcados como estables
