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
196
docs/COVERAGE_STATE.md
Normal 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
@@ -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 |
|
||||||
312
sprints/sprint-1-5/ETAPA_10_PROMPT.md
Normal 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.
|
||||||
471
sprints/sprint-1-5/ETAPA_11_PROMPT.md
Normal 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.
|
||||||
@@ -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) {
|
||||||
doc.text('0', chartLeft - 2, zeroY + 1, { align: 'right' } as Record<string, unknown>)
|
const minSepMm = 8
|
||||||
// Mínimo
|
const zeroNearBottom = (chartBottom - zeroY) < minSepMm
|
||||||
doc.text(_fmtAxisVal(yMin), chartLeft - 2, chartBottom, { align: 'right' } as Record<string, unknown>)
|
if (!zeroNearBottom) {
|
||||||
|
doc.text('0', chartLeft - 2, zeroY + 1, { 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)
|
||||||
|
|||||||
98
tests/e2e/etapa-11-polish.spec.ts
Normal 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/)
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
132
tests/e2e/sprint-1-5-final.spec.ts
Normal 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/)
|
||||||
|
})
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |