docs: cierre formal de Sprint 1.5

- OBSERVATIONS.md reseteado para Sprint 2
- STRATEGIC_DIRECTION.md con reflexiones del sprint
- TECH_DEBT.md curado: items resueltos archivados
- methodology/CASE_STUDIES/inq-roi-sprint-1-5.md generado

Sprint 1.5 cerrado:
- 11 etapas + 1 fix pre-Etapa 11
- 526 unit tests + 38 E2E al cierre
- Producto con identidad InQ aplicada en web + 3 PDFs
- Metodología documentada con casos reales (2 de mala
  clasificación de iniciativa, originaron AP.8 +
  refinamiento C.6)
- Material para transmisión al equipo de 5 técnicos
This commit is contained in:
2026-05-19 07:27:40 -03:00
parent 4f6c424d5c
commit 49364f4b18
20 changed files with 1448 additions and 52 deletions

196
docs/COVERAGE_STATE.md Normal file
View File

@@ -0,0 +1,196 @@
# Estado de cobertura — fin de Sprint 1.5
> Generado: Etapa 10, 18 mayo 2026.
> Base: 526 tests unitarios (33 archivos), suite E2E Playwright.
> Propósito: baseline de partida para Sprint 2.
---
## Resultado de la suite al cierre del sprint
### Tests unitarios (Vitest)
| Métrica | Valor |
|---|---|
| Total tests | 526 |
| Verdes | 526 |
| Rojos | 0 |
| Skip | 0 |
| Archivos de test | 33 |
| Duración | ~7 segundos |
### Tests E2E (Playwright)
| Métrica | Valor |
|---|---|
| Total tests | 38 |
| Verdes | 34 |
| Rojos | 4 (ver Regresiones documentadas) |
| Skip | 0 |
| Duración | ~75 segundos |
### Build / Typecheck / Lint
| Check | Estado | Nota |
|---|---|---|
| `npm run build` | ✅ Limpio | 2865 módulos, ~1 segundo |
| `npm run typecheck` | ✅ Sin errores | TypeScript strict |
| `npm run lint` | ⚠️ 3 errores, 92 warnings | Errores = `no-irregular-whitespace` en tests preexistentes; warnings = `@typescript-eslint/no-explicit-any` esperados en interfaces jsPDF |
---
## Regresiones documentadas (sin acción correctiva)
### RE-01 — `etapa-4-pdfs.spec.ts` (3 tests)
**Tipo:** test obsoleto, no regresión de producción.
**Síntoma:** los tests esperan 3 páginas en Actual/Automatizado y 4 en ROI.
El producto ahora genera 4 y 5 páginas respectivamente (cambios de Etapas 5-8).
**Causa:** el archivo fue escrito para el baseline de Etapa 4 y no se actualizó
cuando Etapas 5, 7 y 8 cambiaron la estructura de páginas.
**Impacto:** ninguno en producción. Los PDFs son correctos.
**Acción Sprint 2:** actualizar las aserciones de página en `etapa-4-pdfs.spec.ts`
o archivarlo en `tests/e2e/archive/` como registro histórico.
### RE-02 — `prod-smoke.spec.ts` (1 test)
**Tipo:** test obsoleto, no regresión de producción.
**Síntoma:** el test verifica `page.title` con `/Process Cost Platform/`
pero el título actual es `"InQ ROI — Cuantificá tus procesos"`.
**Causa:** la migración de marca se hizo en Etapa 2 del Sprint 1.5. El smoke test
no se actualizó al momento del cambio.
**Impacto:** ninguno en producción.
**Acción Sprint 2:** actualizar el selector del smoke test a `/InQ ROI/`.
---
## Áreas con cobertura sólida (unitarios + E2E)
### Motor de simulación BPMN
- `tests/domain/` — simulación determinística, gateways XOR/AND, probabilidades
- `tests/integration/bpmn-parser.test.ts` — parsing de procesos reales
- `tests/integration/bpmn-collaboration.test.ts` — colaboración
- **Evaluación:** muy sólida. El motor es el corazón del producto y está bien cubierto.
### Cálculo de ROI
- `tests/domain/roi.test.ts` — casos edge (inversión cero, ahorro negativo, payback infinito)
- `tests/lib/format-payback.test.ts` — 12 casos del formateador
- **Evaluación:** sólida. Cálculos puros con edge cases cubiertos.
### Exportación PDF — 3 generadores
- `tests/lib/export/pdf-orientation.test.ts` — orientaciones portrait/landscape
- `tests/lib/export/pdf-roi-page1.test.ts` — página 1 del ROI (gradiente, Top3, grid)
- `tests/lib/export/pdf-roi-pages2-5.test.ts` — tablas, análisis, metodología, gráfico SVG
- `tests/lib/export/pdf-scenario-page1.test.ts` — página 1 Actual/Automatizado
- `tests/lib/export/pdf-sections-shared.test.ts` — header/footer compartidos
- E2E: Etapas 4-8 validan PDFs reales con pdfjs-dist
- **Evaluación:** la más completa del sprint. Cobertura unitaria + E2E de los 3 PDFs.
### Helpers de formato
- `tests/lib/format.test.ts` — currency, minutes, timestamp
- `tests/lib/format-payback.test.ts` — payback en todos los rangos
- `tests/lib/colors.test.ts` — tokens de color del heatmap
- **Evaluación:** sólida. Funciones puras con muchos casos.
### Design tokens
- `tests/design-system/tokens.test.ts` — paleta InQ, colores CONCILIA excluidos
- **Evaluación:** básica pero suficiente para validar el sistema de identidad.
### Badge de automatización en BpmnCanvas
- `tests/features/workspace/BpmnCanvas.test.tsx` — lifecycle overlay, posición, HTML
- `tests/features/workspace/BpmnCanvas-badge.test.ts` — propiedades CSS del badge
- E2E: `etapa-9-badge.spec.ts` — aparece/desaparece/reaparece, screenshots
- **Evaluación:** sólida tras Etapa 9.
### Invalidación del store de simulación
- `tests/store/simulation-store-invalidation.test.ts` — 7 casos de invalidación
- **Evaluación:** sólida para los escenarios cubiertos.
---
## Áreas con cobertura parcial
### Componentes React del reporte
- `tests/features/report/` existe con tests de `CumulativeSavingsChart`, `ComparisonTable`, `RoiKpiBar`
- **Gap:** los tests son snapshots. No validan comportamiento interactivo ni edge cases de renderizado.
- **Riesgo:** bajo a corto plazo, medio cuando se agreguen features.
### Persistencia (Dexie/IndexedDB)
- Cobertura: indirecta vía store tests
- **Gap:** no hay tests directos de la capa de persistencia (migraciones, lectura/escritura de procesos complejos)
- **Riesgo:** medio. Las migraciones son el punto más frágil — un error ahí destruye datos del usuario.
### ActivityPanel
- **Gap:** sin tests unitarios ni E2E específicos del panel de actividades
- El comportamiento se verifica indirectamente en tests E2E de generación de PDF
- **Riesgo:** medio. El panel es la interfaz principal de configuración.
---
## Áreas sin cobertura
### Flujo completo de importación BPMN
- No hay test E2E que valide el flujo completo: archivo BPMN inválido → error legible
- No hay test para BPMNs con pools/lanes (collaboration.test.ts cubre el parser, no el UX)
- **Riesgo:** medio. Los usuarios importan archivos de herramientas externas con formatos variables.
### Exportación CSV
- `tests/e2e/` tiene `export-pdf.spec.ts` y `export-roi.spec.ts` pero los CSV no se validan con el mismo rigor
- **Riesgo:** bajo por ahora.
### Regresión visual de PDFs (golden files)
- No hay comparación pixel-a-pixel de PDFs entre versiones
- **Riesgo:** medio-alto. Un cambio visual inadvertido en `pdf-sections.ts` puede pasar los tests actuales sin ser detectado.
### Edge cases del simulador con procesos grandes
- Tests del motor cubren procesos de 3-10 actividades
- **Gap:** procesos de 20+ actividades, gateways OR inclusivos, subprocesos anidados
- **Riesgo:** bajo a corto plazo.
### Gateway OR inclusivo
- El motor simula OR como XOR (documentado). No hay test que valide que la advertencia aparece.
- **Riesgo:** bajo hasta que se implemente OR real.
---
## Gaps priorizados para Sprint 2
### Alta prioridad
1. **Regresión visual PDF (golden files):** implementar comparación de PDF contra baseline usando `pdfjs-dist`. Guarda página como PNG y compara con imagen de referencia. Detecta cambios involuntarios.
2. **Tests de migración Dexie:** validar que cada versión del schema se migra correctamente sin pérdida de datos.
3. **Smoke test E2E con BPMN inválido:** verificar mensaje de error apropiado cuando el archivo no es BPMN válido.
### Media prioridad
4. **Tests del ActivityPanel:** verificar que el formulario de configuración de actividad persiste correctamente a IndexedDB.
5. **Actualizar tests obsoletos:** `etapa-4-pdfs.spec.ts` y `prod-smoke.spec.ts` (ver RE-01, RE-02).
6. **Tests de flujo completo negativo:** ¿qué pasa si se simula sin configurar ningún costo?
### Baja prioridad
7. **Tests de stress del simulador:** proceso con 30+ actividades y gateways anidados.
8. **Tests de exportación CSV:** validar contenido y estructura de los archivos CSV generados.

146
docs/QA_CHECKLIST.md Normal file
View File

