Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
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?
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
/**
|
||
* E2E: Verifica el export PDF y CSV del tab "Comparación + ROI".
|
||
*
|
||
* Flujo completo:
|
||
* 1. Importar medium-with-gateways
|
||
* 2. Configurar task_recv como automatizable ($50 de costo, 5 min)
|
||
* 3. Configurar volumetría e inversión en el tab Global
|
||
* 4. Simular ambos escenarios
|
||
* 5. Navegar al tab "Comparación + ROI"
|
||
* 6. Exportar PDF → verificar tamaño, páginas, keywords, acentos
|
||
* 7. Exportar CSV → verificar BOM, 16 columnas, metadata ROI
|
||
*
|
||
* Test adicional: con BPMN sin automatizables, los tabs ROI y Automatizado
|
||
* están disabled y los botones de export funcionan para el tab actual.
|
||
*
|
||
* Ejecutar: npx playwright test tests/e2e/export-roi.spec.ts
|
||
*/
|
||
import { test, expect } from '@playwright/test'
|
||
import { readFileSync, mkdirSync } from 'fs'
|
||
import { resolve } from 'path'
|
||
import { fileURLToPath } from 'url'
|
||
import { PDFParse } from 'pdf-parse'
|
||
|
||
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 OUTPUT_DIR = resolve(__dirname, '__output__')
|
||
|
||
test.beforeAll(() => {
|
||
mkdirSync(OUTPUT_DIR, { recursive: true })
|
||
})
|
||
|
||
async function parsePdf(buffer: Buffer): Promise<{ numpages: number; text: string }> {
|
||
const parser = new PDFParse({ data: buffer })
|
||
const result = await parser.getText()
|
||
await parser.destroy()
|
||
return { numpages: result.total, text: result.text }
|
||
}
|
||
|
||
// ─── Helper: importar BPMN y llegar al workspace ──────────────────────────────
|
||
|
||
async function importBpmn(page: import('@playwright/test').Page, bpmnFile: string) {
|
||
await page.goto('/import')
|
||
const fileInput = page.locator('#bpmn-file-input')
|
||
await fileInput.setInputFiles(resolve(BPMN_DIR, bpmnFile))
|
||
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
|
||
await page.waitForSelector('button:has-text("Simular")', { timeout: 15_000 })
|
||
await page.waitForTimeout(1_500)
|
||
}
|
||
|
||
// ─── Helper: configurar una actividad como automatizable ──────────────────────
|
||
|
||
async function makeAutomatable(
|
||
page: import('@playwright/test').Page,
|
||
elementId: string,
|
||
cost: number,
|
||
timeMin: number
|
||
) {
|
||
// Click en el elemento BPMN en el canvas
|
||
await page.locator(`[data-element-id="${elementId}"]`).first().click()
|
||
await page.waitForTimeout(500)
|
||
|
||
// Activar el toggle de automatizable
|
||
const toggle = page.getByRole('switch', { name: /automatizable/i })
|
||
if (await toggle.getAttribute('aria-checked') === 'false') {
|
||
await toggle.click()
|
||
await page.waitForTimeout(300)
|
||
}
|
||
|
||
// Ingresar costo automatizado
|
||
const costInput = page.getByLabel(/Costo automatizado/i)
|
||
await costInput.fill(String(cost))
|
||
|
||
// Ingresar tiempo automatizado
|
||
const timeInput = page.getByLabel(/Tiempo automatizado/i)
|
||
await timeInput.fill(String(timeMin))
|
||
|
||
// Guardar
|
||
const guardarBtn = page.getByRole('button', { name: /Guardar/i })
|
||
await guardarBtn.click()
|
||
await page.waitForTimeout(500)
|
||
}
|
||
|
||
// ─── Helper: configurar volumetría en tab Global ──────────────────────────────
|
||
|
||
async function configureGlobal(
|
||
page: import('@playwright/test').Page,
|
||
annualFrequency: number,
|
||
investment: number
|
||
) {
|
||
// Ir al tab Global
|
||
const globalTab = page.getByRole('tab', { name: /Global/i })
|
||
await globalTab.click()
|
||
await page.waitForTimeout(300)
|
||
|
||
// Frecuencia anual
|
||
const freqInput = page.getByLabel(/Frecuencia anual/i)
|
||
await freqInput.fill(String(annualFrequency))
|
||
|
||
// Inversión
|
||
const investInput = page.getByLabel(/Inversión en automatización/i)
|
||
await investInput.fill(String(investment))
|
||
|
||
// Guardar
|
||
const guardarBtn = page.getByRole('button', { name: /Guardar/i })
|
||
await guardarBtn.click()
|
||
await page.waitForTimeout(500)
|
||
}
|
||
|
||
// ─── Helper: simular y llegar al reporte ──────────────────────────────────────
|
||
|
||
async function simulateAndGoToReport(page: import('@playwright/test').Page) {
|
||
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: 30_000 })
|
||
await page.waitForSelector('.bpmn-container .djs-group', { timeout: 30_000 })
|
||
}
|
||
|
||
// ─── Test principal: flujo ROI completo ───────────────────────────────────────
|
||
|
||
test('Export ROI — flujo completo con medium-with-gateways', async ({ page }) => {
|
||
await importBpmn(page, 'medium-with-gateways.bpmn')
|
||
|
||
// Configurar task_recv como automatizable ($50, 5 min)
|
||
await makeAutomatable(page, 'task_recv', 50, 5)
|
||
|
||
// Configurar volumetría e inversión
|
||
await configureGlobal(page, 12_000, 50_000)
|
||
|
||
// Simular
|
||
await simulateAndGoToReport(page)
|
||
|
||
// Navegar al tab ROI
|
||
const roiTab = page.getByRole('tab', { name: /Comparación/i })
|
||
await expect(roiTab).not.toBeDisabled({ timeout: 5_000 })
|
||
await roiTab.click()
|
||
await page.waitForTimeout(500)
|
||
|
||
// ── PDF ROI ────────────────────────────────────────────────────────────────
|
||
const pdfPath = resolve(OUTPUT_DIR, 'roi-test.pdf')
|
||
const pdfDownload = page.waitForEvent('download', { timeout: 90_000 })
|
||
await page.getByRole('button', { name: 'Exportar PDF' }).click()
|
||
const dlPdf = await pdfDownload
|
||
await dlPdf.saveAs(pdfPath)
|
||
|
||
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 30_000 })
|
||
|
||
const pdfBuffer = readFileSync(pdfPath)
|
||
const pdfBytes = pdfBuffer.length
|
||
console.log(`\n📊 ROI PDF: ${pdfBytes} bytes (${(pdfBytes / 1024).toFixed(1)} KB)`)
|
||
|
||
// Tamaño razonable: < 400 KB
|
||
expect(pdfBytes).toBeGreaterThan(30_000)
|
||
expect(pdfBytes).toBeLessThan(400_000)
|
||
|
||
// Nombre con sufijo _roi
|
||
const downloadName = dlPdf.suggestedFilename()
|
||
console.log(` Nombre archivo: ${downloadName}`)
|
||
expect(downloadName).toMatch(/_roi\.pdf$/)
|
||
|
||
// Contenido mínimo
|
||
const { numpages, text } = await parsePdf(pdfBuffer)
|
||
console.log(` Páginas: ${numpages}`)
|
||
console.log(` Primeros 300 chars: ${text.slice(0, 300).replace(/\n/g, ' ')}`)
|
||
|
||
// Al menos 3 páginas (hay 4 en el ROI PDF)
|
||
expect(numpages).toBeGreaterThanOrEqual(3)
|
||
|
||
// Keywords que deben estar en el texto del PDF
|
||
expect(text).toMatch(/ROI|retorno|automatizaci/i)
|
||
expect(text).toMatch(/Payback|payback|recupera/i)
|
||
expect(text).toMatch(/ahorro|Ahorro/i)
|
||
expect(text).toContain('USD')
|
||
|
||
// ── CSV ROI ────────────────────────────────────────────────────────────────
|
||
const csvPath = resolve(OUTPUT_DIR, 'roi-test.csv')
|
||
const csvDownload = page.waitForEvent('download', { timeout: 30_000 })
|
||
await page.getByRole('button', { name: 'Exportar CSV' }).click()
|
||
const dlCsv = await csvDownload
|
||
await dlCsv.saveAs(csvPath)
|
||
|
||
const csvBuffer = readFileSync(csvPath)
|
||
const csvBytes = csvBuffer.length
|
||
console.log(` CSV: ${csvBytes} bytes`)
|
||
console.log(` Nombre archivo CSV: ${dlCsv.suggestedFilename()}`)
|
||
|
||
// Nombre con sufijo _roi
|
||
expect(dlCsv.suggestedFilename()).toMatch(/_roi\.csv$/)
|
||
|
||
const csvText = csvBuffer.toString('utf-8')
|
||
|
||
// BOM UTF-8
|
||
expect(csvText).toMatch(/^/)
|
||
|
||
// Metadata ROI presente
|
||
expect(csvText).toContain('# Ahorro por ejecución')
|
||
expect(csvText).toContain('# Payback (meses)')
|
||
expect(csvText).toContain('# ROI anualizado')
|
||
expect(csvText).toContain('# Frecuencia anual: 12000')
|
||
|
||
// 16 columnas en el header de datos
|
||
const csvNoBom = csvText.startsWith('') ? csvText.slice(1) : csvText
|
||
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
|
||
const colCount = headerLine.split('","').length
|
||
console.log(` Columnas CSV: ${colCount}`)
|
||
expect(colCount).toBe(16)
|
||
|
||
// Columna Automatizable presente
|
||
expect(csvText).toContain('Automatizable')
|
||
})
|
||
|
||
// ─── Test: sin automatizables → tab ROI disabled, export de "Actual" OK ──────
|
||
|
||
test('Tabs ROI/Automatizado disabled cuando no hay actividades automatizables', async ({ page }) => {
|
||
await importBpmn(page, 'simple-linear.bpmn')
|
||
await simulateAndGoToReport(page)
|
||
|
||
// Los tabs deben estar disabled
|
||
const autoTab = page.getByRole('tab', { name: /Automatizado/i })
|
||
const roiTab = page.getByRole('tab', { name: /Comparación/i })
|
||
|
||
await expect(autoTab).toBeDisabled()
|
||
await expect(roiTab).toBeDisabled()
|
||
|
||
// El export PDF/CSV del tab "Actual" sigue funcionando
|
||
const pdfPath = resolve(OUTPUT_DIR, 'no-automatable.pdf')
|
||
const pdfDownload = page.waitForEvent('download', { timeout: 90_000 })
|
||
await page.getByRole('button', { name: 'Exportar PDF' }).click()
|
||
const dlPdf = await pdfDownload
|
||
await dlPdf.saveAs(pdfPath)
|
||
|
||
const downloadName = dlPdf.suggestedFilename()
|
||
console.log(`\n📄 Sin automatizables — nombre PDF: ${downloadName}`)
|
||
expect(downloadName).toMatch(/_actual\.pdf$/)
|
||
|
||
const pdfBuffer = readFileSync(pdfPath)
|
||
expect(pdfBuffer.length).toBeGreaterThan(30_000)
|
||
|
||
const csvDownload = page.waitForEvent('download', { timeout: 30_000 })
|
||
await page.getByRole('button', { name: 'Exportar CSV' }).click()
|
||
const dlCsv = await csvDownload
|
||
|
||
console.log(` Nombre CSV: ${dlCsv.suggestedFilename()}`)
|
||
expect(dlCsv.suggestedFilename()).toMatch(/_actual\.csv$/)
|
||
})
|
||
|
||
// ─── Test: tab Automatizado → PDF y CSV con sufijo correcto ───────────────────
|
||
|
||
test('Export Automatizado — sufijo _automatizado en nombres de archivo', async ({ page }) => {
|
||
await importBpmn(page, 'medium-with-gateways.bpmn')
|
||
await makeAutomatable(page, 'task_recv', 30, 5)
|
||
await simulateAndGoToReport(page)
|
||
|
||
// Ir al tab Automatizado
|
||
const autoTab = page.getByRole('tab', { name: /^Automatizado$/i })
|
||
await expect(autoTab).not.toBeDisabled()
|
||
await autoTab.click()
|
||
await page.waitForTimeout(500)
|
||
|
||
// PDF con sufijo _automatizado
|
||
const pdfPath = resolve(OUTPUT_DIR, 'automatizado.pdf')
|
||
const pdfDownload = page.waitForEvent('download', { timeout: 90_000 })
|
||
await page.getByRole('button', { name: 'Exportar PDF' }).click()
|
||
const dlPdf = await pdfDownload
|
||
await dlPdf.saveAs(pdfPath)
|
||
|
||
const pdfName = dlPdf.suggestedFilename()
|
||
console.log(`\n⚙️ Automatizado PDF: ${pdfName}`)
|
||
expect(pdfName).toMatch(/_automatizado\.pdf$/)
|
||
|
||
const pdfBuffer = readFileSync(pdfPath)
|
||
expect(pdfBuffer.length).toBeGreaterThan(30_000)
|
||
|
||
// CSV con sufijo _automatizado
|
||
const csvDownload = page.waitForEvent('download', { timeout: 30_000 })
|
||
await page.getByRole('button', { name: 'Exportar CSV' }).click()
|
||
const dlCsv = await csvDownload
|
||
expect(dlCsv.suggestedFilename()).toMatch(/_automatizado\.csv$/)
|
||
})
|