Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Tier 1 — Automatizado Build: ✅ limpio Lint: ✅ limpio en archivos nuevos de Sprint 4 (separé resource-types.ts de resource-display.tsx para sacar 2 warnings de Fast Refresh; quedan 0 errores/warnings nuevos). Quedó 1 error preexistente en ReportPage.tsx (no tocado desde Sprint 3, fuera de scope) y ~102 warnings no-explicit-any preexistentes en tests viejos. Tests unitarios: ✅ 521/521 verdes Tests E2E: ⚠️ 43-44/48 verdes (varía levemente entre corridas) — detalle abajo Auditoría inicial vs. estado final Antes: 39/39 specs (100%) fallaban — ROJO-AUTH puro, todos por #bpmn-file-input nunca apareciendo (esperaban el flujo pre-Sprint-4 directamente en /). Decisión tomada (con tu input): Opción A — sesión real. Creaste el usuario martin.bernal@inquality.com.py; yo verifiqué que no tenía fila en public.users y se la inserté con platform_role='platform_admin'. Implementé tests/e2e/setup/auth-setup.ts como globalSetup de Playwright: hace signInWithPassword, inyecta la sesión real en localStorage con el formato exacto que usa supabase-js v2, y genera tests/e2e/setup/auth-state.json (gitignored) en cada corrida. Fix adicional necesario: todos los specs navegaban a / esperando el input de archivo ahí — ahora vive en /import. Corregí los 18 page.goto('/') → /import, incluyendo reescribir el flujo obsoleto de prod-smoke.spec.ts (esperaba una landing pre-auth que ya no existe). Resultado: 33→43-44 specs pasan. Quedan 5-6 ROJO-OTRO, con dos causas raíz identificadas (no relacionadas con auth, fuera del mandato de esta etapa que pide no reescribir lógica de tests): Botón "Guardar" en ActivityPanel queda deshabilitado (3 specs de PDF) — encontré algo extraño: el texto del botón dice "Guardar cambios" (que solo se renderiza si isDirty=true) pero Playwright lo sigue viendo disabled. Esto contradice la lógica simple de disabled={!isDirty} en el componente — sospecho un bug real de re-render en producción, pero diagnosticarlo a fondo requiere debugging en vivo. Recomiendo abrir esto como bug dedicado en un sprint futuro. 2 specs usan IndexedDB/Dexie directamente (export-pdf.spec.ts, validate-dod-etapa4.spec.ts) — Dexie se eliminó completamente en Etapa 4. Estos tests necesitan reescritura para inyectar datos vía Supabase en vez de IndexedDB — eso sí es "reescribir lógica de tests", explícitamente fuera de este alcance. Los resultados varían levemente entre corridas porque Playwright corre specs en paralelo bajo la misma cuenta de test compartida — puede haber interferencia de datos entre specs concurrentes. Sugiero como mejora futura (no implementada ahora): --workers=1 para diagnóstico determinístico. Entregables nuevos tests/e2e/setup/auth-setup.ts — global setup con sesión real tests/e2e/sprint-4-smoke.spec.ts — 9 tests (4 sin auth: login, redirects de /, /workspace, /recursos; 5 con auth: biblioteca sin 500, sin 406 en simulations, catálogo carga, avatar visible, import navega a workspace) docs/CHECKLIST_ENTREGA.md — las 4 tiers completas .env.test (gitignored) + .env.example documentado .gitignore actualizado (.env.test, auth-state.json) Polish Textos de OwnershipBadge y "Compartir con equipo" ya estaban en español — sin cambios. Tooltip "Disponible próximamente" para "Compartir con equipo": no lo agregué, tal como pedía el propio brief, consultarte antes. ¿Lo querés?
193 lines
8.1 KiB
TypeScript
193 lines
8.1 KiB
TypeScript
/**
|
|
* Etapa 8 Sprint 1.5 — PDFs Actual y Automatizado con identidad InQ.
|
|
*
|
|
* Validaciones mandatorias:
|
|
* 1. Cantidad de páginas: 4 en cada PDF
|
|
* 2. Orientaciones: ['portrait','landscape','portrait','portrait']
|
|
* 3. Página 1: "ANÁLISIS DE COSTOS — ESCENARIO ACTUAL/AUTOMATIZADO" presente
|
|
* 4. Página 1: "UN PRODUCTO DE" + "InQuality" + metadata strip "PROCESO"
|
|
* 5. Página 1: SIN texto de KPI Hero gradiente ("AHORRO PROYECTADO" ausente)
|
|
* 6. Página 3: "ANÁLISIS DE COMPOSICIÓN" presente (uppercase)
|
|
* 7. Página 4: "NOTA METODOLÓGICA" presente (uppercase)
|
|
* 8. PDF ROI: sin cambios (5 páginas, página 1 con KPI Hero intacta)
|
|
* 9. Marcas globales: "InQ ROI", "Powered by InQuality"
|
|
*/
|
|
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'
|
|
|
|
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 OUT_DIR = resolve(__dirname, './__output__')
|
|
|
|
test.beforeAll(() => { mkdirSync(OUT_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 }
|
|
}
|
|
|
|
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 setupAndSimulate(page: import('@playwright/test').Page) {
|
|
await page.goto('/import')
|
|
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn'))
|
|
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
|
|
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
|
|
await page.waitForTimeout(800)
|
|
|
|
for (const [id, cost, auto] of [['task_recv', 200, 20], ['task_score', 200, 20], ['task_analisis', 200, 20]] as [string, number, number][]) {
|
|
await page.locator(`[data-element-id="${id}"]`).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(cost))
|
|
await page.getByLabel(/Costo automatizado/i).fill(String(auto))
|
|
await page.getByLabel(/Tiempo automatizado/i).fill('10')
|
|
await page.getByRole('button', { name: /Guardar/i }).click()
|
|
await page.waitForTimeout(400)
|
|
}
|
|
|
|
await page.getByRole('tab', { name: /Global/i }).click()
|
|
await page.waitForTimeout(300)
|
|
await page.getByLabel(/Frecuencia anual/i).fill('5000')
|
|
await page.getByLabel(/Inversión en automatización/i).fill('80000')
|
|
await page.getByRole('button', { name: /Guardar/i }).click()
|
|
await page.waitForTimeout(400)
|
|
|
|
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, tab: 'actual' | 'automatizado' | 'roi', outPath: string) {
|
|
if (tab === 'actual') {
|
|
await page.getByRole('tab', { name: 'Actual' }).click()
|
|
await page.waitForTimeout(400)
|
|
} else if (tab === '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)
|
|
}
|
|
|
|
// ─── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
test('etapa-8-actual.pdf — 4 págs, identidad InQ, SIN gradiente ROI', async ({ page }) => {
|
|
await setupAndSimulate(page)
|
|
const outPath = resolve(OUT_DIR, 'etapa-8-actual.pdf')
|
|
await downloadPdf(page, 'actual', outPath)
|
|
|
|
const buffer = readFileSync(outPath)
|
|
const sizeKB = buffer.length / 1024
|
|
console.log(`Tamaño actual: ${sizeKB.toFixed(1)} KB`)
|
|
|
|
const { numpages, text } = await parsePdf(buffer)
|
|
console.log(`Páginas: ${numpages}`)
|
|
console.log(`Texto p.1 (300 chars): ${text.slice(0, 300).replace(/\n/g, ' ')}`)
|
|
|
|
// Val 1: 4 páginas
|
|
expect(numpages).toBe(4)
|
|
|
|
// Val 2: orientaciones
|
|
const orientations = await getPageOrientations(buffer)
|
|
console.log('Orientaciones:', orientations)
|
|
expect(orientations).toEqual(['portrait', 'landscape', 'portrait', 'portrait'])
|
|
|
|
// Val 3: título diferenciador escenario
|
|
expect(text, 'p.1 debe tener título del escenario').toMatch(/ANÁLISIS DE COSTOS/)
|
|
expect(text, 'p.1 debe identificar escenario actual').toMatch(/ESCENARIO ACTUAL/)
|
|
|
|
// Val 4: elementos de identidad InQ
|
|
expect(text).toMatch(/UN PRODUCTO DE/)
|
|
expect(text).toMatch(/InQuality/)
|
|
expect(text).toMatch(/PROCESO/)
|
|
|
|
// Val 5: SIN KPI Hero gradiente
|
|
expect(text, 'NO debe tener KPI Hero del ROI').not.toMatch(/AHORRO PROYECTADO/)
|
|
|
|
// Val 6: página 3 análisis
|
|
expect(text).toMatch(/ANÁLISIS DE COMPOSICIÓN/)
|
|
|
|
// Val 7: página 4 metodología
|
|
expect(text).toMatch(/NOTA METODOLÓGICA|NOTA METODOLOGICA/)
|
|
|
|
// Val 9: marcas globales
|
|
expect(text).toMatch(/InQ ROI/)
|
|
expect(text).toMatch(/Powered by InQuality/)
|
|
expect(text).not.toContain('Process Cost Platform')
|
|
expect(text).not.toContain('v0.1.0')
|
|
})
|
|
|
|
test('etapa-8-automatizado.pdf — 4 págs, identidad InQ, ESCENARIO AUTOMATIZADO', async ({ page }) => {
|
|
await setupAndSimulate(page)
|
|
const outPath = resolve(OUT_DIR, 'etapa-8-automatizado.pdf')
|
|
await downloadPdf(page, 'automatizado', outPath)
|
|
|
|
const buffer = readFileSync(outPath)
|
|
const { numpages, text } = await parsePdf(buffer)
|
|
console.log(`Páginas automatizado: ${numpages}`)
|
|
console.log(`Texto p.1 (200 chars): ${text.slice(0, 200).replace(/\n/g, ' ')}`)
|
|
|
|
expect(numpages).toBe(4)
|
|
const orientations = await getPageOrientations(buffer)
|
|
expect(orientations).toEqual(['portrait', 'landscape', 'portrait', 'portrait'])
|
|
|
|
expect(text).toMatch(/ANÁLISIS DE COSTOS/)
|
|
expect(text).toMatch(/ESCENARIO AUTOMATIZADO/)
|
|
expect(text).not.toMatch(/AHORRO PROYECTADO/)
|
|
expect(text).toMatch(/ANÁLISIS DE COMPOSICIÓN/)
|
|
expect(text).toMatch(/NOTA METODOLÓGICA|NOTA METODOLOGICA/)
|
|
expect(text).toMatch(/InQ ROI/)
|
|
expect(text).toMatch(/Powered by InQuality/)
|
|
})
|
|
|
|
test('etapa-8-roi.pdf — PDF ROI sin cambios (no-regresión)', async ({ page }) => {
|
|
await setupAndSimulate(page)
|
|
const outPath = resolve(OUT_DIR, 'etapa-8-roi.pdf')
|
|
await downloadPdf(page, 'roi', outPath)
|
|
|
|
const buffer = readFileSync(outPath)
|
|
const { numpages, text } = await parsePdf(buffer)
|
|
console.log(`Páginas ROI: ${numpages}`)
|
|
|
|
// Val 8: ROI sin cambios
|
|
expect(numpages).toBe(5)
|
|
expect(text).toMatch(/ANÁLISIS DE RETORNO DE INVERSIÓN/)
|
|
expect(text).toMatch(/AHORRO PROYECTADO A/)
|
|
expect(text).toMatch(/TOP 3 ACTIVIDADES POR AHORRO/)
|
|
expect(text).toMatch(/InQ ROI/)
|
|
expect(text).toMatch(/Powered by InQuality/)
|
|
|
|
const orientations = await getPageOrientations(buffer)
|
|
expect(orientations).toEqual(['portrait', 'portrait', 'portrait', 'portrait', 'portrait'])
|
|
})
|