@@ -0,0 +1,146 @@
# Checklist de QA — pre-cierre de sprint / pre-deploy
> Lista de verificación para validar el estado del producto antes de
> cerrar un sprint o desplegar a producción.
> Actualizado al cierre de Sprint 1.5 (18 mayo 2026).
> Tiempo estimado: 15-20 minutos (5 automatizado + 10 manual).
---
## 1. Suite automatizada
```
[ ] npm test → 0 rojos, 0 skip sin justificación
[ ] npm run test:e2e → 0 rojos (excluir tests archivados)
[ ] npm run build → sin errores (warnings de chunk-size: ignorar)
[ ] npx tsc --noEmit → sin errores
[ ] npm run lint → 0 errores (warnings de any: ignorar en lib/export/)
```
**Si hay rojos:** registrar en OBSERVATIONS.md como `[CURRENT-SPRINT]` con categoría:
- Bug nuevo introducido en este sprint → corregir antes de cerrar
- Test obsoleto (baseline caducado) → actualizar el test o archivarlo
- Flaky (pasa/falla según timing) → registrar como tech debt
---
## 2. Validación visual — web app
Ejecutar `npm run dev` → abrir `http://localhost:5173`
```
[ ] Homepage carga sin errores en consola del browser
[ ] Importar BPMN: arrastrar medium-with-gateways.bpmn → workspace carga
[ ] BPMN renderiza correctamente (nodos, flujos, gateways visibles)
[ ] Clic en actividad → ActivityPanel se abre con formulario de costos
[ ] Toggle "Automatizable" + Guardar → badge ⚡ naranja aparece sobre el nodo en el canvas
[ ] Desmarcar "Automatizable" + Guardar → badge ⚡ desaparece
[ ] Botón Simular → navega a /report/ con KPI cards
[ ] KPI cards usan paleta naranja InQ (fondo #FEF3E2, no gris)
[ ] Tab "Automatizado" → reporte actualizado con costos automatizados
[ ] Tab "Comparación" → KPI Hero naranja → magenta visible
[ ] CumulativeSavingsChart muestra curva con payback marcado
[ ] Ningún error de consola durante el flujo completo
[ ] Texto "InQ ROI" en header (no "Process Cost Platform")
[ ] Footer: "Hecho desde Paraguay 🇵🇾" (si aplica según decisión pendiente BACKLOG.md)
```
---
## 3. Validación visual — PDFs
Exportar los 3 PDFs después del flujo del punto anterior.
```
PDF Actual (4 páginas):
[ ] Página 1: "ANÁLISIS DE COSTOS — ESCENARIO ACTUAL" + cards naranja InQ
[ ] Página 1: strip de metadata con PROCESO, FECHA, MONEDA
[ ] Página 2: diagrama BPMN landscape con heatmap de colores
[ ] Página 3: "ANÁLISIS DE COMPOSICIÓN" uppercase + tabla con header naranja
[ ] Página 4: "NOTA METODOLÓGICA" uppercase + caja contenedora
[ ] Footer todas las páginas: "InQ ROI · Powered by InQuality · Página X de 4"
PDF Automatizado (4 páginas):
[ ] Página 1: "ANÁLISIS DE COSTOS — ESCENARIO AUTOMATIZADO" + cards naranja InQ
[ ] Páginas 2-4: mismo esquema que Actual
[ ] Footer: "Página X de 4"
PDF ROI (5 páginas):
[ ] Página 1: header denso + banner narrativo + KPI Hero gradiente naranja→magenta
[ ] Página 1: grid 2×2 KPIs secundarios + Top 3 actividades con barras
[ ] Página 2: tabla comparativa con header naranja (#FFF8ED)
[ ] Página 3: "ANÁLISIS DEL AHORRO" + sección transparencia (si aplica)
[ ] Página 4: "NOTA METODOLOGICA - ANALISIS DE ROI" + caja contenedora
[ ] Página 5: "TRAYECTORIA DEL AHORRO ACUMULADO" + gráfico SVG con áreas
[ ] Footer: "InQ ROI · Powered by InQuality · Página X de 5"
```
---
## 4. Regresiones de marca
```
[ ] "InQ" aparece siempre como I-n-Q (no INQ, Inq, inq, inQ)
[ ] "InQ ROI" es el nombre del producto (no "Process Cost Platform")
[ ] "Powered by InQuality" en footer de todos los PDFs
[ ] Sin "v0.1.0" en PDFs (puede estar en web si es útil internamente)
[ ] Sin colores CONCILIA (#572FA2, #CB1889) en ningún componente
[ ] Gradiente naranja→magenta solo en KPI Hero ROI (no en PDFs Actual/Automatizado)
```
---
## 5. Documentación del sprint
```
[ ] CLAUDE.md tiene criterios de iniciativa actualizados
[ ] methodology/PATTERNS.md tiene los patrones del sprint cosechados
[ ] methodology/ANTI_PATTERNS.md tiene los anti-patrones del sprint
[ ] OBSERVATIONS.md: todos los items son [RESOLVED] o [DEFERRED-*]
[ ] TECH_DEBT.md: items del sprint actualizados/promocionados
[ ] BACKLOG.md: items nuevos del sprint agregados
[ ] STRATEGIC_DIRECTION.md: refleja decisiones tomadas en el sprint
[ ] sprints/sprint-N/ tiene todos los ETAPA_N_PROMPT.md del sprint
```
---
## 6. Artefactos del sprint
```
[ ] tests/e2e/__output__/sprint-N-N-final/ tiene los 3 PDFs baseline
[ ] tests/screenshots/ tiene screenshots de Playwright de features visuales
[ ] docs/COVERAGE_STATE.md actualizado con el estado al cierre
[ ] docs/QA_CHECKLIST.md actualizado si hubo cambios en el proceso
[ ] git tag sprint-N-cerrado apuntando al commit de cierre
```
---
## Smoke test manual rápido (5 minutos)
Para verificar el flujo completo en producción después de un deploy:
```
1. Abrir la URL de producción (Cloudflare Pages)
2. Importar medium-with-gateways.bpmn desde public/sample-processes/
3. Configurar task_recv: costo directo 200, automatizado 20, marcar automatable → Guardar
4. Verificar: badge ⚡ naranja aparece sobre el nodo en el canvas
5. Clic en "Simular"
6. En el reporte, verificar KPI cards con paleta naranja
7. Ir a tab "Comparación" → verificar KPI Hero con gradiente naranja→magenta
8. Exportar PDF Actual → verificar que descarga y tiene 4 páginas
9. Exportar PDF ROI → verificar que descarga y tiene 5 páginas
10. Verificar que "InQ ROI" aparece en el header web y en los PDFs
Tiempo estimado: 5 minutos
Resultado esperado: todo funciona sin errores de consola
```
---
## Historial de actualizaciones
| Versión | Sprint | Cambios |
|---|---|---|
| 1.0 | Sprint 1.5 | Creación inicial al cierre del sprint |

View File

