chore(sprint-5/etapa-6): limpieza técnica — E2E Supabase, fix flakiness guardar, docs cierre sprint
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
This commit is contained in:
@@ -7,19 +7,44 @@
|
||||
* Ejecutar: npx playwright test
|
||||
*/
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { readFileSync, mkdirSync } from 'fs'
|
||||
import { readFileSync, mkdirSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { PDFParse } from 'pdf-parse'
|
||||
import sharp from 'sharp'
|
||||
import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
test.use({ storageState: 'tests/e2e/setup/auth-state.json' })
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const ROOT = resolve(__dirname, '../..')
|
||||
const BPMN_DIR = resolve(__dirname, '../../public/sample-processes')
|
||||
const OUTPUT_DIR = resolve(__dirname, '__output__')
|
||||
|
||||
// ─── Cliente Supabase con service_role_key para inyección directa de datos ────
|
||||
// Reemplaza la inyección vía IndexedDB (Dexie, eliminado en Sprint 4) por escritura
|
||||
// directa en Supabase, bypaseando RLS con el service_role_key. Ver .env.example.
|
||||
|
||||
function loadEnvFile(filename: string): Record<string, string> {
|
||||
const path = resolve(ROOT, filename)
|
||||
if (!existsSync(path)) return {}
|
||||
const out: Record<string, string> = {}
|
||||
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
const idx = trimmed.indexOf('=')
|
||||
if (idx === -1) continue
|
||||
out[trimmed.slice(0, idx)] = trimmed.slice(idx + 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const localEnv = loadEnvFile('.env.local')
|
||||
const testEnv = loadEnvFile('.env.test')
|
||||
const SUPABASE_URL = localEnv.VITE_SUPABASE_URL
|
||||
const SERVICE_ROLE_KEY = testEnv.SUPABASE_SERVICE_ROLE_KEY
|
||||
|
||||
test.beforeAll(() => {
|
||||
mkdirSync(OUTPUT_DIR, { recursive: true })
|
||||
})
|
||||
@@ -312,74 +337,69 @@ async function analyzeJpegColors(jpegBuf: Buffer): Promise<{
|
||||
|
||||
// ─── Test: gradiente real con costos dispares ─────────────────────────────────
|
||||
|
||||
test('Heatmap con gradiente real — medium-with-gateways.bpmn', async ({ page }) => {
|
||||
// Costos dispares por bpmnElementId para forzar varianza significativa.
|
||||
// Probabilidades de ejecución del proceso: task_recv/valid/analisis/archivo = 1.0,
|
||||
// los demás ≈ 0.5. Se asignan costos para que:
|
||||
// task_recv → expectedCost $10.000 → costo más alto → ROJO
|
||||
// task_analisis → expectedCost $5.000 → mitad del max → ÁMBAR (t ≈ 0.5)
|
||||
// resto → expectedCost < $500 → costos bajos → VERDE
|
||||
const COST_BY_ELEMENT: Record<string, number> = {
|
||||
task_recv: 10_000, // prob 1.0 → expected $10.000 → ROJO
|
||||
task_valid: 200, // prob 1.0 → expected $200
|
||||
task_score: 200, // prob 0.5 → expected $100
|
||||
task_pedir_docs: 200, // prob 0.5 → expected $100
|
||||
task_bureau: 200, // prob 0.5 → expected $100
|
||||
task_empleador: 200, // prob 0.5 → expected $100
|
||||
task_analisis: 5_000, // prob 1.0 → expected $5.000 → ÁMBAR
|
||||
task_aprobar: 200, // prob 0.5 → expected $100
|
||||
task_rechazar: 200, // prob 0.5 → expected $100
|
||||
task_desembolso: 200, // prob 0.5 → expected $100
|
||||
task_archivo: 200, // prob 1.0 → expected $200 → VERDE (mínimo)
|
||||
}
|
||||
test.describe('Heatmap con gradiente real', () => {
|
||||
let createdProcessId: string | null = null
|
||||
|
||||
// ── 1. Importar BPMN y navegar al workspace ─────────────────────────────────
|
||||
await page.goto('/import')
|
||||
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn'))
|
||||
await page.waitForURL(/\/workspace\//)
|
||||
test.afterAll(async () => {
|
||||
if (createdProcessId && SUPABASE_URL && SERVICE_ROLE_KEY) {
|
||||
const supabaseAdmin = createClient(SUPABASE_URL, SERVICE_ROLE_KEY)
|
||||
await supabaseAdmin.from('processes').delete().eq('id', createdProcessId)
|
||||
}
|
||||
})
|
||||
|
||||
const processId = page.url().split('/workspace/')[1]
|
||||
test('Heatmap con gradiente real — medium-with-gateways.bpmn', async ({ page }) => {
|
||||
test.skip(
|
||||
!SUPABASE_URL || !SERVICE_ROLE_KEY,
|
||||
'SUPABASE_SERVICE_ROLE_KEY no configurada en .env.test — ver .env.example para el formato esperado'
|
||||
)
|
||||
|
||||
// ── 2. Inyectar costos dispares directamente en IndexedDB (Dexie) ───────────
|
||||
await page.evaluate(async ({ procId, costs }) => {
|
||||
const DB_NAME = 'ProcessCostPlatform'
|
||||
// Costos dispares por bpmnElementId para forzar varianza significativa.
|
||||
// Probabilidades de ejecución del proceso: task_recv/valid/analisis/archivo = 1.0,
|
||||
// los demás ≈ 0.5. Se asignan costos para que:
|
||||
// task_recv → expectedCost $10.000 → costo más alto → ROJO
|
||||
// task_analisis → expectedCost $5.000 → mitad del max → ÁMBAR (t ≈ 0.5)
|
||||
// resto → expectedCost < $500 → costos bajos → VERDE
|
||||
const COST_BY_ELEMENT: Record<string, number> = {
|
||||
task_recv: 10_000, // prob 1.0 → expected $10.000 → ROJO
|
||||
task_valid: 200, // prob 1.0 → expected $200
|
||||
task_score: 200, // prob 0.5 → expected $100
|
||||
task_pedir_docs: 200, // prob 0.5 → expected $100
|
||||
task_bureau: 200, // prob 0.5 → expected $100
|
||||
task_empleador: 200, // prob 0.5 → expected $100
|
||||
task_analisis: 5_000, // prob 1.0 → expected $5.000 → ÁMBAR
|
||||
task_aprobar: 200, // prob 0.5 → expected $100
|
||||
task_rechazar: 200, // prob 0.5 → expected $100
|
||||
task_desembolso: 200, // prob 0.5 → expected $100
|
||||
task_archivo: 200, // prob 1.0 → expected $200 → VERDE (mínimo)
|
||||
}
|
||||
|
||||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME)
|
||||
req.onsuccess = () => resolve(req.result)
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
// ── 1. Importar BPMN y navegar al workspace ─────────────────────────────────
|
||||
await page.goto('/import')
|
||||
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn'))
|
||||
await page.waitForURL(/\/workspace\//)
|
||||
|
||||
const activities = await new Promise<any[]>((resolve, reject) => {
|
||||
const tx = db.transaction('activities', 'readonly')
|
||||
const index = tx.objectStore('activities').index('processId')
|
||||
const req = index.getAll(procId)
|
||||
req.onsuccess = () => resolve(req.result)
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
const processId = page.url().split('/workspace/')[1]
|
||||
createdProcessId = processId
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const tx = db.transaction('activities', 'readwrite')
|
||||
const store = tx.objectStore('activities')
|
||||
let pending = activities.length
|
||||
if (pending === 0) { resolve(); return }
|
||||
// ── 2. Inyectar costos dispares directamente en Supabase (service_role_key) ──
|
||||
const supabaseAdmin = createClient(SUPABASE_URL!, SERVICE_ROLE_KEY!)
|
||||
const { data: activities, error: actErr } = await supabaseAdmin
|
||||
.from('activities')
|
||||
.select('id, bpmn_element_id')
|
||||
.eq('process_id', processId)
|
||||
if (actErr) throw actErr
|
||||
|
||||
for (const act of activities) {
|
||||
const req = store.put({
|
||||
...act,
|
||||
directCostFixed: (costs as Record<string, number>)[act.bpmnElementId] ?? 200,
|
||||
executionTimeMinutes: 60,
|
||||
})
|
||||
req.onsuccess = () => { if (--pending === 0) resolve() }
|
||||
req.onerror = () => reject(req.error)
|
||||
}
|
||||
})
|
||||
const { error: upsertErr } = await supabaseAdmin.from('activities').upsert(
|
||||
(activities ?? []).map((act) => ({
|
||||
id: act.id,
|
||||
direct_cost_fixed: COST_BY_ELEMENT[act.bpmn_element_id as string] ?? 200,
|
||||
execution_time_minutes: 60,
|
||||
}))
|
||||
)
|
||||
if (upsertErr) throw upsertErr
|
||||
|
||||
db.close()
|
||||
}, { procId: processId, costs: COST_BY_ELEMENT })
|
||||
|
||||
// ── 3. Recargar workspace para que Zustand relea desde Dexie ────────────────
|
||||
await page.reload()
|
||||
// ── 3. Recargar workspace para que el store de Zustand relea desde Supabase ──
|
||||
await page.reload()
|
||||
await page.waitForSelector('button:has-text("Simular")', { timeout: 15_000 })
|
||||
await page.waitForTimeout(1_500) // esperar carga de store
|
||||
|
||||
@@ -452,4 +472,5 @@ test('Heatmap con gradiente real — medium-with-gateways.bpmn', async ({ page }
|
||||
// orangePx no se exige con mínimo estricto: con solo 11 actividades el stop naranja
|
||||
// (t=0.67) puede no tener ninguna actividad exactamente ahí — alcanza con que exista
|
||||
// la transición suave entre amarillo y rojo (lo prueban los tests unitarios de colors.ts).
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user