/** * Tests permanentes del parser BPMN. * Protegen contra regresiones cuando se modifique parseBpmnXml, * extractActivityElements o extractGatewayElements. * * Conteos esperados (fijos, derivados del XML de los sample BPMNs): * * simple-linear.bpmn * nodes total : 7 (1 startEvent + 5 tasks + 1 endEvent) * flows total : 6 * tasks : 5 * gateways : 0 * events : 2 * divergentes : 0 * * medium-with-gateways.bpmn * nodes total : 17 (1 start + 11 tasks + 4 gateways + 1 end) * flows total : 19 * tasks : 11 * gateways : 4 (gw_datos XOR, gw_parallel_split AND, gw_parallel_join AND, gw_decision XOR) * events : 2 * divergentes : 3 (gw_datos:2, gw_parallel_split:2, gw_decision:2) * * complex-with-loop.bpmn * nodes total : 13 (1 start + 9 tasks + 2 gateways + 1 end) * flows total : 14 * tasks : 9 * gateways : 2 (gw_resultado XOR 3-salidas, gw_merge XOR convergente) * events : 2 * divergentes : 1 (gw_resultado:3) */ import { describe, it, expect } from 'vitest' import { readFileSync } from 'fs' import { resolve } from 'path' import { parseBpmnXml, extractActivityElements, extractGatewayElements, BpmnParseError, } from '@/domain/bpmn-parser' import { isTaskNode, isGatewayNode } from '@/domain/types' function loadSample(name: string): string { return readFileSync(resolve(__dirname, '../../public/sample-processes', name), 'utf-8') } function countByCategory(graph: ReturnType) { let tasks = 0, gateways = 0, events = 0, unknown = 0 for (const node of graph.nodes.values()) { if (isTaskNode(node.type)) tasks++ else if (isGatewayNode(node.type)) gateways++ else if (['startEvent', 'endEvent', 'intermediateThrowEvent', 'intermediateCatchEvent'].includes(node.type)) events++ else unknown++ } return { tasks, gateways, events, unknown } } // ─── simple-linear.bpmn ────────────────────────────────────────────────────── describe('parser / simple-linear.bpmn', () => { const xml = loadSample('simple-linear.bpmn') it('parsea sin lanzar error', () => { expect(() => parseBpmnXml(xml)).not.toThrow() }) it('grafo: 7 nodos y 6 flujos de secuencia', () => { const g = parseBpmnXml(xml) expect(g.nodes.size).toBe(7) expect(g.flows.size).toBe(6) }) it('grafo: 5 tasks | 0 gateways | 2 eventos', () => { const g = parseBpmnXml(xml) const { tasks, gateways, events, unknown } = countByCategory(g) expect(tasks).toBe(5) expect(gateways).toBe(0) expect(events).toBe(2) expect(unknown).toBe(0) }) it('1 startEvent detectado como punto de entrada', () => { const g = parseBpmnXml(xml) expect(g.startEventIds).toHaveLength(1) expect(g.startEventIds[0]).toBe('start') }) it('extractActivityElements: 5 actividades con nombres correctos', () => { const g = parseBpmnXml(xml) const acts = extractActivityElements(g) expect(acts).toHaveLength(5) const names = acts.map((a) => a.name) expect(names).toContain('Recibir solicitud') expect(names).toContain('Verificar documentación') expect(names).toContain('Procesar pedido') expect(names).toContain('Notificar al cliente') expect(names).toContain('Archivar expediente') acts.forEach((a) => expect(a.type).toBe('task')) }) it('extractGatewayElements: 0 gateways divergentes (proceso lineal)', () => { const g = parseBpmnXml(xml) const gws = extractGatewayElements(g) expect(gws).toHaveLength(0) }) }) // ─── medium-with-gateways.bpmn ─────────────────────────────────────────────── describe('parser / medium-with-gateways.bpmn', () => { const xml = loadSample('medium-with-gateways.bpmn') it('parsea sin lanzar error', () => { expect(() => parseBpmnXml(xml)).not.toThrow() }) it('grafo: 17 nodos y 19 flujos de secuencia', () => { const g = parseBpmnXml(xml) expect(g.nodes.size).toBe(17) expect(g.flows.size).toBe(19) }) it('grafo: 11 tasks | 4 gateways | 2 eventos', () => { const g = parseBpmnXml(xml) const { tasks, gateways, events, unknown } = countByCategory(g) expect(tasks).toBe(11) expect(gateways).toBe(4) expect(events).toBe(2) expect(unknown).toBe(0) }) it('extractActivityElements: exactamente 11 actividades', () => { const g = parseBpmnXml(xml) const acts = extractActivityElements(g) expect(acts).toHaveLength(11) const ids = acts.map((a) => a.bpmnElementId) expect(ids).toContain('task_recv') expect(ids).toContain('task_valid') expect(ids).toContain('task_pedir_docs') expect(ids).toContain('task_score') expect(ids).toContain('task_bureau') expect(ids).toContain('task_empleador') expect(ids).toContain('task_analisis') expect(ids).toContain('task_aprobar') expect(ids).toContain('task_rechazar') expect(ids).toContain('task_desembolso') expect(ids).toContain('task_archivo') }) it('extractGatewayElements: 3 gateways divergentes con salidas correctas', () => { const g = parseBpmnXml(xml) const gws = extractGatewayElements(g) expect(gws).toHaveLength(3) const byId = Object.fromEntries(gws.map((gw) => [gw.bpmnElementId, gw])) // gw_datos: XOR con 2 salidas expect(byId['gw_datos']).toBeDefined() expect(byId['gw_datos'].outgoing).toHaveLength(2) expect(byId['gw_datos'].gatewayType).toBe('exclusiveGateway') // gw_parallel_split: AND con 2 salidas expect(byId['gw_parallel_split']).toBeDefined() expect(byId['gw_parallel_split'].outgoing).toHaveLength(2) expect(byId['gw_parallel_split'].gatewayType).toBe('parallelGateway') // gw_decision: XOR con 2 salidas expect(byId['gw_decision']).toBeDefined() expect(byId['gw_decision'].outgoing).toHaveLength(2) expect(byId['gw_decision'].gatewayType).toBe('exclusiveGateway') // gw_parallel_join NO debe estar (convergente: 1 salida) expect(byId['gw_parallel_join']).toBeUndefined() }) }) // ─── complex-with-loop.bpmn ─────────────────────────────────────────────────── describe('parser / complex-with-loop.bpmn', () => { const xml = loadSample('complex-with-loop.bpmn') it('parsea sin lanzar error', () => { expect(() => parseBpmnXml(xml)).not.toThrow() }) it('grafo: 13 nodos y 14 flujos de secuencia', () => { const g = parseBpmnXml(xml) expect(g.nodes.size).toBe(13) expect(g.flows.size).toBe(14) }) it('grafo: 9 tasks | 2 gateways | 2 eventos', () => { const g = parseBpmnXml(xml) const { tasks, gateways, events, unknown } = countByCategory(g) expect(tasks).toBe(9) expect(gateways).toBe(2) expect(events).toBe(2) expect(unknown).toBe(0) }) it('extractActivityElements: exactamente 9 actividades', () => { const g = parseBpmnXml(xml) const acts = extractActivityElements(g) expect(acts).toHaveLength(9) const ids = acts.map((a) => a.bpmnElementId) expect(ids).toContain('task_preparar') expect(ids).toContain('task_inspeccion') expect(ids).toContain('task_documentar') expect(ids).toContain('task_correcciones') expect(ids).toContain('task_rechazo') expect(ids).toContain('task_informe_rechazo') expect(ids).toContain('task_entrega') expect(ids).toContain('task_certificado') expect(ids).toContain('task_registro') }) it('extractGatewayElements: 1 gateway divergente (gw_resultado con 3 salidas)', () => { const g = parseBpmnXml(xml) const gws = extractGatewayElements(g) expect(gws).toHaveLength(1) expect(gws[0].bpmnElementId).toBe('gw_resultado') expect(gws[0].outgoing).toHaveLength(3) expect(gws[0].gatewayType).toBe('exclusiveGateway') // gw_merge es convergente (1 salida) — no debe aparecer }) it('el flujo de loop existe en el grafo (correc_inspeccion apunta a task_inspeccion)', () => { const g = parseBpmnXml(xml) const loopFlow = g.flows.get('flow_correc_inspeccion') expect(loopFlow).toBeDefined() expect(loopFlow!.sourceRef).toBe('task_correcciones') expect(loopFlow!.targetRef).toBe('task_inspeccion') }) }) // ─── Casos de error del parser ──────────────────────────────────────────────── describe('parser / manejo de errores', () => { it('lanza BpmnParseError si el XML no tiene elemento ', () => { expect(() => parseBpmnXml('')).toThrow(BpmnParseError) }) it('lanza BpmnParseError si el XML está malformado', () => { expect(() => parseBpmnXml(' { const xml = ` ` expect(() => parseBpmnXml(xml)).toThrow(BpmnParseError) }) })