@@ -0,0 +1,312 @@
# Sprint 1.5 — Etapa 10: Validación E2E final + preparación Sprint 2
> **Objetivo:** validar que el Sprint 1.5 (Etapas 1-9) produjo un sistema funcional, sin regresiones, con cobertura clara y baseline reproducible. Generar checklist de QA reutilizable y documento de estado de cobertura para que Sprint 2 arranque con confianza.
>
> **Pre-requisitos:** Etapas 1-9 cerradas. Suite de tests existente con ~526 tests verdes. Sistema de design tokens InQ aplicado en web app + 3 PDFs.
>
> **Etapa siguiente:** Etapa 11 — Polish final + documentación del Sprint 1.5 (refinamientos diferidos del KPI Hero, banner, grid 2x2, Top 3 + cierre formal del sprint).
---
## 1. Esta etapa es distinta a las anteriores
Las Etapas 1-9 produjeron código y artefactos visuales. Etapa 10 **valida lo construido**. No agrega features, no rediseña visuales. Su producto principal es **confianza en el sistema**.
**Mentalidad correcta para esta etapa:**
- Si todo funciona como debe → reporte limpio, baseline documentado
- Si hay regresiones o bugs no cazados antes → catalogarlos sin pánico, anotarlos para resolver con criterio (algunos en Etapa 11, otros en Sprint 2)
- NO arreglar cosas durante esta etapa sin consultar. La etapa es **diagnóstica**, no correctiva.
---
## 2. Alcance estricto
### Sí entra
-**Correr la suite E2E completa** del proyecto
-**Documentar resultado**: cuántos tests, cuántos verdes, cuántos rojos, cuántos skip
-**Generar los 3 PDFs finales del sprint** (Actual, Automatizado, ROI) en `tests/e2e/__output__/sprint-1-5-final/`
-**Generar reporte de estado de cobertura** en `docs/COVERAGE_STATE.md` con:
- Qué áreas tienen tests sólidos (unitarios + E2E)
- Qué áreas tienen tests débiles o nulos (gaps identificados)
- Recomendaciones de qué cubrir en Sprint 2
-**Crear `docs/QA_CHECKLIST.md`** reutilizable: lista de verificación para futuros cierres de sprint o pre-deploys
-**Smoke test manual documentado** del flujo end-to-end (qué hacer en el navegador para verificar el producto)
-**Identificar regresiones o anomalías** comparando con baseline previo de cada etapa
### NO entra
- ❌ Agregar tests nuevos para cubrir gaps detectados (eso es Sprint 2)
- ❌ Arreglar bugs detectados durante la validación (a menos que sean bloqueantes — consultar antes)
- ❌ Cambios visuales en web app o PDFs
- ❌ Refinamientos del KPI Hero, banner, grid 2x2, Top 3 (Etapa 11)
- ❌ Cambios en la arquitectura, deps, o configuración
---
## 3. Lo que esta etapa debe producir
### 3.1 Reporte de ejecución de la suite
Formato esperado en el reporte final a director:
```
Suite de tests — estado actual:
Tests unitarios:
- Total: N
- Verdes: N
- Rojos: N (listar archivos + razón)
- Skip: N (listar archivos + razón si está documentada)
Tests E2E (Playwright):
- Total: N
- Verdes: N
- Rojos: N
- Skip: N
Build:
- npm run build: ✓ o ✗ (con detalle si falla)
- npm run typecheck: ✓ o ✗
- npm run lint: ✓ o ✗
Tiempo total de ejecución de la suite:
- Unit: N segundos
- E2E: N segundos
```
### 3.2 PDFs finales del sprint
En la carpeta `tests/e2e/__output__/sprint-1-5-final/`, generar:
- `final-actual.pdf` — proceso medium-with-gateways, escenario actual
- `final-automatizado.pdf` — proceso medium-with-gateways, escenario automatizado
- `final-roi.pdf` — proceso medium-with-gateways, ROI con frecuencia 5000/año, 3 años, USD 80.000
Estos PDFs son **el baseline final del sprint**. Cualquier cambio futuro debe compararse contra ellos.
Validar al generar:
- PDF Actual: 4 páginas, orientaciones `[portrait, landscape, portrait, portrait]`
- PDF Automatizado: 4 páginas, mismas orientaciones
- PDF ROI: 5 páginas, todas portrait, KPI Hero gradiente en página 1, trayectoria del ahorro en página 5
### 3.3 `docs/COVERAGE_STATE.md`
Documento con esta estructura mínima:
```markdown
# Estado de cobertura — fin de Sprint 1.5
## Áreas con cobertura sólida
[Lista de módulos/features con tests unitarios + E2E robustos]
Ejemplos esperados:
- Motor de simulación BPMN (lib/simulator)
- Cálculo de ROI (lib/roi)
- Exportación PDF (3 generadores)
- Formato de moneda y payback (helpers)
- Badge automatizable en BpmnCanvas
## Áreas con cobertura parcial
[Lista de módulos donde hay tests unitarios pero faltan E2E, o viceversa]
## Áreas sin cobertura
[Lista de módulos críticos sin tests, identificados durante revisión]
## Gaps identificados para Sprint 2
[Tests que vale la pena agregar en Sprint 2, priorizados por riesgo]
Ejemplos esperables:
- Tests E2E del flujo completo de importación BPMN
- Tests de regresión visual de PDFs (golden files)
- Tests de stress en simulador con procesos grandes (50+ actividades)
- Tests de edge cases en cálculo de ROI (inversión cero, payback infinito, etc.)
```
### 3.4 `docs/QA_CHECKLIST.md`
Checklist reutilizable para cierres de sprint o pre-deploys. Estructura:
```markdown
# Checklist de QA — pre-cierre de sprint / pre-deploy
> Lista de verificación para validar el estado del producto antes de
> cerrar un sprint o desplegar a producción.
## 1. Suite de tests
[ ] npm test pasa con 0 rojos
[ ] npm run test:e2e pasa con 0 rojos
[ ] npm run build sin errores ni warnings nuevos
[ ] npm run typecheck limpio
[ ] npm run lint limpio
## 2. Validación visual web app
[ ] Workspace carga correctamente con simple-linear
[ ] BPMN se renderiza sin errores en consola
[ ] Badge ⚡ aparece en actividades automatizables (después de Guardar)
[ ] Reporte muestra KPI cards con paleta InQ
[ ] Tab ROI muestra KPI Hero con gradiente naranja → magenta
[ ] Switch toggle activa/desactiva escenario automatizado
[ ] CumulativeSavingsChart se ve correctamente
## 3. Validación visual PDFs
[ ] PDF Actual tiene 4 páginas (incluye BPMN landscape)
[ ] PDF Automatizado tiene 4 páginas (incluye BPMN landscape)
[ ] PDF ROI tiene 5 páginas (con trayectoria del ahorro)
[ ] Header "InQ ROI" naranja en todas las páginas
[ ] Footer "Powered by InQuality" + paginación correcta
[ ] Sin caracteres rotos visibles
[ ] Sin texto truncado en márgenes
## 4. Regresiones
[ ] No aparece "Process Cost Platform" en ningún lugar
[ ] No aparece "v0.1.0" en PDFs (solo en web)
[ ] No aparece "INQ", "Inq", "inq" (verificar regla I-n-Q)
## 5. Documentación
[ ] CLAUDE.md actualizado con decisiones del sprint
[ ] methodology/ tiene cosechas aplicadas
[ ] OBSERVATIONS.md vacío o solo con [RESOLVED]
[ ] BACKLOG.md y TECH_DEBT.md actualizados
[ ] STRATEGIC_DIRECTION.md refleja decisiones del sprint
```
### 3.5 Smoke test manual
Documentar **pasos exactos** que el director (o un consultor de InQuality) puede ejecutar en el navegador para validar que el producto funciona. Esto va a la sección del QA_CHECKLIST.md o como archivo separado `docs/SMOKE_TEST.md`.
Pasos mínimos esperados:
```
1. npm run dev
2. Abrir http://localhost:5173/
3. Importar BPMN de ejemplo (medium-with-gateways)
4. Configurar costos en 3 actividades:
- Recepcionar solicitud: USD 200 directo / USD 20 automatizado
- Calcular score crediticio: USD 100 directo / USD 10 automatizado
- Analizar riesgo crediticio: USD 200 directo / USD 20 automatizado
5. Marcar las 3 como automatizables y Guardar cada una
6. Verificar que aparecen 3 badges ⚡ en el canvas
7. Ir a tab Reporte → verificar KPI cards
8. Ir a tab ROI → verificar KPI Hero gradiente
9. Configurar parámetros ROI: 5000 ejecuciones/año, 3 años, USD 80.000 inversión
10. Exportar los 3 PDFs (Actual, Automatizado, ROI)
11. Verificar que los PDFs descargados tienen la cantidad de páginas correcta
Tiempo estimado: 5 minutos
```
---
## 4. Validaciones automatizadas mandatorias
```
Antes de present_files, ejecutá y confirmá:
1. Suite completa de tests pasa:
- Unit tests: TODOS verdes (NO permitir rojos o skips sin justificación)
- E2E tests: TODOS verdes
- Si hay rojos: documentar exactamente cuáles y por qué
2. Los 3 PDFs finales se generan correctamente:
- Tamaños razonables (Actual ~80KB, Automatizado ~80KB, ROI ~250KB con gradiente)
- Cantidad de páginas correcta (4, 4, 5)
- Orientaciones correctas
3. Build, typecheck, lint: TODOS limpios
4. Archivos creados en su lugar:
- docs/COVERAGE_STATE.md
- docs/QA_CHECKLIST.md (o sección dentro de COVERAGE_STATE.md)
- tests/e2e/__output__/sprint-1-5-final/ con los 3 PDFs
5. NO hay archivos huérfanos:
- No quedaron PDFs viejos de etapas anteriores en directorios productivos
- tests/screenshots/ no creció más allá de lo esperado
```
---
## 5. Entregables (Definition of Done)
```
[ ] Suite completa de tests ejecutada y reportada
[ ] 3 PDFs finales del sprint generados en sprint-1-5-final/
[ ] docs/COVERAGE_STATE.md creado con áreas sólidas + parciales + gaps
[ ] docs/QA_CHECKLIST.md creado y reutilizable
[ ] Smoke test manual documentado
[ ] Reporte de regresiones (si las hay) catalogadas sin actuar
[ ] Build limpio, typecheck limpio, lint limpio
[ ] PDFs presentados con paths absolutos para validación visual del director
[ ] Sin código nuevo agregado al producto (solo docs + artefactos de QA)
[ ] Decisiones por iniciativa: esperado "Ninguna" (la etapa es diagnóstica)
```
---
## 6. Reporte final a entregar
1. **Tabla de archivos creados** (esperado: solo docs + PDFs en sprint-1-5-final/)
2. **Resultado de la suite completa** (verdes/rojos/skip por categoría)
3. **3 PDFs finales presentados con paths absolutos**
4. **Resumen de COVERAGE_STATE.md**: áreas sólidas, parciales, gaps
5. **Resumen de QA_CHECKLIST.md**: cuántos items tiene, cómo está organizado
6. **Regresiones detectadas**: si hay, listadas SIN ACCIÓN CORRECTIVA (anotadas para Etapa 11 o Sprint 2)
7. **Anomalías encontradas**: cualquier comportamiento inesperado durante la validación
8. **Recomendaciones para Sprint 2**: cosas que vale la pena cubrir con tests adicionales
---
## 7. Reglas operativas
1. **NO ARREGLAR bugs durante esta etapa.** Si encontrás regresión o anomalía, **anotala en el reporte** y dejala así. Excepción única: si encontrás un bug que rompe el build completo o impide generar los PDFs finales, consultar antes de arreglar.
2. **NO AGREGAR tests nuevos** para cubrir gaps. Los gaps van a COVERAGE_STATE.md para Sprint 2.
3. **NO CAMBIAR código de producción.** Solo se permite agregar documentación.
4. **NO TOCAR archivos visuales del producto** (PDFs, web app). Solo generarlos para baseline.
5. **Política de iniciativa proactiva refinada (post-Etapa 8):**
- Cualquier cambio en código de producción durante esta etapa: Nivel 3 (consultar previa)
- Crear nuevos archivos en `docs/`: permitido si están en scope (ver §3)
- Crear archivos fuera de `docs/`: Nivel 2 mínimo
6. **Si la suite tiene rojos**: documentarlos sin arreglarlos. La etapa puede cerrarse con rojos conocidos siempre que estén documentados y categorizados (bug nuevo vs bug heredado vs flaky).
7. **Si encontrás contradicción** entre prompt y código: PARÁ y consultá.
---
## 8. Mentalidad correcta para esta etapa
Etapa 10 es **inventario, no construcción**. Pensá como auditor externo:
- ¿Qué tengo construido?
- ¿Qué tan robusto está testeado?
- ¿Qué falta cubrir?
- ¿Hay sorpresas escondidas?
Si encontrás algo que no estaba en el radar (test que pasa pero el comportamiento real es raro, dependencia desactualizada, archivo huérfano de etapas pasadas), **eso es valor de esta etapa**. Reportarlo es lo correcto.
Si todo está limpio y predecible, eso también es valor — significa que las Etapas 1-9 dejaron un sistema en buen estado.
**El éxito de esta etapa no se mide en cambios producidos. Se mide en confianza generada para arrancar Sprint 2.**
---
## 9. Una nota sobre el cierre del Sprint 1.5
Después de Etapa 10, queda solo Etapa 11 (polish + documentación). Cuando esa cierre:
- El Sprint 1.5 se considera completo
- Se hace tag `sprint-1-5-cerrado` en git
- Se ejecuta el cierre de OBSERVATIONS.md (purgar items resueltos, archivar)
- Se actualiza STRATEGIC_DIRECTION.md con reflexiones del sprint
- Sprint 2 arranca con backlog claro, baseline de cobertura conocido, y método refinado
Etapa 10 es el último filtro antes de ese cierre.

