/** * Tests permanentes de persistencia en IndexedDB vía Dexie. * Usan fake-indexeddb (parcheado en tests/setup.ts) para correr en Node. * * Cada test limpia las tablas en beforeEach para garantizar aislamiento. * Verifican que los repositorios escriben y recuperan datos correctamente. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { db } from '@/persistence/db' import { processRepo, activityRepo, gatewayRepo, resourceRepo, simulationRepo, } from '@/persistence/repositories' import type { Process, Activity, GatewayConfig, Resource, Simulation, SimulationResult } from '@/domain/types' // ─── Fixtures ───────────────────────────────────────────────────────────────── function makeProcess(id = 'proc-1'): Process { return { id, name: 'Proceso de prueba', clientName: 'Cliente Test', bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0, createdAt: 1000, updatedAt: 2000, } } function makeActivity(id: string, processId: string, bpmnElementId: string): Activity { return { id, processId, bpmnElementId, name: `Tarea ${bpmnElementId}`, type: 'task', directCostFixed: 100, executionTimeMinutes: 30, assignedResources: [{ resourceId: 'res-1', utilizationMinutes: 15, units: 1 }], automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0, } } function makeGateway(id: string, processId: string, bpmnElementId: string): GatewayConfig { return { id, processId, bpmnElementId, gatewayType: 'exclusive', branches: [ { flowId: 'f1', targetElementId: 'taskA', probability: 0.7 }, { flowId: 'f2', targetElementId: 'taskB', probability: 0.3 }, ], } } function makeResource(id: string, processId: string): Resource { return { id, processId, name: 'Analista', type: 'role', costPerHour: 60, } } function makeSimulation(id: string, processId: string): Simulation { const result: SimulationResult = { totalCost: 1200, totalDirectCost: 1000, totalIndirectCost: 200, totalTimeMinutes: 90, perActivity: [], perResource: [], warnings: [], } return { id, processId, executedAt: 3000, result } } // ─── Setup / Teardown ───────────────────────────────────────────────────────── beforeEach(async () => { await db.open() await db.transaction( 'rw', [db.processes, db.activities, db.gateways, db.resources, db.simulations], async () => { await Promise.all([ db.processes.clear(), db.activities.clear(), db.gateways.clear(), db.resources.clear(), db.simulations.clear(), ]) } ) }) afterEach(async () => { await db.close() }) // ─── Process ───────────────────────────────────────────────────────────────── describe('IndexedDB / processRepo', () => { it('guarda un proceso y lo recupera por id', async () => { const proc = makeProcess() await processRepo.save(proc) const retrieved = await processRepo.getById('proc-1') expect(retrieved).toBeDefined() expect(retrieved!.name).toBe('Proceso de prueba') expect(retrieved!.currency).toBe('USD') expect(retrieved!.overheadPercentage).toBe(0.2) }) it('getById devuelve undefined para id inexistente', async () => { const result = await processRepo.getById('no-existe') expect(result).toBeUndefined() }) it('getAll devuelve todos los procesos guardados', async () => { await processRepo.save(makeProcess('p1')) await processRepo.save(makeProcess('p2')) await processRepo.save(makeProcess('p3')) const all = await processRepo.getAll() expect(all).toHaveLength(3) }) it('save sobrescribe (put) un proceso existente', async () => { const proc = makeProcess() await processRepo.save(proc) await processRepo.save({ ...proc, name: 'Nombre actualizado', updatedAt: 9999 }) const updated = await processRepo.getById('proc-1') expect(updated!.name).toBe('Nombre actualizado') expect(updated!.updatedAt).toBe(9999) }) it('delete elimina el proceso y sus entidades relacionadas en cascada', async () => { const proc = makeProcess('proc-del') await processRepo.save(proc) await activityRepo.saveMany([makeActivity('a1', 'proc-del', 'task1')]) await gatewayRepo.saveMany([makeGateway('g1', 'proc-del', 'gw1')]) await resourceRepo.save(makeResource('r1', 'proc-del')) await processRepo.delete('proc-del') expect(await processRepo.getById('proc-del')).toBeUndefined() expect(await activityRepo.getByProcess('proc-del')).toHaveLength(0) expect(await gatewayRepo.getByProcess('proc-del')).toHaveLength(0) expect(await resourceRepo.getByProcess('proc-del')).toHaveLength(0) }) }) // ─── Activity ───────────────────────────────────────────────────────────────── describe('IndexedDB / activityRepo', () => { const processId = 'proc-act' it('saveMany y getByProcess devuelven las mismas actividades', async () => { const acts = [ makeActivity('a1', processId, 'task1'), makeActivity('a2', processId, 'task2'), makeActivity('a3', processId, 'task3'), ] await activityRepo.saveMany(acts) const retrieved = await activityRepo.getByProcess(processId) expect(retrieved).toHaveLength(3) const ids = retrieved.map((a) => a.id).sort() expect(ids).toEqual(['a1', 'a2', 'a3']) }) it('persiste assignedResources (array anidado) correctamente', async () => { const act = makeActivity('a1', processId, 'task1') await activityRepo.save(act) const [retrieved] = await activityRepo.getByProcess(processId) expect(retrieved.assignedResources).toHaveLength(1) expect(retrieved.assignedResources[0].resourceId).toBe('res-1') expect(retrieved.assignedResources[0].utilizationMinutes).toBe(15) expect(retrieved.assignedResources[0].units).toBe(1) }) it('no devuelve actividades de otro proceso', async () => { await activityRepo.save(makeActivity('a-other', 'otro-proc', 'task1')) const result = await activityRepo.getByProcess(processId) expect(result).toHaveLength(0) }) }) // ─── GatewayConfig ──────────────────────────────────────────────────────────── describe('IndexedDB / gatewayRepo', () => { const processId = 'proc-gw' it('saveMany y getByProcess devuelven la config con branches correctas', async () => { const gw = makeGateway('g1', processId, 'gw_decision') await gatewayRepo.saveMany([gw]) const [retrieved] = await gatewayRepo.getByProcess(processId) expect(retrieved.bpmnElementId).toBe('gw_decision') expect(retrieved.gatewayType).toBe('exclusive') expect(retrieved.branches).toHaveLength(2) expect(retrieved.branches[0].probability).toBe(0.7) expect(retrieved.branches[1].probability).toBe(0.3) }) it('save individual actualiza la config existente', async () => { const gw = makeGateway('g1', processId, 'gw1') await gatewayRepo.save(gw) const updated: GatewayConfig = { ...gw, branches: [ { flowId: 'f1', targetElementId: 'taskA', probability: 0.6 }, { flowId: 'f2', targetElementId: 'taskB', probability: 0.4 }, ], } await gatewayRepo.save(updated) const [retrieved] = await gatewayRepo.getByProcess(processId) expect(retrieved.branches[0].probability).toBe(0.6) }) }) // ─── Resource ───────────────────────────────────────────────────────────────── describe('IndexedDB / resourceRepo', () => { const processId = 'proc-res' it('save y getByProcess recupera el recurso correctamente', async () => { await resourceRepo.save(makeResource('r1', processId)) const [retrieved] = await resourceRepo.getByProcess(processId) expect(retrieved.name).toBe('Analista') expect(retrieved.costPerHour).toBe(60) expect(retrieved.type).toBe('role') }) it('delete elimina un recurso por id', async () => { await resourceRepo.save(makeResource('r1', processId)) await resourceRepo.save(makeResource('r2', processId)) await resourceRepo.delete('r1') const remaining = await resourceRepo.getByProcess(processId) expect(remaining).toHaveLength(1) expect(remaining[0].id).toBe('r2') }) it('múltiples recursos para el mismo proceso', async () => { const types = ['role', 'system', 'equipment'] as const for (const [i, type] of types.entries()) { await resourceRepo.save({ ...makeResource(`r${i}`, processId), type }) } const all = await resourceRepo.getByProcess(processId) expect(all).toHaveLength(3) const typeSet = new Set(all.map((r) => r.type)) expect(typeSet.has('role')).toBe(true) expect(typeSet.has('system')).toBe(true) expect(typeSet.has('equipment')).toBe(true) }) }) // ─── Simulation ─────────────────────────────────────────────────────────────── describe('IndexedDB / simulationRepo', () => { const processId = 'proc-sim' it('save y getLatestByProcess recuperan el snapshot de resultado', async () => { const sim = makeSimulation('sim-1', processId) await simulationRepo.save(sim) const retrieved = await simulationRepo.getLatestByProcess(processId) expect(retrieved).toBeDefined() expect(retrieved!.result.totalCost).toBe(1200) expect(retrieved!.result.totalDirectCost).toBe(1000) expect(retrieved!.result.totalIndirectCost).toBe(200) }) it('getLatestByProcess devuelve undefined si no hay simulaciones', async () => { const result = await simulationRepo.getLatestByProcess('proc-sin-sim') expect(result).toBeUndefined() }) it('múltiples simulaciones: getByProcess las recupera todas', async () => { await simulationRepo.save({ ...makeSimulation('s1', processId), executedAt: 1000 }) await simulationRepo.save({ ...makeSimulation('s2', processId), executedAt: 2000 }) await simulationRepo.save({ ...makeSimulation('s3', processId), executedAt: 3000 }) const all = await simulationRepo.getByProcess(processId) expect(all).toHaveLength(3) }) it('persiste warnings en el resultado (campo libre)', async () => { const sim: Simulation = { ...makeSimulation('s-warn', processId), result: { ...makeSimulation('s-warn', processId).result, warnings: ['Loop detectado en taskX'], }, } await simulationRepo.save(sim) const retrieved = await simulationRepo.getLatestByProcess(processId) expect(retrieved!.result.warnings).toHaveLength(1) expect(retrieved!.result.warnings[0]).toContain('Loop detectado') }) }) // ─── Round-trip completo (simula el flujo de ImportPage) ───────────────────── describe('IndexedDB / round-trip proceso completo', () => { it('guarda proceso + actividades + gateways + recursos, luego los recupera íntegros', async () => { const proc = makeProcess('rt-proc') const acts = [ makeActivity('rt-a1', 'rt-proc', 'taskA'), makeActivity('rt-a2', 'rt-proc', 'taskB'), ] const gws = [makeGateway('rt-g1', 'rt-proc', 'gw1')] const resources = [makeResource('rt-r1', 'rt-proc')] await Promise.all([ processRepo.save(proc), activityRepo.saveMany(acts), gatewayRepo.saveMany(gws), resourceRepo.save(resources[0]), ]) // Simular recarga de página const [retrievedProc, retrievedActs, retrievedGws, retrievedRes] = await Promise.all([ processRepo.getById('rt-proc'), activityRepo.getByProcess('rt-proc'), gatewayRepo.getByProcess('rt-proc'), resourceRepo.getByProcess('rt-proc'), ]) expect(retrievedProc!.id).toBe('rt-proc') expect(retrievedActs).toHaveLength(2) expect(retrievedGws).toHaveLength(1) expect(retrievedGws[0].branches).toHaveLength(2) expect(retrievedRes).toHaveLength(1) expect(retrievedRes[0].costPerHour).toBe(60) }) })