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:
280
tests/e2e/export-roi.spec.ts
Normal file
280
tests/e2e/export-roi.spec.ts
Normal 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$/)
|
||||
})
|
||||
Reference in New Issue
Block a user