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
let ty = y + padV
// Contexto (línea 1): peso 400, gris — información de fondo
doc.setFont('helvetica', 'normal')
doc.setFontSize(9)
doc.setTextColor(...PDF_COLORS.textSecondary)
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)
return y + boxH + 8
@@ -184,17 +192,41 @@ function _drawNarrativeBanner(
// ── 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
try {
const scale = 3
const w = Math.round(wMm * scale)
const h = Math.round(hMm * scale)
const r = Math.round(radiusMm * scale)
const canvas = document.createElement('canvas')
canvas.width = w
canvas.height = h
const ctx = canvas.getContext('2d')
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
grad.addColorStop(0, '#F59845')
grad.addColorStop(1, '#ED3E8F')
@@ -227,32 +259,28 @@ function _drawHeroCard(
): number {
const heroH = 46
// Gradiente de fondo
const dataUrl = _createGradientDataUrl(cW, heroH)
// Gradiente con esquinas redondeadas baked en el PNG via canvas clip
const dataUrl = _createGradientDataUrl(cW, heroH, 4)
if (dataUrl) {
doc.addImage(dataUrl, 'PNG', m, y, cW, heroH)
} else {
_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
const heroX = m + 14
let ty = y + 12
let ty = y + 11
// Label superior
doc.setFont('helvetica', 'bold')
doc.setFontSize(9)
doc.setTextColor(255, 255, 255)
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.setFontSize(28)
doc.setFontSize(26)
doc.setTextColor(...C.white)
doc.text(formatCurrency(roi.cumulativeSavingsHorizon, currency), heroX, ty)
ty += 9
@@ -275,8 +303,9 @@ function _drawSecondaryGrid(
doc: DocLike, roi: RoiResult, process: Process, currency: string,
m: number, cW: number, y: number
): number {
// Refinamiento: cardH aumentado de 24→28 para acomodar tipografía uniforme (9/18/9pt)
const cardW = (cW - 4) / 2
const cardH = 24
const cardH = 28
const gapX = 4, gapY = 4
const r = 2
@@ -294,7 +323,7 @@ function _drawSecondaryGrid(
{
label: 'AHORRO ANUAL',
value: formatCurrency(roi.annualSavings, currency),
delta: `× ${process.analysisHorizonYears} años`,
delta: `x ${process.analysisHorizonYears} años`,
col: 1, row: 0,
},
{
@@ -319,23 +348,23 @@ function _drawSecondaryGrid(
doc.setFillColor(...C.orangeLight)
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.setFontSize(6.5)
doc.setFontSize(9)
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.setFontSize(card.value.length > 16 ? 9 : 12)
doc.setFontSize(18)
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.setFontSize(7.5)
doc.setFontSize(9)
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)
}
@@ -362,12 +391,12 @@ function _drawTop3Activities(
.sort((a, b) => b.savings - a.savings)
.slice(0, 3)
// Título
// Título: Refinamiento — 11pt, naranja oscuro (era 8pt, gris)
doc.setFont('helvetica', 'bold')
doc.setFontSize(8)
doc.setTextColor(...PDF_COLORS.textDisabled)
doc.setFontSize(11)
doc.setTextColor(...C.orangeDarker)
doc.text('TOP 3 ACTIVIDADES POR AHORRO', m, y)
y += 7
y += 10
if (top3.length === 0) {
doc.setFont('helvetica', 'normal')
@@ -395,9 +424,13 @@ function _drawTop3Activities(
doc.setTextColor(...PDF_COLORS.textPrimary)
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.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
if (barFill > 0) {
@@ -1011,11 +1044,19 @@ export function drawTrajectoryChart(
// Máximo
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) {
const minSepMm = 8
const zeroNearBottom = (chartBottom - zeroY) < minSepMm
if (!zeroNearBottom) {
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>)
} 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)

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 ────────────────────────────────────────────────────────────────────
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)
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 sizeKB = buffer.length / 1024
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 < 80 KB por página (3 págs)').toBeLessThan(80 * 3)
expect(sizeKB, 'Tamaño total > 15 KB por página').toBeGreaterThan(15 * 4)
expect(sizeKB, 'Tamaño total < 80 KB por página (4 págs)').toBeLessThan(80 * 4)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
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')
})
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)
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 sizeKB = buffer.length / 1024
console.log(`etapa-4-automatizado.pdf: ${sizeKB.toFixed(1)} KB`)
expect(sizeKB).toBeGreaterThan(15 * 3)
expect(sizeKB).toBeLessThan(80 * 3)
expect(sizeKB).toBeGreaterThan(15 * 4)
expect(sizeKB).toBeLessThan(80 * 4)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
expect(numpages).toBe(3)
expect(numpages).toBe(4)
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)
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 sizeKB = buffer.length / 1024
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)
expect(sizeKB, 'ROI PDF debe tener contenido mínimo (4 páginas texto)').toBeGreaterThan(20)
expect(sizeKB, 'ROI PDF no debe ser irrazonablemente grande').toBeLessThan(300)
// ROI PDF incluye imagen de gradiente en página 1 → tamaño mayor (~250 KB)
expect(sizeKB, 'ROI PDF debe tener contenido mínimo (5 páginas)').toBeGreaterThan(20)
expect(sizeKB, 'ROI PDF no debe ser irrazonablemente grande').toBeLessThan(400)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
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')
// 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)
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 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')
// BOM UTF-8
expect(csvText).toMatch(/^/)
expect(csvText).toMatch(/^/)
// Metadata ROI presente
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')
// 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 colCount = headerLine.split('","').length
console.log(` Columnas CSV: ${colCount}`)

View File

@@ -19,7 +19,7 @@ test.beforeAll(() => {
test('prod-smoke — import → simulate → export PDF', async ({ page }) => {
// ── 1. Landing page carga correctamente ──────────────────────────────────
await page.goto('/')
await expect(page).toHaveTitle(/Process Cost Platform/)
await expect(page).toHaveTitle(/InQ ROI/)
// Verificar heading principal
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)
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')
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')
})
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')
drawRoiPage1(
mockDoc as any,
@@ -178,7 +178,7 @@ describe('PDF ROI — Página 1 rediseñada (Etapa 6)', () => {
'USD'
)
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 () => {

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