Files
inq-roi-simulador-web/tests/e2e/etapa-4-pdfs.spec.ts
Marcos Benítez 49364f4b18 docs: cierre formal de Sprint 1.5
- OBSERVATIONS.md reseteado para Sprint 2
- STRATEGIC_DIRECTION.md con reflexiones del sprint
- TECH_DEBT.md curado: items resueltos archivados
- methodology/CASE_STUDIES/inq-roi-sprint-1-5.md generado

Sprint 1.5 cerrado:
- 11 etapas + 1 fix pre-Etapa 11
- 526 unit tests + 38 E2E al cierre
- Producto con identidad InQ aplicada en web + 3 PDFs
- Metodología documentada con casos reales (2 de mala
  clasificación de iniciativa, originaron AP.8 +
  refinamiento C.6)
- Material para transmisión al equipo de 5 técnicos
2026-05-19 07:27:40 -03:00

182 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Etapa 4 Sprint 1.5 — Validación de PDFs con header/footer compartido InQ.
*
* Genera los 3 PDFs con setup realista y valida:
* 1. Palabras clave presentes ("InQ ROI", "Powered by InQuality")
* 2. Términos prohibidos ausentes ("Process Cost Platform", "v0.1.0", "INQ", "Inq")
* 3. Cantidad de páginas (actual=3, automatizado=3, roi=4)
* 4. Tamaño por página (1580 KB por página)
*/
import { test, expect } from '@playwright/test'
import { resolve } from 'path'
import { mkdirSync, readFileSync } from 'fs'
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 OUT_DIR = resolve(__dirname, './__output__')
test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) })
// ─── Helpers ──────────────────────────────────────────────────────────────────
async function importBpmn(page: import('@playwright/test').Page, filename: string) {
await page.goto('/')
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename))
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
await page.waitForTimeout(800)
}
async function setActivityCosts(
page: import('@playwright/test').Page,
elementId: string, directCost: number, automatedCost: number
) {
await page.locator(`[data-element-id="${elementId}"]`).first().click()
await page.waitForTimeout(400)
const toggle = page.getByRole('switch', { name: /automatizable/i })
if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click()
await page.waitForTimeout(200)
await page.getByLabel(/Costo directo fijo/i).fill(String(directCost))
await page.getByLabel(/Costo automatizado/i).fill(String(automatedCost))
await page.getByLabel(/Tiempo automatizado/i).fill('10')
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
}
async function configureGlobal(page: import('@playwright/test').Page, freq: number, inv: number) {
await page.getByRole('tab', { name: /Global/i }).click()
await page.waitForTimeout(300)
await page.getByLabel(/Frecuencia anual/i).fill(String(freq))
await page.getByLabel(/Inversión en automatización/i).fill(String(inv))
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
}
async function setupAndSimulate(page: import('@playwright/test').Page) {
await importBpmn(page, 'medium-with-gateways.bpmn')
await setActivityCosts(page, 'task_recv', 200, 20)
await setActivityCosts(page, 'task_score', 200, 20)
await setActivityCosts(page, 'task_analisis', 200, 20)
await configureGlobal(page, 5000, 80000)
await page.getByRole('button', { name: 'Simular' }).click()
await page.waitForURL(/\/report\//, { timeout: 20_000 })
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 })
await page.waitForSelector('.bpmn-container .djs-group', { timeout: 30_000 })
await page.waitForTimeout(800)
}
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 }
}
async function downloadPdf(
page: import('@playwright/test').Page,
targetTab: 'actual' | 'automatizado' | 'roi',
outPath: string
) {
if (targetTab === 'actual') {
await page.getByRole('tab', { name: 'Actual' }).click()
await page.waitForTimeout(400)
} else if (targetTab === 'automatizado') {
await page.getByRole('tab', { name: 'Automatizado' }).click()
await page.waitForTimeout(400)
} else {
await page.getByRole('tab', { name: /Comparación/i }).click()
await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 })
await page.waitForTimeout(500)
}
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 30_000 }),
page.getByRole('button', { name: 'Exportar PDF' }).click(),
])
await download.saveAs(outPath)
return download.suggestedFilename()
}
function validatePdfText(text: string, filename: string) {
// Palabras clave obligatorias
expect(text, `${filename}: debe contener "InQ ROI"`).toMatch(/InQ ROI/)
expect(text, `${filename}: debe contener "Powered by InQuality"`).toMatch(/Powered by InQuality/)
// Términos prohibidos
expect(text, `${filename}: no debe contener "Process Cost Platform"`).not.toContain('Process Cost Platform')
expect(text, `${filename}: no debe contener "v0.1.0"`).not.toContain('v0.1.0')
expect(text, `${filename}: no debe contener "INQ" (mal ortografía)`).not.toContain('INQ')
expect(text, `${filename}: no debe contener "Inq" (mal ortografía)`).not.toContain('Inq')
}
// ─── Tests ────────────────────────────────────────────────────────────────────
test('etapa-4-actual.pdf — 4 páginas, marca InQ, sin términos prohibidos', async ({ page }) => {
await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-4-actual.pdf')
const filename = await downloadPdf(page, 'actual', outPath)
console.log('Nombre archivo:', filename)
expect(filename).toMatch(/_actual\.pdf$/)
const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024
console.log(`Tamaño: ${sizeKB.toFixed(1)} KB`)
expect(sizeKB, 'Tamaño total > 15 KB por página').toBeGreaterThan(15 * 4)
expect(sizeKB, 'Tamaño total < 80 KB por página (4 págs)').toBeLessThan(80 * 4)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
console.log(`Primeros 300 chars: ${text.slice(0, 300).replace(/\n/g, ' ')}`)
expect(numpages).toBe(4)
validatePdfText(text, 'etapa-4-actual.pdf')
})
test('etapa-4-automatizado.pdf — 4 páginas, marca InQ, sin términos prohibidos', async ({ page }) => {
await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-4-automatizado.pdf')
const filename = await downloadPdf(page, 'automatizado', outPath)
expect(filename).toMatch(/_automatizado\.pdf$/)
const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024
console.log(`etapa-4-automatizado.pdf: ${sizeKB.toFixed(1)} KB`)
expect(sizeKB).toBeGreaterThan(15 * 4)
expect(sizeKB).toBeLessThan(80 * 4)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
expect(numpages).toBe(4)
validatePdfText(text, 'etapa-4-automatizado.pdf')
})
test('etapa-4-roi.pdf — 5 páginas, marca InQ, keywords ROI, sin términos prohibidos', async ({ page }) => {
await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-4-roi.pdf')
const filename = await downloadPdf(page, 'roi', outPath)
expect(filename).toMatch(/_roi\.pdf$/)
const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024
console.log(`etapa-4-roi.pdf: ${sizeKB.toFixed(1)} KB`)
// ROI PDF incluye imagen de gradiente en página 1 → tamaño mayor (~250 KB)
expect(sizeKB, 'ROI PDF debe tener contenido mínimo (5 páginas)').toBeGreaterThan(20)
expect(sizeKB, 'ROI PDF no debe ser irrazonablemente grande').toBeLessThan(400)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
console.log(`Primeros 400 chars: ${text.slice(0, 400).replace(/\n/g, ' ')}`)
expect(numpages).toBe(5)
validatePdfText(text, 'etapa-4-roi.pdf')
// Keywords específicos del PDF de ROI
expect(text).toMatch(/Payback|payback/)
expect(text).toMatch(/ahorro|Ahorro/)
expect(text).toContain('USD')
})