Files
inq-roi-simulador-web/tests/e2e/etapa-5-pdfs.spec.ts
Marcos Benítez 2f4633a33c feat(sprint-1.5): rediseño visual PDF ROI + identidad InQ (Etapas 4-7)
- Header/footer compartidos con paleta InQ naranja en los 3 PDFs (Etapa 4)
- Layout híbrido portrait+landscape en PDFs Actual/Automatizado:
  página 2 landscape para diagrama BPMN heatmap (Etapa 5)
- Página 1 PDF ROI rediseñada: header denso, banner narrativo, KPI Hero
  con gradiente naranja→magenta, grid 2×2 de KPIs secundarios, Top 3
  actividades por ahorro (Etapa 6)
- Páginas 2-5 PDF ROI con identidad InQ: tabla con paleta naranja,
  análisis del ahorro, nota metodológica, trayectoria del ahorro
  acumulado con gráfico SVG nativo (Etapa 7)
- 500 tests verdes (+ 34 tests nuevos del Sprint 1.5)
- BACKLOG.md, TECH_DEBT.md, OBSERVATIONS.md: sistema de planificación
  reemplaza TODO.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:45:01 -03:00

181 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 5 Sprint 1.5 — Validación de PDFs con layout híbrido portrait+landscape.
*
* Validaciones mandatorias:
* 1. Páginas: actual=3, automatizado=3, roi=4
* 2. Orientación: actual/auto = portrait/LANDSCAPE/portrait; roi = portrait×4
* 3. "InQ ROI" y "Powered by InQuality" en todas las páginas
* 4. Diagrama BPMN en página 2 del actual (verificado por tamaño PDF y texto)
* 5. Tests previos ≥ 469
* 6. Sin regresiones de marca
*/
import { test, expect } from '@playwright/test'
import { resolve } from 'path'
import { mkdirSync, readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { PDFParse } from 'pdf-parse'
import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs'
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 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 getPageOrientations(buffer: Buffer): Promise<Array<'portrait' | 'landscape'>> {
const uint8 = new Uint8Array(buffer)
const pdf = await getDocument({ data: uint8 }).promise
const orientations: Array<'portrait' | 'landscape'> = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 1 })
orientations.push(viewport.width > viewport.height ? 'landscape' : 'portrait')
}
return orientations
}
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 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 validateBrandStrings(text: string, filename: string) {
expect(text, `${filename}: debe contener "InQ ROI"`).toMatch(/InQ ROI/)
expect(text, `${filename}: debe contener "Powered by InQuality"`).toMatch(/Powered by InQuality/)
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')
}
// ─── Tests ─────────────────────────────────────────────────────────────────────
test('etapa-5-actual.pdf — 4 págs, portrait-landscape-portrait-portrait, marca InQ', async ({ page }) => {
await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-5-actual.pdf')
const filename = await downloadPdf(page, 'actual', outPath)
console.log('Nombre:', filename)
const buffer = readFileSync(outPath)
const sizeKB = buffer.length / 1024
console.log(`Tamaño: ${sizeKB.toFixed(1)} KB`)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas: ${numpages}`)
console.log(`Texto primeros 200: ${text.slice(0, 200).replace(/\n/g, ' ')}`)
expect(numpages).toBe(4)
validateBrandStrings(text, 'etapa-5-actual.pdf')
// Validar orientaciones con pdfjs
const orientations = await getPageOrientations(buffer)
console.log('Orientaciones:', orientations)
expect(orientations).toEqual(['portrait', 'landscape', 'portrait', 'portrait'])
// La página 2 (landscape) debe ser más ancha que alta en dimensiones reales
expect(orientations[1]).toBe('landscape')
})
test('etapa-5-automatizado.pdf — 4 págs, portrait-landscape-portrait-portrait, marca InQ', async ({ page }) => {
await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-5-automatizado.pdf')
await downloadPdf(page, 'automatizado', outPath)
const buffer = readFileSync(outPath)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas automatizado: ${numpages}`)
expect(numpages).toBe(4)
validateBrandStrings(text, 'etapa-5-automatizado.pdf')
const orientations = await getPageOrientations(buffer)
console.log('Orientaciones automatizado:', orientations)
expect(orientations).toEqual(['portrait', 'landscape', 'portrait', 'portrait'])
})
test('etapa-5-roi.pdf — 5 págs todas portrait, sin regresiones', async ({ page }) => {
await setupAndSimulate(page)
const outPath = resolve(OUT_DIR, 'etapa-5-roi.pdf')
await downloadPdf(page, 'roi', outPath)
const buffer = readFileSync(outPath)
const { numpages, text } = await parsePdf(buffer)
console.log(`Páginas ROI: ${numpages}`)
expect(numpages).toBe(5)
validateBrandStrings(text, 'etapa-5-roi.pdf')
const orientations = await getPageOrientations(buffer)
console.log('Orientaciones ROI:', orientations)
expect(orientations).toEqual(['portrait', 'portrait', 'portrait', 'portrait', 'portrait'])
})