Files
inq-roi-simulador-web/tests/e2e/payback-etapa-2.spec.ts
Marcos Benítez 84daf82e65
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Sprint 4 — Etapa 8: reporte de entrega
Tier 1 — Automatizado
Build:  limpio
Lint:  limpio en archivos nuevos de Sprint 4 (separé resource-types.ts de resource-display.tsx para sacar 2 warnings de Fast Refresh; quedan 0 errores/warnings nuevos). Quedó 1 error preexistente en ReportPage.tsx (no tocado desde Sprint 3, fuera de scope) y ~102 warnings no-explicit-any preexistentes en tests viejos.
Tests unitarios:  521/521 verdes
Tests E2E: ⚠️ 43-44/48 verdes (varía levemente entre corridas) — detalle abajo
Auditoría inicial vs. estado final
Antes: 39/39 specs (100%) fallaban — ROJO-AUTH puro, todos por #bpmn-file-input nunca apareciendo (esperaban el flujo pre-Sprint-4 directamente en /).

Decisión tomada (con tu input): Opción A — sesión real. Creaste el usuario martin.bernal@inquality.com.py; yo verifiqué que no tenía fila en public.users y se la inserté con platform_role='platform_admin'. Implementé tests/e2e/setup/auth-setup.ts como globalSetup de Playwright: hace signInWithPassword, inyecta la sesión real en localStorage con el formato exacto que usa supabase-js v2, y genera tests/e2e/setup/auth-state.json (gitignored) en cada corrida.

Fix adicional necesario: todos los specs navegaban a / esperando el input de archivo ahí — ahora vive en /import. Corregí los 18 page.goto('/') → /import, incluyendo reescribir el flujo obsoleto de prod-smoke.spec.ts (esperaba una landing pre-auth que ya no existe).

Resultado: 33→43-44 specs pasan. Quedan 5-6 ROJO-OTRO, con dos causas raíz identificadas (no relacionadas con auth, fuera del mandato de esta etapa que pide no reescribir lógica de tests):

Botón "Guardar" en ActivityPanel queda deshabilitado (3 specs de PDF) — encontré algo extraño: el texto del botón dice "Guardar cambios" (que solo se renderiza si isDirty=true) pero Playwright lo sigue viendo disabled. Esto contradice la lógica simple de disabled={!isDirty} en el componente — sospecho un bug real de re-render en producción, pero diagnosticarlo a fondo requiere debugging en vivo. Recomiendo abrir esto como bug dedicado en un sprint futuro.
2 specs usan IndexedDB/Dexie directamente (export-pdf.spec.ts, validate-dod-etapa4.spec.ts) — Dexie se eliminó completamente en Etapa 4. Estos tests necesitan reescritura para inyectar datos vía Supabase en vez de IndexedDB — eso sí es "reescribir lógica de tests", explícitamente fuera de este alcance.
Los resultados varían levemente entre corridas porque Playwright corre specs en paralelo bajo la misma cuenta de test compartida — puede haber interferencia de datos entre specs concurrentes. Sugiero como mejora futura (no implementada ahora): --workers=1 para diagnóstico determinístico.

Entregables nuevos
tests/e2e/setup/auth-setup.ts — global setup con sesión real
tests/e2e/sprint-4-smoke.spec.ts — 9 tests (4 sin auth: login, redirects de /, /workspace, /recursos; 5 con auth: biblioteca sin 500, sin 406 en simulations, catálogo carga, avatar visible, import navega a workspace)
docs/CHECKLIST_ENTREGA.md — las 4 tiers completas
.env.test (gitignored) + .env.example documentado
.gitignore actualizado (.env.test, auth-state.json)
Polish
Textos de OwnershipBadge y "Compartir con equipo" ya estaban en español — sin cambios.
Tooltip "Disponible próximamente" para "Compartir con equipo": no lo agregué, tal como pedía el propio brief, consultarte antes. ¿Lo querés?
2026-06-19 11:22:50 -03:00

177 lines
7.1 KiB
TypeScript

