/** * 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$/) })