Files
inq-roi-simulador-web/tests/e2e/export-roi.spec.ts
2026-06-23 22:33:01 -03:00

284 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 — espera a que el botón esté habilitado (isDirty) antes de clickear,
// y a que vuelva a deshabilitarse (confirmación de guardado) antes de continuar.
await page.waitForSelector('[data-testid="save-activity-btn"]:not([disabled])', { timeout: 10_000 })
await page.click('[data-testid="save-activity-btn"]')
await page.waitForSelector('[data-testid="save-activity-btn"][disabled]', { timeout: 10_000 })
}
// ─── 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$/)
})