/**
* Etapa 2 Sprint 1.5 — Validación visual de formatPayback.
*
* 1. Screenshot del RoiKpiBar mostrando payback formateado (no "0.0 meses").
* 2. Screenshot de TransparencySection con ROI > 1000%.
* 3. PDF correcto del tab "Comparación + ROI" guardado para validación visual.
*
* Setup: medium-with-gateways, task_recv + task_score + task_analisis automatizables,
* $200 costo directo / $20 costo automatizado, 5000 ejecuciones/año, $80.000 inversión.
*
* Root cause del PDF erróneo en run anterior: el test exportaba antes de navegar al
* tab ROI, por lo que activeTab = 'actual' y se generaba el PDF del escenario actual.
* Fix: navegar al tab "Comparación + ROI" ANTES de exportar.
*/
import { test, expect } from '@playwright/test'
import { resolve } from 'path'
import { mkdirSync } from 'fs'
import { fileURLToPath } from 'url'
test.use({ storageState: 'tests/e2e/setup/auth-state.json' })
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 }) })
// ─── Helpers ──────────────────────────────────────────────────────────────────
async function importBpmn(page: import('@playwright/test').Page, filename: string) {
await page.goto('/import')
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename))
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
await page.waitForTimeout(1_000)
}
async function setActivityCosts(
page: import('@playwright/test').Page,
elementId: string,
directCost: number,
automatedCost: number,
automatedTime: number
) {
await page.locator(`[data-element-id="${elementId}"]`).first().click()
await page.waitForTimeout(500)
const toggle = page.getByRole('switch', { name: /automatizable/i })
if (await toggle.getAttribute('aria-checked') === 'false') {
await toggle.click()
await page.waitForTimeout(300)
}
const directCostInput = page.getByLabel(/Costo directo fijo/i)
await directCostInput.fill(String(directCost))
const autoCostInput = page.getByLabel(/Costo automatizado/i)
await autoCostInput.fill(String(automatedCost))
const autoTimeInput = page.getByLabel(/Tiempo automatizado/i)
await autoTimeInput.fill(String(automatedTime))
const guardar = page.getByRole('button', { name: /Guardar/i })
await guardar.click()
await page.waitForTimeout(500)
}
async function configureGlobal(
page: import('@playwright/test').Page,
annualFrequency: number,
investment: number
) {
const globalTab = page.getByRole('tab', { name: /Global/i })
await globalTab.click()
await page.waitForTimeout(300)
const freqInput = page.getByLabel(/Frecuencia anual/i)
await freqInput.fill(String(annualFrequency))
const investInput = page.getByLabel(/Inversión en automatización/i)
await investInput.fill(String(investment))
const guardar = page.getByRole('button', { name: /Guardar/i })
await guardar.click()
await page.waitForTimeout(500)
}
async function setupRealisticScenario(page: import('@playwright/test').Page) {
await importBpmn(page, 'medium-with-gateways.bpmn')
await setActivityCosts(page, 'task_recv', 200, 20, 10)
await setActivityCosts(page, 'task_score', 200, 20, 10)
await setActivityCosts(page, 'task_analisis', 200, 20, 10)
await configureGlobal(page, 5000, 80000)
const simBtn = page.getByRole('button', { name: 'Simular' })
await expect(simBtn).not.toBeDisabled({ timeout: 5_000 })
await simBtn.click()
await page.waitForURL(/\/report\//, { timeout: 20_000 })
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 })
await page.waitForTimeout(800)
// Navegar al tab Comparación + ROI — NECESARIO para que activeTab sea 'roi'
const roiTab = page.getByRole('tab', { name: /Comparación/i })
await expect(roiTab).not.toBeDisabled({ timeout: 5_000 })
await roiTab.click()
await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 })
await page.waitForTimeout(800)
}
// ─── Tests ────────────────────────────────────────────────────────────────────
test('screenshot RoiKpiBar — payback formateado (no "0.0 meses")', async ({ page }) => {
await setupRealisticScenario(page)
// Screenshot del viewport — muestra KPI bar
await page.screenshot({
path: resolve(OUT_DIR, 'payback-roi-kpi-bar.png'),
fullPage: false,
})
// Verificar que el payback card NO muestra "0.0 meses"
const cards = page.locator('[data-testid="roi-kpi-card"]')
const texts = await cards.allTextContents()
console.log('KPI card texts:', JSON.stringify(texts, null, 2))
const hasOldFormat = texts.some(t => /\b0\.0 meses\b/.test(t))
expect(hasOldFormat, 'Payback card must not show "0.0 meses" — use formatPayback').toBe(false)
})
test('screenshot TransparencySection — ROI > 1000% dispara aviso', async ({ page }) => {
await setupRealisticScenario(page)
// Verificar que ROI es > 1000% (condición para disparar Transparencia)
const cards = page.locator('[data-testid="roi-kpi-card"]')
const texts = await cards.allTextContents()
const roiText = texts.find(t => t.toLowerCase().includes('roi anual')) ?? ''
console.log('ROI card text:', roiText)
// Scroll hasta el final de la página para revelar TransparencySection
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await page.waitForTimeout(500)
// Verificar que la sección existe en el DOM
const transparency = page.locator('text=Transparencia metodológica').first()
await expect(transparency).toBeVisible({ timeout: 5_000 })
// Screenshot full-page para capturar la sección
await page.screenshot({
path: resolve(OUT_DIR, 'payback-transparency-section.png'),
fullPage: true,
})
console.log('TransparencySection visible con ROI:', roiText)
})
test('PDF ROI correcto — tab "Comparación + ROI" activo al exportar', async ({ page }) => {
// Root cause del PDF erróneo anterior: exportar con activeTab = 'actual'.
// Fix: setupRealisticScenario ya navega al tab ROI ANTES de este punto.
await setupRealisticScenario(page)
// Tab ROI ya activo — el BPMN canvas no existe en este tab, no esperar por él
await page.waitForTimeout(500)
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 30_000 }),
page.getByRole('button', { name: 'Exportar PDF' }).click(),
])
const outPath = resolve(OUT_DIR, 'etapa-2-payback-roi-sample.pdf')
await download.saveAs(outPath)
console.log('PDF ROI guardado:', outPath)
expect(download.suggestedFilename()).toMatch(/_roi\.pdf$|\.pdf$/)
// El nombre debe contener "roi" si la implementación usa activeTab en el slug
console.log('Nombre sugerido:', download.suggestedFilename())
})