ESTADO: Aprobado
FECHA: 2026-08-14
AUTOR: Carlos Alberto Torres Camargo (arquitecto)
RELACIONADOS: ADR-003 (Política de entrega a QA), ADR-002 (División de Libraries), ADR-FLAT_RECORD (contrato aplanado)
IMPLEMENTACIÓN DE REFERENCIA: nebula-erp, MR !198 — apps/nebula-shell-e2e/src/support/sesion-e2e.ts
nebula-erp tenía Playwright configurado y seis specs escritos, pero ninguno aportaba señal: todos terminaban saltándose.
E2E_STORAGE_STATE, un archivo de sesión que alguien debe producir a mano antes de correrlos.E2E_USER / E2E_PASSWORD para hablar con api-dev.En CI no existe ninguna de las dos cosas, así que el job pasaba en verde sin haber ejercido una sola pantalla. Un verde que no significa nada es peor que un rojo: enseña a confiar en una señal vacía.
A eso se sumó un hallazgo que agrava el cuadro. Al revisar el estado real de develop había 45 pruebas unitarias en rojo repartidas en dos proyectos, acumuladas durante meses sin que nadie lo notara.
No fue porque no se ejecutaran. El CI/CD de nebula-erp es Jenkins (Jenkinsfile, patrón angularNxPipeline de la librería centrica-pipeline), que construye develop y despliega a dev en cada integración, ejecutando lint, pruebas y build. La causa está en los defaults de ese patrón, que el Jenkinsfile del proyecto no sobrescribe:
installDepsBlocking : false,
e2eBlocking : false,
lintBlocking : false,
testsBlocking : false,
sonarBlocking : false,
buildBlocking : true, // el único que detiene el pipeline
Y así se ejecutan:
def suffix = cfg.testsBlocking ? '' : ' || echo "WARN: tests unitarios fallaron o ausentes (no bloqueante)"'
Es decir: las pruebas corrían en cada build, fallaban, imprimían un WARN y el pipeline seguía adelante hasta desplegar. El aviso quedaba enterrado en el log y la deuda creció sin resistencia. Lo mismo aplica a lint, a Sonar y a los E2E.
Nota sobre
.gitlab-ci.yml. El repositorio conserva un.gitlab-ci.ymlheredado del andamiaje de Nx (chore: initialize Nx workspace, marzo de 2026). No forma parte del CI/CD del proyecto, no tiene runners asignados y sus pipelines quedan encolados sin ejecutarse. No debe tomarse como señal de nada; confundirlo con el pipeline real lleva a diagnósticos equivocados.
Cada pantalla que se cubra tendrá dos specs, con nombre explícito y propósito distinto. No se sustituyen: responden preguntas diferentes.
| Variante | Archivo | Corre en CI | Responde |
|---|---|---|---|
| Mock | <modulo>-mock.spec.ts |
Sí, siempre | ¿La pantalla está bien cableada? ¿Pinta lo que el contrato entrega? ¿La petición sale con la forma acordada? |
| API real | <modulo>-api-dev.spec.ts |
No | ¿El backend desplegado cumple el contrato que el front consume? |
La variante mock es la red de seguridad: barata, determinista y sin dependencias, corre en cada MR. La variante API real es la evidencia de integración que se anexa a la entrega de QA; se ejecuta bajo demanda y se salta sola cuando faltan credenciales, en vez de fallar.
Prohibido que un spec mock exija E2E_STORAGE_STATE, credenciales o backend. Si los exige, no corre en CI, y entonces no es una prueba: es documentación.
E2E_USER, E2E_PASSWORD, E2E_API, E2E_ENV, E2E_COMPANY_ID. Jamás escritas en el repositorio, ni siquiera como valor por defecto.
Todo lo que sigue vive en apps/nebula-shell-e2e/src/support/sesion-e2e.ts y se reutiliza. Ningún spec repite esta plomería.
Levantar un E2E contra este ERP tiene cuatro obstáculos que no se ven hasta que se chocan. Están documentados aquí porque cada uno costó una sesión de diagnóstico.
Las rutas están tras authGuard, que consulta AuthService.isSessionActive(). Sin sesión, la app redirige a /login y la prueba nunca llega a la pantalla.
isSessionActive() comprueba únicamente que exista access_token y que token_expiry sea futuro. No valida la firma: decodeToken se limita a un atob del payload. Eso permite sembrar una sesión sintética en localStorage con addInitScript y entrar sin servidor de autenticación.
localStorage.setItem('access_token', `${b64(cabecera)}.${b64({ User: usuario })}.e2e`);
localStorage.setItem('token_expiry', String(Date.now() + 8 * 60 * 60 * 1000));
localStorage.setItem('user_data', JSON.stringify(usuario));
Debe hacerse con addInitScript y no después del goto: AuthService lee el token en su constructor, así que escribir más tarde llega cuando el guard ya redirigió.
El token no abre nada en ningún ambiente real, porque el backend sí verifica la firma. Sirve solo para que el guard del navegador deje pasar.
Este es el que más desconcierta. Navegar a /layout/tesorería/embargo/list no monta la pantalla: deja el contenedor con su mensaje "Selecciona una opción de Tesorería para comenzar".
Los módulos montan un TabContainerComponent, y la pantalla vive dentro de una pestaña que solo nace al pulsar la opción en el menú lateral (addTabFromMenu). El menú, a su vez, lo sirve el backend.
Por tanto el spec debe responder POST /simappe-admin/api/v1/menu/read-by-roles-grouped con el árbol que espera SidebarService.buildMenuTree —roles → applications → modules → optionGroups → options— y después navegar pulsando, como haría el usuario.
normalizeRouteStatic reescribe el módulo de la rutaSidebarWrapper.normalizeRouteStatic reemplaza el segmento de módulo de toda ruta del menú por el valor de localStorage.selectedRoute, y cuando esa clave no existe usa 'accounting'.
Sin sembrarla, pulsar "Embargos" navega a /layout/accounting/embargo/list —una ruta que el router no tiene— y la pestaña abre vacía, con el breadcrumb diciendo "Accounting". El síntoma no apunta en absoluto a la causa.
localStorage.setItem('selectedRoute', 'tesorería'); // debe coincidir con el segmento real
Conviene interceptar todo el tráfico **/api/v1/** con una respuesta por defecto, para que ninguna llamada de arranque no prevista (menú, permisos, parámetros, i18n) escape al baseURL real y deje la pantalla a medias por un motivo ajeno a lo que se prueba.
El default debe ser un array vacío, no un objeto:
cuerpo = sobre([]); // correcto
cuerpo = sobre({}); // rompe el arranque
TranslationService hace (langs || []).filter(...). Si recibe {}, revienta con TypeError: filter is not a function y la aplicación entera queda en blanco. Al revés es inocuo: un consumidor que espera objeto y recibe [] solo lee undefined.
Excepción: el bundle i18n (/bundle/…) sí debe responder objeto, porque se indexa por clave.
Dos cuidados:
getByText global termina pulsando cualquier otra cosa y la navegación no ocurre. Usar .menu-link / .submenu-link.formatKey, que altera mayúsculas: "Tesorería" se muestra como "TesoreríA". Buscar de forma laxa.Mock — cableado y contrato de salida:
Pensión alimenticia, no PENSION_ALIMENTICIA);page-response / /listado, paginación en el cuerpo).API real — integración:
Cuando el ambiente no tiene datos para un caso, el test se salta explícitamente con el motivo. Nunca se afloja la aserción para que pase: un verde obtenido bajando el listón es exactamente el problema que este ADR busca eliminar.
A favor
En contra
isSessionActive() siga sin validar firma en el navegador. Si eso cambia —y sería razonable que cambiara—, el soporte hay que ajustarlo. Está aislado en un solo archivo justamente por eso.Pendiente, fuera del alcance de este ADR
Escribir las pruebas resuelve que exista la señal; no resuelve que la señal frene algo. Con los defaults actuales de angularNxPipeline —testsBlocking, lintBlocking y e2eBlocking en false— una regresión futura seguirá produciendo un WARN en el log de Jenkins mientras el pipeline despliega igual. Es decir: estos E2E avisan, pero hoy no detienen nada.
Decidir qué etapas pasan a ser bloqueantes, y en qué ramas, es una decisión de la política de construcción y despliegue, no de la estrategia de pruebas. Se deja planteada aquí porque sin ella el valor de este ADR se reduce a la mitad.
apps/nebula-shell-e2e/src/<modulo>-mock.spec.ts.beforeEach: prepararPagina(page, '<modulo>'), mockearMenu(...), las rutas del API con sus fixtures, y abrirDesdeMenu(...).<modulo>-api-dev.spec.ts con test.skip(!CREDENCIALES, ...), autenticar con autenticar(request) y verificar el contrato real.# Variante que corre siempre
npx nx e2e nebula-shell-e2e --grep="mock"
# Variante de evidencia
E2E_USER=... E2E_PASSWORD=... npx nx e2e nebula-shell-e2e --grep="api real"
El reporte HTML de Playwright es autocontenido: se anexa como un solo archivo y se abre en el navegador sin servidor.