Antes de la última revisión de etapa 4. Sprint 1. Previo al ajuste de look n feel de app, reportes, nombre y colores.

This commit is contained in:
2026-05-15 22:14:53 -03:00
parent b6f31f4371
commit 8b71c6ee13
54 changed files with 6497 additions and 261 deletions

View File

@@ -0,0 +1,280 @@
/**
* 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'
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('/')
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$/)
})