View File

@@ -0,0 +1,471 @@
# Sprint 1.5 — Etapa 11: Polish final + cierre formal del sprint
> **Objetivo:** completar los refinamientos visuales diferidos de Etapa 6 sobre la página 1 del PDF ROI (KPI Hero, banner narrativo, grid 2x2, Top 3). Ejecutar el cierre formal del Sprint 1.5: curaduría de OBSERVATIONS.md, migración de items DEFERRED, actualización de STRATEGIC_DIRECTION.md, generación de case study en methodology/, y tag del sprint.
>
> **Pre-requisitos:** Etapas 1-10 cerradas. CI 100% verde. 3 PDFs baseline en `tests/e2e/__output__/sprint-1-5-final/`. Docs `COVERAGE_STATE.md` y `QA_CHECKLIST.md` creados.
>
> **Etapa siguiente:** apertura de Sprint 2 (etapa por planificar, no parte de este sprint).
---
## 1. Esta etapa tiene dos partes diferenciadas
**Parte A — Polish visual** (mitad de la etapa): refinamientos del PDF ROI página 1.
**Parte B — Cierre formal** (otra mitad): curaduría de archivos del sprint.
Hacé las dos partes EN ORDEN. La parte A primero. Cuando esté validada por el director, ejecutar parte B.
---
## 2. Parte A — Polish visual de página 1 PDF ROI
### 2.1 Alcance estricto de Parte A
Recoger los 4 refinamientos diferidos documentados en `TECH_DEBT.md` sección "Refinamientos visuales del PDF":
#### Refinamiento 1 — KPI Hero (gradiente naranja → magenta)
**Hallazgos del director en Etapa 6:**
- Cifra demasiado pegada al label "AHORRO PROYECTADO A 3 AÑOS"
- Tamaño de la cifra (USD 8.020.000,00) podría reducirse ligeramente
- Esquinas cuadradas — pasa, pero si se logra redondear, mejor
**Cambios propuestos:**
```
Label "AHORRO PROYECTADO A N AÑOS":
- Tamaño: 10pt (sin cambios)
- Margin-bottom: aumentar de 6mm a 9mm (más respiro hacia la cifra)
- Letter-spacing: 1.2pt (sin cambios)
Cifra principal (USD N.NNN.NNN):
- Tamaño: bajar de 30pt a 26pt (más balanceada)
- Tracking: -0.8pt (un poco menos cerrado)
- Margin-bottom: 5mm (sin cambios significativos)
Esquinas redondeadas (intento opcional):
- Si jsPDF permite recortar la imagen del gradiente con border-radius,
aplicar 6mm de radius
- Si requiere implementación compleja (cuatro triángulos blancos en
esquinas, máscara SVG, etc.), DEJAR esquinas cuadradas y reportar
- Tiempo máximo invertido: 30 minutos. Si no sale, pasar.
```
#### Refinamiento 2 — Banner narrativo
**Hallazgo del director:**
- "Texto corrido", no destaca cifras ni palabras clave
- Podría tener tratamiento más visual
**Cambios propuestos:**
```
Estructura del banner:
- Mantener fondo #FFF8ED + border-left 3px #F59845
- Mantener tamaño general del texto (11pt)
Tratamiento tipográfico del contenido:
- Las CIFRAS dentro del texto en peso 600 + color #B45309 (naranja oscuro):
- "5.000 ejecuciones"
- "3 años"
- "USD 80.000,00"
- "USD 540,00"
- "90,0%"
- "0.4 meses" (o equivalente del payback)
- El RESULTADO ("Resultado: ahorro de...") en peso 500
- El resto del texto en peso 400 (sin cambios)
Implementación técnica:
- jsPDF requiere usar doc.text() con setFont() para cada segmento
- Construir el banner como secuencia de fragmentos en lugar de párrafo
monolítico
- Alternativa: usar doc.text() con array de strings y posicionar
manualmente
```
#### Refinamiento 3 — Grid 2x2 KPIs secundarios
**Hallazgo del director:**
- "No tienen consistencia en el uso de tipografía"
- Verificar que los 4 cards sigan el mismo patrón visual
**Cambios propuestos:**
Auditar visualmente los 4 cards y unificar:
```
Card label superior (las 4 cards):
- Tamaño: 9pt (fijo, no variar)
- Peso: 500
- Color: #92400E
- Letter-spacing: 1pt
- Uppercase
- Margin-bottom: 5mm (fijo)
Card cifra principal (las 4 cards):
- Tamaño: 18pt (fijo, no variar entre cards)
- Peso: 700 (bold)
- Color: #0F172A
- Font-family: mono (alineación de números consistente)
- Margin-bottom: 3mm
Card delta/contexto (las 4 cards):
- Tamaño: 9pt (fijo)
- Peso: 500
- Color: #B45309
- Margin-top: 0 (después del 3mm de margin-bottom de la cifra)
```
**Específicamente verificar:**
- Card "AHORRO POR EJECUCIÓN": la línea "-90,0% vs. actual" debe tener mismo tamaño/peso/color que las deltas de los otros cards
- Card "PAYBACK": la línea "Dentro del horizonte" debe tener mismo tratamiento
- Card "AHORRO ANUAL": "× 3 años" debe tener mismo tratamiento
- Card "ROI ANUAL": "retorno anual" debe tener mismo tratamiento
#### Refinamiento 4 — Top 3 actividades
**Hallazgos del director:**
- Título "TOP 3 ACTIVIDADES POR AHORRO" se ve pequeño
- Las barras no tienen fondo para contraste
**Cambios propuestos:**
```
Título de la sección:
- Texto: "TOP 3 ACTIVIDADES POR AHORRO"
- Tamaño: aumentar de 9pt a 11pt
- Peso: 600 (más bold)
- Color: #92400E (naranja oscuro, en vez de gris)
- Letter-spacing: 1.2pt
- Margin-bottom: 8mm (más respiro hacia las barras)
Barras horizontales:
- Track (fondo): mantener #FED7AA (inq-orange-100)
- Fill: mantener #F59845 (inq-orange)
- Aumentar visibilidad del track: agregar border 0.3pt #F59845 en track
para definir el área completa
- Border-radius: 1mm en track y fill
Layout fila:
- Mantener grid 3 columnas (nombre 40% / barra 40% / cifra 20%)
- Gap entre filas: 5mm (un poco más que antes)
```
### 2.2 Validación visual obligatoria de Parte A
Cuando Claude Code termine los 4 refinamientos, **NO HACER PARTE B AÚN**. Generar el PDF ROI actualizado:
```
tests/e2e/__output__/etapa-11-roi.pdf
```
Y presentarlo para validación del director. El director va a:
1. Comparar la página 1 con el PDF de Etapa 6 (donde se identificaron los problemas)
2. Decidir si los 4 refinamientos resuelven sus observaciones
3. O pedir ajustes específicos
**Solo después del OK explícito del director sobre Parte A, ejecutar Parte B.**
### 2.3 Iteración esperada
Es probable que la primera pasada no satisfaga al 100%. El director va a dar feedback específico tipo:
- "La cifra del KPI Hero todavía está muy grande, bajá a 24pt"
- "El banner no destaca lo suficiente, los números deberían ser color naranja oscuro más fuerte"
- "Las barras del Top 3 ahora tienen border, pero el border es muy delgado"
**Mentalidad correcta:** la primera pasada es punto de partida. La conversación visual con el director afina hasta llegar al resultado. Esperá 1-3 iteraciones.
---
## 3. Parte B — Cierre formal del sprint
**Ejecutar SOLO después del OK explícito del director sobre Parte A.**
### 3.1 Curaduría de OBSERVATIONS.md
Acciones específicas:
1. **Items con etiqueta `[RESOLVED]`**:
- Verificar que todos están realmente resueltos
- Trasladar a una sección final del archivo "Historial de Sprint 1.5" o eliminarlos según preferencia del director
2. **Items con etiqueta `[DEFERRED-TECH-DEBT]`**:
- Verificar que ya están migrados a `TECH_DEBT.md`
- Si NO están migrados, migrarlos ahora
- Eliminar de `OBSERVATIONS.md`
3. **Items con etiqueta `[DEFERRED-BACKLOG]`**:
- Verificar que ya están migrados a `BACKLOG.md`
- Si NO están migrados, migrarlos ahora
- Eliminar de `OBSERVATIONS.md`
4. **Items con etiqueta `[CURRENT-SPRINT]`**:
- Si están resueltos por Parte A (polish final), marcarlos `[RESOLVED]`
- Si NO están resueltos, **PARAR Y CONSULTAR**: ¿se difieren a Sprint 2 o se cierran como deuda asumida?
5. **Vaciar el archivo dejando estructura para Sprint 2**:
```markdown
# OBSERVATIONS — Sprint 2
> Hallazgos descubiertos durante validación visual del sprint actual.
> Este archivo se vacía al cerrar el sprint.
(Sin hallazgos activos. Sprint 2 inicia.)
## Convención de etiquetas
[mantener la convención de etiquetas como referencia]
```
### 3.2 Actualización de STRATEGIC_DIRECTION.md
Agregar al final del archivo una nueva sección con reflexiones del Sprint 1.5:
```markdown
## Reflexiones del cierre de Sprint 1.5
### Lo que funcionó
- [Capturar 3-5 cosas que el director identificó como aprendizajes positivos]
- Ejemplo esperable: "Política de iniciativa proactiva con consulta clasificada
redujo errores de scope después de Etapa 8"
### Lo que costó más de lo esperado
- [Capturar 2-3 dificultades específicas]
- Ejemplo esperable: "El gradiente del KPI Hero requirió varias iteraciones"
### Decisiones arquitectónicas tomadas durante el sprint
- ASCII puro en fórmulas matemáticas (en lugar de Σ Unicode)
- Layout híbrido portrait/landscape para PDFs
- Jerarquía visual: KPI Hero exclusivo del PDF ROI
- Badge ⚡ con posición basada en `left` (no `right`)
- Permisos de Claude Code versionados en .claude/
### Estado de las definiciones del producto
- Identidad visual: estable hasta Sprint 4+
- Posicionamiento de mercado: pendiente de consolidar con feedback real
- Métricas de éxito: pendientes de definir con datos de uso
### Anticipación para Sprint 2
[Espacio para que el director complete cuando arranque Sprint 2]
```
### 3.3 Generación de case study
Crear `methodology/CASE_STUDIES/inq-roi-sprint-1-5.md` con resumen del sprint para uso futuro del director con su equipo.
Estructura:
```markdown
# Case Study — InQ ROI Sprint 1.5: Rediseño visual con identidad InQ
## Contexto inicial
- Producto: InQ ROI, herramienta interna de InQuality
- Director: Marcos (CEO/fundador InQuality)
- Equipo de ejecución: Claude Code (un solo agente) + Claude Sonnet (asesor)
- Duración planeada: 11 etapas
- Duración real: 11 etapas + 1 fix puntual entre Etapas 10 y 11
## Alcance del sprint
[Lista de qué se construyó: identidad visual, sistema de design,
3 PDFs rediseñados, badge en web, metodología documentada]
## Aplicación del método
### Patrones aplicados con casos concretos
[Mencionar cada patrón usado durante el sprint con el caso específico
donde se aplicó:]
- A.1 Brief versionado: BRIEF.md de 852 líneas, base del sprint completo
- A.2 Mockup como contrato: mockups iterados pre-implementación
- B.4 Reglas no negociables en CLAUDE.md: marca "InQ", política de
iniciativa proactiva
- C.1 Validación visual mandatoria: cazó truncamiento de notas en Etapa 5
- C.3 Auditoría adversarial: detectó decisión proactiva no autorizada
en Etapa 3
- C.6 Iniciativa agéntica con consulta clasificada: refinada en Etapas 5 y 8
- D.4 Permisos versionados en .claude/: aplicado mid-sprint
### Anti-patrones detectados con casos concretos
- AP.3 Racionalización de output anómalo: detectado en validación
visual de Etapa 5 (compactación de páginas)
- AP.8 Adelanto silencioso de deuda técnica: detectado en Etapa 7-8
(reescritura de fórmulas Σ → ASCII)
### Cosechas del método originadas en este sprint
- Refinamiento del Patrón C.6 con criterios explícitos de clasificación
- Nuevo Anti-Patrón AP.7: Bypass total de permisos en desarrollo local
- Nuevo Anti-Patrón AP.8: Adelanto silencioso de deuda técnica
- Nuevo Patrón D.4: Permisos del agente versionados en .claude/
- Extensión del Principio 3: "El auditor también debe ser auditado"
## Lecciones aprendidas
[3-5 lecciones operativas que el director identificó como transferibles
al equipo]
## Métricas
- Etapas: 11 (+ 1 fix puntual)
- Tests: 526 unit + 38 E2E al cierre
- Archivos modificados/creados: [contar de los commits]
- Decisiones de iniciativa: total y desglose por nivel
- Casos de mala clasificación de iniciativa: 2 (Etapas 5 y 7-8)
## Material para transmisión al equipo
[Sugerencias de qué partes de este case study usar en las 4 sesiones
× 1h de transmisión del método al equipo de 5 técnicos de InQuality]
```
### 3.4 Tag final del sprint
```bash
git tag sprint-1-5-cerrado
git push --tags
```
### 3.5 Validación final del cierre
Antes de declarar Etapa 11 (y Sprint 1.5) cerrados:
- [ ] OBSERVATIONS.md vaciado o resetado para Sprint 2
- [ ] STRATEGIC_DIRECTION.md actualizado con reflexiones del sprint
- [ ] CASE_STUDIES/inq-roi-sprint-1-5.md creado
- [ ] Tag git `sprint-1-5-cerrado` creado
- [ ] Suite completa de tests sigue 100% verde
- [ ] CI limpio
---
## 4. Validaciones automatizadas
```
Parte A — Polish visual:
1. PDF ROI etapa-11-roi.pdf generado, 5 páginas
2. Página 1: cifra del KPI Hero ≤ 26pt
3. Página 1: banner narrativo con cifras destacadas (verificar fragmentos
de texto con setFont en peso 600)
4. Página 1: grid 2x2 con tamaños consistentes en los 4 cards
5. Página 1: título "TOP 3 ACTIVIDADES POR AHORRO" en color naranja
oscuro (#92400E)
6. PDFs Actual y Automatizado: SIN CAMBIOS respecto a Etapa 8
7. Tests previos siguen verdes (526 unit + 38 E2E)
Parte B — Cierre formal:
8. OBSERVATIONS.md tiene estructura limpia para Sprint 2
9. STRATEGIC_DIRECTION.md tiene nueva sección de reflexiones
10. methodology/CASE_STUDIES/inq-roi-sprint-1-5.md existe y tiene
estructura mínima esperada
11. Tag git sprint-1-5-cerrado existe en local (push pendiente del director)
```
---
## 5. Entregables (Definition of Done)
```
PARTE A:
[ ] 4 refinamientos del PDF ROI página 1 aplicados según especificación
[ ] PDF ROI etapa-11-roi.pdf generado para validación visual
[ ] PDFs Actual y Automatizado NO modificados
[ ] Tests previos verdes
[ ] OK explícito del director sobre Parte A antes de avanzar
PARTE B:
[ ] OBSERVATIONS.md curado y reseteado para Sprint 2
[ ] STRATEGIC_DIRECTION.md actualizado con reflexiones
[ ] methodology/CASE_STUDIES/inq-roi-sprint-1-5.md creado
[ ] Tag git sprint-1-5-cerrado local
[ ] CI 100% verde
[ ] Reporte final entregado
```
---
## 6. Reporte final a entregar
1. **Tabla de archivos modificados/creados** (parte A + parte B separadas)
2. **PDF ROI etapa-11-roi.pdf presentado** con path absoluto
3. **Validaciones automatizadas confirmadas** (11 puntos)
4. **Resumen de cambios en cada refinamiento**: qué se modificó exactamente
5. **Decisiones tomadas durante refinamientos** (especialmente sobre
esquinas redondeadas del KPI Hero — sí/no se logró)
6. **Resumen de curaduría**: cuántos items procesados de cada estado
([RESOLVED], [DEFERRED-*], [CURRENT-SPRINT])
7. **Confirmación de tag git** y estado final del sprint
8. **Decisiones de iniciativa proactiva** según política CLAUDE.md
---
## 7. Reglas operativas
1. **PARTE A ANTES DE PARTE B.** No mezclar. Cuando Parte A está validada
por el director, recién entonces ejecutar Parte B.
2. **ITERACIÓN EN PARTE A.** Los refinamientos visuales pueden requerir
2-3 pasadas. Eso es normal. Mantener iteraciones acotadas a feedback
específico del director, sin re-rediseñar de cero.
3. **NO TOCAR PDFs Actual y Automatizado.** Cerrados en Etapa 8.
4. **NO MODIFICAR código no relacionado con los 4 refinamientos** durante
Parte A. Es polish, no refactor.
5. **EN PARTE B, las decisiones de curaduría son del director.** Si un
item DEFERRED no está claro a dónde migrar, consultar. Si un item
CURRENT-SPRINT no se resolvió en el sprint, consultar antes de
diferirlo automáticamente.
6. **Política de iniciativa refinada (post-Etapa 8):**
- Cambios en código fuera de los 4 refinamientos: Nivel 3 — consultar
- Cambios en archivos de documentación dentro del scope de Parte B:
Nivel 2 — reportar
- Eliminar items de OBSERVATIONS.md: SIEMPRE Nivel 2 (reportar lo
que se eliminó/movió)
7. **Si encontrás contradicción** entre prompt y código: PARÁ y consultá.
8. **Si la iteración de Parte A se extiende mucho**, evaluar con el
director si vale la pena cerrar Etapa 11 con el polish "razonable"
y diferir refinamientos finos a una etapa de mantenimiento en Sprint 2.
---
## 8. Por qué esta etapa importa
Etapa 11 es **el cierre del primer sprint donde se construyó el método mientras se construía el producto**. Lo que decidas acá determina:
1. La calidad final del entregable visual que va a clientes
2. La calidad del registro del sprint (case study + reflexiones)
3. La capacidad de transmitir el método a tu equipo con material concreto
**No te apures con esta etapa.** Si Parte A requiere 3 iteraciones, está bien. Si Parte B requiere 1 hora de curaduría con decisiones por item, está bien.
El éxito de Etapa 11 NO se mide en velocidad. Se mide en:
- Página 1 del PDF ROI alcanza el nivel que vos pretendías al inicio del sprint
- Sprint 1.5 cierra con artefactos transferibles (case study + docs actualizados)
- Tu equipo puede ver Sprint 1.5 como ejemplo concreto del método en acción
---
## 9. Después de Etapa 11
Cuando esta etapa cierre:
- Sprint 1.5 queda completamente cerrado
- Tu siguiente trabajo es planificar Sprint 2 con la dirección estratégica documentada (rebalanceo web/PDF, criterio de wow obligatorio)
- Las 4 sesiones × 1h de transmisión metodológica al equipo pueden arrancar usando el case study como material
Es el momento de inflexión. Sprint 1.5 fue donde construiste vocabulario, patrones y método. Sprint 2 va a ser donde vos aplicás eso para producir features de valor, y donde tu equipo eventualmente toma el método y lo usa.
Esta es la última etapa donde estamos todos enfocados solo en InQ ROI. Después, el método cobra vida propia.

