281 lines
11 KiB
TypeScript
281 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'
|
||
|
||
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$/)
|
||
})
|