Pruebas y control de calidad
Qué se prueba, con qué herramienta y qué revisar antes de dar algo por terminado.
Principio
Una función no está terminada porque compile. Está terminada cuando su comportamiento está demostrado, especialmente si toca dinero, tiempo, evidencia o permisos.
Niveles
| Nivel | Herramienta | Contra qué corre | Qué demuestra |
|---|---|---|---|
| Unitario | Vitest | Nada externo; módulos puros | Que las fórmulas son correctas |
| Integración | Vitest | PostgreSQL y almacenamiento reales | Que las invariantes se sostienen |
| Extremo a extremo | Playwright | La aplicación compilada | Que los recorridos completos funcionan |
| Manual | Navegador | La aplicación real | Que la experiencia se siente bien |
pnpm test # unitarios
pnpm test:integration # integración (requiere docker compose up -d)
pnpm test:e2e # recorridos completos
pnpm verify # typecheck + lint + unitariosTests unitarios
134 tests sobre los módulos puros. Sin mocks de Prisma: estos módulos no conocen la base de datos.
| Archivo | Cubre |
|---|---|
modules/finance/money.test.ts | Conversión, parseo ambiguo de separadores, formato, margen |
modules/finance/calculations.test.ts | Ingresos, costos, margen, cifras parciales |
modules/time-tracking/calculations.test.ts | Suma de sesiones, sesión activa, ventana operativa |
modules/payroll/calculations.test.ts | Tarifas, redondeo, costo laboral, nómina por período |
modules/services/status-machine.test.ts | Transiciones válidas e inválidas, reglas por rol |
modules/properties/sensitive.test.ts | Acceso a códigos según estado y ventana de gracia |
lib/datetime.test.ts | Días de negocio, medianoche, formato costarricense |
modules/audit/sanitize.test.ts | Redacción de secretos y serialización de instancias |
Casos que merecen mención
Coma flotante. Se comprueba explícitamente que la aritmética en enteros es exacta donde IEEE-754 falla:
expect(colonesToCentimos(0.1) + colonesToCentimos(0.2))
.toBe(colonesToCentimos(0.3));Medianoche en Costa Rica. Las 23:00 en Costa Rica son las 05:00 UTC del día siguiente. Un test verifica que ese instante pertenece al día de negocio anterior, que es como lo entiende el negocio:
expect(toBusinessDayKey(new Date("2026-09-02T05:00:00Z")))
.toBe("2026-09-01");Los tests corren con TZ=UTC a propósito: si algún cálculo dependiera
implícitamente de la zona del proceso, fallarían.
Retomar el trabajo. 3 h + 20 min = 3 h 20 min, no 20 min.
Tests de integración
74 tests contra PostgreSQL y MinIO reales. No se simula Prisma: el objetivo es precisamente comprobar que las restricciones CHECK, los índices únicos y las transacciones se comportan como esperamos, y eso un mock no lo puede probar.
| Archivo | Cubre |
|---|---|
service-lifecycle.test.ts | Ciclo completo del servicio, snapshots, reapertura, multi-empleado |
authorization.test.ts | Aislamiento entre empleados, datos sensibles, invariantes de la base |
evidence-storage.test.ts | Firmar, subir, confirmar, borrar, expirar contra S3 real |
payroll.test.ts | Cálculo, congelado del recibo, reversión |
service-report.test.ts | Datos del reporte, filtración financiera, paginación del PDF |
supplies.test.ts | Aprobación obligatoria, totales exactos, costos ausentes |
Preparación
docker compose up -ddocker compose exec db psql -U royalclean -d postgres -c "CREATE DATABASE royalclean_test OWNER royalclean;"El arranque aplica las migraciones con prisma migrate deploy —el mismo comando
que corre en producción— para que los tests validen exactamente el esquema real.
Cada test parte de una base vacía.
Casos que merecen mención
El snapshot es inmutable. Se crea un servicio, se cambia el título en la plantilla y se verifica que el servicio conserva el título original.
El pago está congelado. Se registra un pago, luego se duplican las horas y se sube la tarifa, y se verifica que el recibo sigue diciendo lo mismo.
El aislamiento es indistinguible. Un empleado pidiendo el servicio de otro y pidiendo un id inexistente reciben el mismo mensaje de error.
Los códigos no viajan. Cuando el acceso expiró, se comprueba que la clave
doorCode no existe en el objeto, no que esté vacía.
La URL firmada es estricta. Subir con un Content-Type distinto del
autorizado es rechazado por el almacenamiento.
El reporte no filtra. Se buscan la tarifa y el costo interno en los datos serializados del reporte y no deben aparecer.
La base de datos defiende. Se intenta insertar dos sesiones abiertas del mismo empleado, una sesión que termina antes de empezar, un precio negativo y una hora fuera del día. Todas fallan a nivel de motor.
Pruebas de extremo a extremo
26 tests con Playwright sobre la aplicación compilada, no sobre el servidor de desarrollo: éste compila cada ruta la primera vez que se pide y bajo la carga de una suite completa llega a caerse, produciendo fallos que no son del código. Además, así los recorridos se prueban contra el mismo artefacto que se despliega.
Dos proyectos, cada uno con el dispositivo que corresponde:
| Proyecto | Dispositivo | Cubre |
|---|---|---|
escritorio | 1440x900 | Recorridos de administración |
movil | Pixel 7 | Recorridos del personal de campo |
| Archivo | Flujo del §99 |
|---|---|
admin/directorio.spec.ts | 1 - alta de cliente, propiedad y servicio |
admin/revision.spec.ts | 3 y 5 - revisión y nómina |
empleado/servicio.spec.ts | 2 y 4 - trabajo de campo |
empleado/seguridad.spec.ts | 6 - intentos de acceso indebido |
Preparación:
docker compose up -dpnpm db:seedpnpm test:e2eEl servidor se compila y se levanta solo, en el puerto 3101 para no chocar con el de desarrollo.
Casos que merecen mención
El personal no ve importes. Dos tests leen el texto completo de las pantallas de historial y perfil y comprueban que no contienen el símbolo del colón. Es una comprobación tosca a propósito: no depende de saber dónde podría aparecer un importe.
Los códigos llegan tapados. Se verifica que el código real no está en la página hasta pulsar «Mostrar códigos», y que vuelve a desaparecer al ocultarlo.
El 404 es indistinguible. Un servicio ajeno y un identificador inventado producen exactamente la misma pantalla y el mismo código de estado.
El login no delata cuentas. Se comparan los mensajes de error de un usuario inexistente y de uno real con contraseña incorrecta: deben ser idénticos.
Verificación manual
Lo que sigue teniendo sentido comprobar a mano:
Flujo del personal (móvil)
- Iniciar sesión, ver el servicio del día.
- Abrir el servicio: propiedad, dirección, botón de Google Maps.
- Pulsar «Mostrar códigos» y comprobar que aparecen enmascarados antes.
- Iniciar el servicio; el cronómetro corre.
- Abrir una tarea, tomar una foto, ver la barra de progreso.
- Intentar completar sin foto «después»: el botón está deshabilitado y explica por qué.
- Completar todas las tareas y finalizar.
- Retomar el trabajo y comprobar que el tiempo suma.
Flujo de administración
- Crear cliente, propiedad y plantilla de tareas.
- Programar un servicio y asignar personal.
- Revisar el servicio completado: tiempos, evidencia, incidentes, cifras.
- Corregir un horario con motivo y verlo en la auditoría.
- Registrar un pago y abrir el recibo.
- Generar el PDF y revisar que no contiene datos internos.
Tamaños a comprobar
| Dispositivo | Ancho | Qué mirar |
|---|---|---|
| iPhone SE | 375 px | Que nada se desborde |
| iPhone actual | 393 px | Zonas táctiles cómodas |
| Android medio | 412 px | Barra inferior sobre el área segura |
| Tablet | 768 px | Transición de tarjetas a tablas |
| Portátil | 1366 px | Barra lateral y contenido |
| Escritorio | 1920 px | Que el contenido no se estire sin límite |
En ningún tamaño debe existir scroll horizontal del cuerpo de la página.
PWA
- El manifiesto carga sin errores.
- Se puede instalar en Android e iOS.
- Se abre en modo
standalone, sin barra del navegador. - Los atajos llevan a «Hoy» e «Historial».
Aceptación de seguridad
Cubierta por tests de integración:
- Un empleado pidiendo un servicio ajeno recibe 404.
- Un empleado no ve códigos de acceso fuera de su servicio activo.
- Un empleado no puede registrar pagos.
- Una URL de R2 sin firma es rechazada.
Lista completa en SECURITY.md.
Definición de terminado
Antes de dar por buena una función:
-
pnpm verifyen verde -
pnpm test:integrationen verde si toca la base de datos -
pnpm test:e2een verde si cambia un recorrido de la interfaz - Tests para el camino feliz y para el caso que debe fallar
- Si toca dinero o tiempo, test del cálculo
- Si toca permisos, test del acceso indebido
- Verificación manual en móvil si afecta al personal de campo
- Textos visibles en español
- Estados de carga y de vacío resueltos
- Documentación actualizada si cambia una regla de negocio
Qué falta
Ver IMPLEMENTATION_STATUS.md. Los recorridos cubren los seis flujos críticos del §99; lo que queda son casos de menor riesgo, como subir una fotografía de verdad desde el navegador, que hoy se verifica a nivel de integración contra el almacenamiento real.