View File

@@ -172,11 +172,19 @@ function _drawNarrativeBanner(
const tx = m + borderW + padH const tx = m + borderW + padH
let ty = y + padV let ty = y + padV
// Contexto (línea 1): peso 400, gris — información de fondo
doc.setFont('helvetica', 'normal') doc.setFont('helvetica', 'normal')
doc.setFontSize(9) doc.setFontSize(9)
doc.setTextColor(...PDF_COLORS.textSecondary) doc.setTextColor(...PDF_COLORS.textSecondary)
doc.text(l1, tx, ty) doc.text(l1, tx, ty)
ty += l1.length * 4.2 + 3 ty += l1.length * 4.2 + 4
// Resultado (línea 2): peso 700, naranja oscuro — cifras y conclusión destacadas
// Refinamiento: contraste visual entre contexto y resultado
doc.setFont('helvetica', 'bold')
doc.setFontSize(9)
doc.setTextColor(...C.orangeDark)
doc.text(l2, tx, ty) doc.text(l2, tx, ty)
return y + boxH + 8 return y + boxH + 8
@@ -184,17 +192,41 @@ function _drawNarrativeBanner(
// ── 5.4 KPI Hero con gradiente ──────────────────────────────────────────────── // ── 5.4 KPI Hero con gradiente ────────────────────────────────────────────────
function _createGradientDataUrl(wMm: number, hMm: number): string | null { function _createGradientDataUrl(wMm: number, hMm: number, radiusMm = 0): string | null {
if (typeof document === 'undefined') return null if (typeof document === 'undefined') return null
try { try {
const scale = 3 const scale = 3
const w = Math.round(wMm * scale) const w = Math.round(wMm * scale)
const h = Math.round(hMm * scale) const h = Math.round(hMm * scale)
const r = Math.round(radiusMm * scale)
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
canvas.width = w canvas.width = w
canvas.height = h canvas.height = h
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) return null if (!ctx) return null
// Fondo blanco opaco — las esquinas fuera del clip quedan blancas,
// lo que se ve igual que bordes redondeados sobre página blanca sin
// depender de la transparencia PNG en jsPDF.
ctx.fillStyle = '#FFFFFF'
ctx.fillRect(0, 0, w, h)
// Clip al rounded rect antes de pintar el gradiente
if (r > 0) {
ctx.beginPath()
ctx.moveTo(r, 0)
ctx.lineTo(w - r, 0)
ctx.arcTo(w, 0, w, r, r)
ctx.lineTo(w, h - r)
ctx.arcTo(w, h, w - r, h, r)
ctx.lineTo(r, h)
ctx.arcTo(0, h, 0, h - r, r)
ctx.lineTo(0, r)
ctx.arcTo(0, 0, r, 0, r)
ctx.closePath()
ctx.clip()
}
const grad = ctx.createLinearGradient(0, 0, w, h) // 135deg approx const grad = ctx.createLinearGradient(0, 0, w, h) // 135deg approx
grad.addColorStop(0, '#F59845') grad.addColorStop(0, '#F59845')
grad.addColorStop(1, '#ED3E8F') grad.addColorStop(1, '#ED3E8F')
@@ -227,32 +259,28 @@ function _drawHeroCard(
): number { ): number {
const heroH = 46 const heroH = 46
// Gradiente de fondo // Gradiente con esquinas redondeadas baked en el PNG via canvas clip
const dataUrl = _createGradientDataUrl(cW, heroH) const dataUrl = _createGradientDataUrl(cW, heroH, 4)
if (dataUrl) { if (dataUrl) {
doc.addImage(dataUrl, 'PNG', m, y, cW, heroH) doc.addImage(dataUrl, 'PNG', m, y, cW, heroH)
} else { } else {
_drawGradientFallback(doc, m, y, cW, heroH) _drawGradientFallback(doc, m, y, cW, heroH)
} }
// Esquinas redondeadas — simular con rect blanco en las esquinas (Nivel 1)
// jsPDF no soporta clip en addImage, así que el gradiente queda sin border-radius.
// Aceptable para esta etapa.
// Texto sobre el gradiente // Texto sobre el gradiente
const heroX = m + 14 const heroX = m + 14
let ty = y + 12 let ty = y + 11
// Label superior // Label superior
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.setFontSize(9) doc.setFontSize(9)
doc.setTextColor(255, 255, 255) doc.setTextColor(255, 255, 255)
doc.text(`AHORRO PROYECTADO A ${process.analysisHorizonYears} AÑOS`, heroX, ty) doc.text(`AHORRO PROYECTADO A ${process.analysisHorizonYears} AÑOS`, heroX, ty)
ty += 8 ty += 11 // Refinamiento: más espacio label→cifra (era 8mm)
// Cifra principal 28pt // Cifra principal — 26pt (Refinamiento: bajada de 28pt para mejor balance)
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.setFontSize(28) doc.setFontSize(26)
doc.setTextColor(...C.white) doc.setTextColor(...C.white)
doc.text(formatCurrency(roi.cumulativeSavingsHorizon, currency), heroX, ty) doc.text(formatCurrency(roi.cumulativeSavingsHorizon, currency), heroX, ty)
ty += 9 ty += 9
@@ -275,8 +303,9 @@ function _drawSecondaryGrid(
doc: DocLike, roi: RoiResult, process: Process, currency: string, doc: DocLike, roi: RoiResult, process: Process, currency: string,
m: number, cW: number, y: number m: number, cW: number, y: number
): number { ): number {
// Refinamiento: cardH aumentado de 24→28 para acomodar tipografía uniforme (9/18/9pt)
const cardW = (cW - 4) / 2 const cardW = (cW - 4) / 2
const cardH = 24 const cardH = 28
const gapX = 4, gapY = 4 const gapX = 4, gapY = 4
const r = 2 const r = 2
@@ -294,7 +323,7 @@ function _drawSecondaryGrid(
{ {
label: 'AHORRO ANUAL', label: 'AHORRO ANUAL',
value: formatCurrency(roi.annualSavings, currency), value: formatCurrency(roi.annualSavings, currency),
delta: `× ${process.analysisHorizonYears} años`, delta: `x ${process.analysisHorizonYears} años`,
col: 1, row: 0, col: 1, row: 0,
}, },
{ {
@@ -319,23 +348,23 @@ function _drawSecondaryGrid(
doc.setFillColor(...C.orangeLight) doc.setFillColor(...C.orangeLight)
doc.roundedRect(cx, cy, cardW, cardH, r, r, 'F') doc.roundedRect(cx, cy, cardW, cardH, r, r, 'F')
// Label // Label: 9pt uniforme en los 4 cards (Refinamiento: era 6.5pt variable)
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.setFontSize(6.5) doc.setFontSize(9)
doc.setTextColor(...C.orangeDarker) doc.setTextColor(...C.orangeDarker)
doc.text(card.label, cx + 5, cy + 6) doc.text(card.label, cx + 5, cy + 7)
// Valor // Valor: 18pt fijo en los 4 cards (Refinamiento: era variable 9-12pt)
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.setFontSize(card.value.length > 16 ? 9 : 12) doc.setFontSize(18)
doc.setTextColor(...PDF_COLORS.textPrimary) doc.setTextColor(...PDF_COLORS.textPrimary)
doc.text(card.value, cx + 5, cy + 14) doc.text(card.value, cx + 5, cy + 17)
// Delta // Delta: 9pt uniforme en los 4 cards (Refinamiento: era 7.5pt)
doc.setFont('helvetica', 'normal') doc.setFont('helvetica', 'normal')
doc.setFontSize(7.5) doc.setFontSize(9)
doc.setTextColor(...C.orangeDark) doc.setTextColor(...C.orangeDark)
doc.text(card.delta, cx + 5, cy + 20) doc.text(card.delta, cx + 5, cy + 24)
maxY = Math.max(maxY, cy + cardH) maxY = Math.max(maxY, cy + cardH)
} }
@@ -362,12 +391,12 @@ function _drawTop3Activities(
.sort((a, b) => b.savings - a.savings) .sort((a, b) => b.savings - a.savings)
.slice(0, 3) .slice(0, 3)
// Título // Título: Refinamiento — 11pt, naranja oscuro (era 8pt, gris)
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.setFontSize(8) doc.setFontSize(11)
doc.setTextColor(...PDF_COLORS.textDisabled) doc.setTextColor(...C.orangeDarker)
doc.text('TOP 3 ACTIVIDADES POR AHORRO', m, y) doc.text('TOP 3 ACTIVIDADES POR AHORRO', m, y)
y += 7 y += 10
if (top3.length === 0) { if (top3.length === 0) {
doc.setFont('helvetica', 'normal') doc.setFont('helvetica', 'normal')
@@ -395,9 +424,13 @@ function _drawTop3Activities(
doc.setTextColor(...PDF_COLORS.textPrimary) doc.setTextColor(...PDF_COLORS.textPrimary)
doc.text(name, m, y + barH - 1) doc.text(name, m, y + barH - 1)
// Barra de fondo // Barra de fondo (track) con border naranja para definir el área completa
doc.setFillColor(...C.orangeBar) doc.setFillColor(...C.orangeBar)
doc.roundedRect(m + nameColW, y, barColW, barH, 1, 1, 'F') doc.roundedRect(m + nameColW, y, barColW, barH, 1, 1, 'F')
doc.setDrawColor(...C.heroOrange)
doc.setLineWidth(0.3)
doc.roundedRect(m + nameColW, y, barColW, barH, 1, 1, 'S')
doc.setLineWidth(0.2)
// Barra de fill // Barra de fill
if (barFill > 0) { if (barFill > 0) {
@@ -1011,11 +1044,19 @@ export function drawTrajectoryChart(
// Máximo // Máximo
doc.text(_fmtAxisVal(yMax), chartLeft - 2, chartTop + 4, { align: 'right' } as Record<string, unknown>) doc.text(_fmtAxisVal(yMax), chartLeft - 2, chartTop + 4, { align: 'right' } as Record<string, unknown>)
// Cero (solo si hay rango negativo) // Cero y mínimo: cuando la línea cero queda casi en el borde inferior (inversión
// muy pequeña relativa al ahorro), los labels "0" y yMin se solapan con "Año 0".
// En ese caso mostramos "0" elevado 4mm dentro del área del gráfico y omitimos
// yMin (la zona negativa es visualmente invisible; el dato está en la anotación).
if (yMin < 0) { if (yMin < 0) {
const minSepMm = 8
const zeroNearBottom = (chartBottom - zeroY) < minSepMm
if (!zeroNearBottom) {
doc.text('0', chartLeft - 2, zeroY + 1, { align: 'right' } as Record<string, unknown>) doc.text('0', chartLeft - 2, zeroY + 1, { align: 'right' } as Record<string, unknown>)
// Mínimo
doc.text(_fmtAxisVal(yMin), chartLeft - 2, chartBottom, { align: 'right' } as Record<string, unknown>) doc.text(_fmtAxisVal(yMin), chartLeft - 2, chartBottom, { align: 'right' } as Record<string, unknown>)
} else {
doc.text('0', chartLeft - 2, chartBottom - 4, { align: 'right' } as Record<string, unknown>)
}
} }
// 7. Marker de payback (línea vertical punteada, solo si hay payback en horizonte) // 7. Marker de payback (línea vertical punteada, solo si hay payback en horizonte)

View File

@@ -0,0 +1,98 @@
/**
* Etapa 11 Parte A — Genera el PDF ROI con los 4 refinamientos del polish final.
* Guarda en tests/e2e/__output__/etapa-11-roi.pdf para validación visual del director.
*/
import { test, expect } from '@playwright/test'
import { resolve } from 'path'
import { mkdirSync, readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { PDFParse } from 'pdf-parse'
import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const BPMN_DIR = resolve(__dirname, '../../public/sample-processes')
const OUT_DIR = resolve(__dirname, './__output__')
test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) })
async function parsePdf(buffer: Buffer) {
const parser = new PDFParse({ data: buffer })
const result = await parser.getText()
await parser.destroy()
return { numpages: result.total, text: result.text }
}
async function getOrientations(buffer: Buffer): Promise<string[]> {
const uint8 = new Uint8Array(buffer)
const pdf = await getDocument({ data: uint8 }).promise
const out: string[] = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const vp = page.getViewport({ scale: 1 })
out.push(vp.width > vp.height ? 'landscape' : 'portrait')
}
return out
}
test('etapa-11-roi.pdf — polish final página 1 ROI', async ({ page }) => {
await page.goto('/')
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn'))
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
await page.waitForTimeout(800)
for (const [id, cost, auto] of [
['task_recv', 200, 20], ['task_score', 200, 20], ['task_analisis', 200, 20]
] as [string, number, number][]) {
await page.locator(`[data-element-id="${id}"]`).first().click()
await page.waitForTimeout(400)
const toggle = page.getByRole('switch', { name: /automatizable/i })
if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click()
await page.waitForTimeout(200)
await page.getByLabel(/Costo directo fijo/i).fill(String(cost))
await page.getByLabel(/Costo automatizado/i).fill(String(auto))
await page.getByLabel(/Tiempo automatizado/i).fill('10')
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
}
await page.getByRole('tab', { name: /Global/i }).click()
await page.waitForTimeout(300)
await page.getByLabel(/Frecuencia anual/i).fill('5000')
await page.getByLabel(/Inversión en automatización/i).fill('80000')
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
await page.getByRole('button', { name: 'Simular' }).click()
await page.waitForURL(/\/report\//, { timeout: 20_000 })
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 })
await page.waitForSelector('.bpmn-container .djs-group', { timeout: 30_000 })
await page.waitForTimeout(800)
await page.getByRole('tab', { name: /Comparación/i }).click()
await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 })
await page.waitForTimeout(500)
const outPath = resolve(OUT_DIR, 'etapa-11-roi.pdf')
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 30_000 }),
page.getByRole('button', { name: 'Exportar PDF' }).click(),
])
await download.saveAs(outPath)
const buffer = readFileSync(outPath)
const sizeKB = (buffer.length / 1024).toFixed(1)
const { numpages, text } = await parsePdf(buffer)
const orientations = await getOrientations(buffer)
console.log(`etapa-11-roi.pdf: ${numpages} págs, ${sizeKB} KB`)
console.log(`Texto p.1 (500 chars): ${text.slice(0, 500).replace(/\n/g, ' ')}`)
console.log(`Orientaciones: ${orientations.join(', ')}`)
expect(numpages).toBe(5)
expect(orientations).toEqual(['portrait', 'portrait', 'portrait', 'portrait', 'portrait'])
expect(text).toMatch(/AHORRO PROYECTADO A/)
expect(text).toMatch(/TOP 3 ACTIVIDADES POR AHORRO/)
expect(text).toMatch(/InQ ROI/)
expect(text).toMatch(/Powered by InQuality/)
})

View File

@@ -114,7 +114,7 @@ function validatePdfText(text: string, filename: string) {
// ─── Tests ──────────────────────────────────────────────────────────────────── // ─── Tests ────────────────────────────────────────────────────────────────────
test('etapa-4-actual.pdf — 3 páginas, marca InQ, sin términos prohibidos', async ({ page }) => { test('etapa-4-actual.pdf — 4 páginas, marca InQ, sin términos prohibidos', async ({ page }) => {
await setupAndSimulate(page) await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-4-actual.pdf') const outPath = resolve(OUT_DIR, 'etapa-4-actual.pdf')
@@ -125,17 +125,17 @@ test('etapa-4-actual.pdf — 3 páginas, marca InQ, sin términos prohibidos', a
const buffer = readFileSync(outPath) const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024 const sizeKB = buffer.length / 1024
console.log(`Tamaño: ${sizeKB.toFixed(1)} KB`) console.log(`Tamaño: ${sizeKB.toFixed(1)} KB`)
expect(sizeKB, 'Tamaño total > 15 KB por página').toBeGreaterThan(15 * 3) expect(sizeKB, 'Tamaño total > 15 KB por página').toBeGreaterThan(15 * 4)
expect(sizeKB, 'Tamaño total < 80 KB por página (3 págs)').toBeLessThan(80 * 3) expect(sizeKB, 'Tamaño total < 80 KB por página (4 págs)').toBeLessThan(80 * 4)
const { numpages, text } = await parsePdf(buffer) const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`) console.log(`Páginas: ${numpages}`)
console.log(`Primeros 300 chars: ${text.slice(0, 300).replace(/\n/g, ' ')}`) console.log(`Primeros 300 chars: ${text.slice(0, 300).replace(/\n/g, ' ')}`)
expect(numpages).toBe(3) expect(numpages).toBe(4)
validatePdfText(text, 'etapa-4-actual.pdf') validatePdfText(text, 'etapa-4-actual.pdf')
}) })
test('etapa-4-automatizado.pdf — 3 páginas, marca InQ, sin términos prohibidos', async ({ page }) => { test('etapa-4-automatizado.pdf — 4 páginas, marca InQ, sin términos prohibidos', async ({ page }) => {
await setupAndSimulate(page) await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-4-automatizado.pdf') const outPath = resolve(OUT_DIR, 'etapa-4-automatizado.pdf')
@@ -145,16 +145,16 @@ test('etapa-4-automatizado.pdf — 3 páginas, marca InQ, sin términos prohibid
const buffer = readFileSync(outPath) const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024 const sizeKB = buffer.length / 1024
console.log(`etapa-4-automatizado.pdf: ${sizeKB.toFixed(1)} KB`) console.log(`etapa-4-automatizado.pdf: ${sizeKB.toFixed(1)} KB`)
expect(sizeKB).toBeGreaterThan(15 * 3) expect(sizeKB).toBeGreaterThan(15 * 4)
expect(sizeKB).toBeLessThan(80 * 3) expect(sizeKB).toBeLessThan(80 * 4)
const { numpages, text } = await parsePdf(buffer) const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`) console.log(`Páginas: ${numpages}`)
expect(numpages).toBe(3) expect(numpages).toBe(4)
validatePdfText(text, 'etapa-4-automatizado.pdf') validatePdfText(text, 'etapa-4-automatizado.pdf')
}) })
test('etapa-4-roi.pdf — 4 páginas, marca InQ, keywords ROI, sin términos prohibidos', async ({ page }) => { test('etapa-4-roi.pdf — 5 páginas, marca InQ, keywords ROI, sin términos prohibidos', async ({ page }) => {
await setupAndSimulate(page) await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-4-roi.pdf') const outPath = resolve(OUT_DIR, 'etapa-4-roi.pdf')
@@ -164,14 +164,14 @@ test('etapa-4-roi.pdf — 4 páginas, marca InQ, keywords ROI, sin términos pro
const buffer = readFileSync(outPath) const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024 const sizeKB = buffer.length / 1024
console.log(`etapa-4-roi.pdf: ${sizeKB.toFixed(1)} KB`) console.log(`etapa-4-roi.pdf: ${sizeKB.toFixed(1)} KB`)
// ROI PDF no tiene imagen BPMN embebida → tamaño por página es menor (~8-12 KB texto puro) // ROI PDF incluye imagen de gradiente en página 1 → tamaño mayor (~250 KB)
expect(sizeKB, 'ROI PDF debe tener contenido mínimo (4 páginas texto)').toBeGreaterThan(20) expect(sizeKB, 'ROI PDF debe tener contenido mínimo (5 páginas)').toBeGreaterThan(20)
expect(sizeKB, 'ROI PDF no debe ser irrazonablemente grande').toBeLessThan(300) expect(sizeKB, 'ROI PDF no debe ser irrazonablemente grande').toBeLessThan(400)
const { numpages, text } = await parsePdf(buffer) const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`) console.log(`Páginas: ${numpages}`)
console.log(`Primeros 400 chars: ${text.slice(0, 400).replace(/\n/g, ' ')}`) console.log(`Primeros 400 chars: ${text.slice(0, 400).replace(/\n/g, ' ')}`)
expect(numpages).toBe(4) expect(numpages).toBe(5)
validatePdfText(text, 'etapa-4-roi.pdf') validatePdfText(text, 'etapa-4-roi.pdf')
// Keywords específicos del PDF de ROI // Keywords específicos del PDF de ROI

View File

@@ -145,7 +145,7 @@ for (const c of CASES) {
expect(csvBytes, `CSV de ${c.file} debe ser > 100 bytes`).toBeGreaterThan(100) expect(csvBytes, `CSV de ${c.file} debe ser > 100 bytes`).toBeGreaterThan(100)
const csvText = csvBuffer.toString('utf-8') const csvText = csvBuffer.toString('utf-8')
expect(csvText, 'CSV debe tener BOM UTF-8').toMatch(/^/) expect(csvText, 'CSV debe tener BOM UTF-8').toMatch(/^/)
expect(csvText, 'CSV debe contener "Costo total"').toContain('Costo total') expect(csvText, 'CSV debe contener "Costo total"').toContain('Costo total')
expect(csvText, 'CSV debe contener la moneda').toContain(c.currency) expect(csvText, 'CSV debe contener la moneda').toContain(c.currency)
}) })

View File

@@ -191,7 +191,7 @@ test('Export ROI — flujo completo con medium-with-gateways', async ({ page })
const csvText = csvBuffer.toString('utf-8') const csvText = csvBuffer.toString('utf-8')
// BOM UTF-8 // BOM UTF-8
expect(csvText).toMatch(/^/) expect(csvText).toMatch(/^/)
// Metadata ROI presente // Metadata ROI presente
expect(csvText).toContain('# Ahorro por ejecución') expect(csvText).toContain('# Ahorro por ejecución')
@@ -200,7 +200,7 @@ test('Export ROI — flujo completo con medium-with-gateways', async ({ page })
expect(csvText).toContain('# Frecuencia anual: 12000') expect(csvText).toContain('# Frecuencia anual: 12000')
// 16 columnas en el header de datos // 16 columnas en el header de datos
const csvNoBom = csvText.startsWith('') ? csvText.slice(1) : csvText const csvNoBom = csvText.startsWith('') ? csvText.slice(1) : csvText
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())! const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
const colCount = headerLine.split('","').length const colCount = headerLine.split('","').length
console.log(` Columnas CSV: ${colCount}`) console.log(` Columnas CSV: ${colCount}`)

View File

@@ -19,7 +19,7 @@ test.beforeAll(() => {
test('prod-smoke — import → simulate → export PDF', async ({ page }) => { test('prod-smoke — import → simulate → export PDF', async ({ page }) => {
// ── 1. Landing page carga correctamente ────────────────────────────────── // ── 1. Landing page carga correctamente ──────────────────────────────────
await page.goto('/') await page.goto('/')
await expect(page).toHaveTitle(/Process Cost Platform/) await expect(page).toHaveTitle(/InQ ROI/)
// Verificar heading principal // Verificar heading principal
await expect(page.getByText('Cuantificá el costo operativo')).toBeVisible({ timeout: 10_000 }) await expect(page.getByText('Cuantificá el costo operativo')).toBeVisible({ timeout: 10_000 })
@@ -83,7 +83,7 @@ test('prod-smoke — import → simulate → export PDF', async ({ page }) => {
await dlCsv.saveAs(csvPath) await dlCsv.saveAs(csvPath)
const csvText = readFileSync(csvPath, 'utf-8') const csvText = readFileSync(csvPath, 'utf-8')
expect(csvText, 'CSV debe tener BOM UTF-8').toMatch(/^/) expect(csvText, 'CSV debe tener BOM UTF-8').toMatch(/^/)
expect(csvText, 'CSV debe contener encabezado').toContain('Costo total') expect(csvText, 'CSV debe contener encabezado').toContain('Costo total')
console.log(`\n✅ Prod smoke PASSED — https://process-cost-platform.pages.dev`) console.log(`\n✅ Prod smoke PASSED — https://process-cost-platform.pages.dev`)

View File

@@ -0,0 +1,132 @@
/**
* Etapa 10 Sprint 1.5 — Generación de PDFs baseline final del sprint.
* Genera los 3 PDFs de referencia en tests/e2e/__output__/sprint-1-5-final/
* NO es un test de regresión — es el artefacto baseline para Sprint 2.
*/
import { test, expect } from '@playwright/test'
import { resolve } from 'path'
import { mkdirSync, readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { PDFParse } from 'pdf-parse'
import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const BPMN_DIR = resolve(__dirname, '../../public/sample-processes')
const OUT_DIR = resolve(__dirname, './__output__/sprint-1-5-final')
test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) })
async function parsePdf(buffer: Buffer) {
const parser = new PDFParse({ data: buffer })
const result = await parser.getText()
await parser.destroy()
return { numpages: result.total, text: result.text }
}
async function getOrientations(buffer: Buffer): Promise<string[]> {
const uint8 = new Uint8Array(buffer)
const pdf = await getDocument({ data: uint8 }).promise
const out: string[] = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const vp = page.getViewport({ scale: 1 })
out.push(vp.width > vp.height ? 'landscape' : 'portrait')
}
return out
}
async function setupFullScenario(page: import('@playwright/test').Page) {
await page.goto('/')
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn'))
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
await page.waitForTimeout(800)
for (const [id, cost, auto] of [
['task_recv', 200, 20], ['task_score', 200, 20], ['task_analisis', 200, 20]
] as [string, number, number][]) {
await page.locator(`[data-element-id="${id}"]`).first().click()
await page.waitForTimeout(400)
const toggle = page.getByRole('switch', { name: /automatizable/i })
if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click()
await page.waitForTimeout(200)
await page.getByLabel(/Costo directo fijo/i).fill(String(cost))
await page.getByLabel(/Costo automatizado/i).fill(String(auto))
await page.getByLabel(/Tiempo automatizado/i).fill('10')
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
}
await page.getByRole('tab', { name: /Global/i }).click()
await page.waitForTimeout(300)
await page.getByLabel(/Frecuencia anual/i).fill('5000')
await page.getByLabel(/Inversión en automatización/i).fill('80000')
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
await page.getByRole('button', { name: 'Simular' }).click()
await page.waitForURL(/\/report\//, { timeout: 20_000 })
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 })
await page.waitForSelector('.bpmn-container .djs-group', { timeout: 30_000 })
await page.waitForTimeout(800)
}
async function downloadTab(page: import('@playwright/test').Page, tab: 'actual' | 'automatizado' | 'roi', outPath: string) {
if (tab === 'actual') {
await page.getByRole('tab', { name: 'Actual' }).click()
await page.waitForTimeout(400)
} else if (tab === 'automatizado') {
await page.getByRole('tab', { name: 'Automatizado' }).click()
await page.waitForTimeout(400)
} else {
await page.getByRole('tab', { name: /Comparación/i }).click()
await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 })
await page.waitForTimeout(500)
}
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 30_000 }),
page.getByRole('button', { name: 'Exportar PDF' }).click(),
])
await download.saveAs(outPath)
}
test('sprint-1-5-final: generar y validar 3 PDFs baseline', async ({ page }) => {
await setupFullScenario(page)
// PDF Actual
const actualPath = resolve(OUT_DIR, 'final-actual.pdf')
await downloadTab(page, 'actual', actualPath)
const actualBuf = readFileSync(actualPath)
const actualKB = (actualBuf.length / 1024).toFixed(1)
const { numpages: actualPages } = await parsePdf(actualBuf)
const actualOrientations = await getOrientations(actualBuf)
console.log(`final-actual.pdf: ${actualPages} págs, ${actualKB} KB, ${actualOrientations.join(',')}`)
expect(actualPages).toBe(4)
expect(actualOrientations).toEqual(['portrait', 'landscape', 'portrait', 'portrait'])
// PDF Automatizado
const autoPath = resolve(OUT_DIR, 'final-automatizado.pdf')
await downloadTab(page, 'automatizado', autoPath)
const autoBuf = readFileSync(autoPath)
const autoKB = (autoBuf.length / 1024).toFixed(1)
const { numpages: autoPages } = await parsePdf(autoBuf)
const autoOrientations = await getOrientations(autoBuf)
console.log(`final-automatizado.pdf: ${autoPages} págs, ${autoKB} KB, ${autoOrientations.join(',')}`)
expect(autoPages).toBe(4)
expect(autoOrientations).toEqual(['portrait', 'landscape', 'portrait', 'portrait'])
// PDF ROI
const roiPath = resolve(OUT_DIR, 'final-roi.pdf')
await downloadTab(page, 'roi', roiPath)
const roiBuf = readFileSync(roiPath)
const roiKB = (roiBuf.length / 1024).toFixed(1)
const { numpages: roiPages, text: roiText } = await parsePdf(roiBuf)
const roiOrientations = await getOrientations(roiBuf)
console.log(`final-roi.pdf: ${roiPages} págs, ${roiKB} KB, ${roiOrientations.join(',')}`)
expect(roiPages).toBe(5)
expect(roiOrientations).toEqual(['portrait', 'portrait', 'portrait', 'portrait', 'portrait'])
expect(roiText).toMatch(/AHORRO PROYECTADO A/)
expect(roiText).toMatch(/TRAYECTORIA DEL AHORRO ACUMULADO/)
expect(roiText).toMatch(/InQ ROI/)
expect(roiText).toMatch(/Powered by InQuality/)
})

View File

@@ -166,7 +166,7 @@ describe('PDF ROI — Página 1 rediseñada (Etapa 6)', () => {
expect(textCalls).toContain('TOP 3 ACTIVIDADES POR AHORRO') expect(textCalls).toContain('TOP 3 ACTIVIDADES POR AHORRO')
}) })
it('drawRoiPage1 usa setFontSize(28) para la cifra del KPI Hero', async () => { it('drawRoiPage1 usa setFontSize(26) para la cifra del KPI Hero (Etapa 11: bajada de 28→26pt)', async () => {
const { drawRoiPage1 } = await import('@/lib/export/pdf-sections-roi') const { drawRoiPage1 } = await import('@/lib/export/pdf-sections-roi')
drawRoiPage1( drawRoiPage1(
mockDoc as any, mockDoc as any,
@@ -178,7 +178,7 @@ describe('PDF ROI — Página 1 rediseñada (Etapa 6)', () => {
'USD' 'USD'
) )
const fontSizes = mockDoc.setFontSize.mock.calls.map((c: number[]) => c[0]) const fontSizes = mockDoc.setFontSize.mock.calls.map((c: number[]) => c[0])
expect(fontSizes).toContain(28) expect(fontSizes).toContain(26)
}) })
it('drawRoiPage1 dibuja rectángulos para la grilla 2x2 y barras top3', async () => { it('drawRoiPage1 dibuja rectángulos para la grilla 2x2 y barras top3', async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB