/** * Smoke tests del export PDF. * jsPDF + autotable se mockean porque requieren canvas/DOM completo. * Se verifica: llamadas al builder, metadata, nombre de archivo, no-throw. */ import { describe, it, expect, vi, beforeEach } from 'vitest' // ─── Mock de jsPDF ──────────────────────────────────────────────────────────── const mockDoc = { setProperties: vi.fn(), setFont: vi.fn(), setFontSize: vi.fn(), setTextColor: vi.fn(), setFillColor: vi.fn(), setDrawColor: vi.fn(), setLineWidth: vi.fn(), text: vi.fn(), rect: vi.fn(), roundedRect: vi.fn(), line: vi.fn(), addImage: vi.fn(), addPage: vi.fn(), setPage: vi.fn(), save: vi.fn(), splitTextToSize: vi.fn().mockImplementation((text: string) => [text]), internal: { getNumberOfPages: vi.fn().mockReturnValue(3) }, } // Arrow functions no pueden ser constructoras — usar function() regular vi.mock('jspdf', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any function MockJsPDF(this: any) { return mockDoc } return { jsPDF: MockJsPDF } }) vi.mock('jspdf-autotable', () => ({ default: vi.fn(), })) // ─── Fixtures ───────────────────────────────────────────────────────────────── import type { Process, Simulation } from '@/domain/types' const mockProcess: Process = { id: 'p1', name: 'Proceso de Aprobación de Crédito', clientName: 'Banco XYZ', bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, createdAt: 0, updatedAt: 0, } const mockSimulation: Simulation = { id: 'sim1', processId: 'p1', executedAt: new Date('2026-05-13T10:00:00Z').getTime(), result: { totalCost: 1200, totalDirectCost: 1000, totalIndirectCost: 200, totalTimeMinutes: 90, perActivity: [ { activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud', expectedDirectCost: 600, expectedIndirectCost: 120, expectedTotalCost: 720, percentOfTotal: 60, executionProbability: 1.0, expectedExecutions: 1.0, executionTimeMinutes: 30, resourceCostBreakdown: [], }, { activityId: 'a2', bpmnElementId: 'task2', activityName: 'Procesar', expectedDirectCost: 400, expectedIndirectCost: 80, expectedTotalCost: 480, percentOfTotal: 40, executionProbability: 0.8, expectedExecutions: 0.8, executionTimeMinutes: 60, resourceCostBreakdown: [], }, ], perResource: [], warnings: [], }, } // ─── Tests ──────────────────────────────────────────────────────────────────── describe('exportToPdf — smoke tests', () => { beforeEach(() => { vi.clearAllMocks() }) it('no lanza error con parámetros válidos y heatmap null', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await expect(exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null, })).resolves.not.toThrow() }) it('llama a doc.addPage al menos una vez (estructura multi-página)', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) // El PDF tiene al menos 2 páginas (análisis + tabla) expect(mockDoc.addPage).toHaveBeenCalled() }) it('llama a setProperties con metadata correcta', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) expect(mockDoc.setProperties).toHaveBeenCalledWith(expect.objectContaining({ title: expect.stringContaining('Proceso de Aprobación de Crédito'), author: 'Banco XYZ', subject: 'Simulación de proceso BPMN', })) }) it('llama a doc.save con nombre de archivo correcto', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) const saveCall = mockDoc.save.mock.calls[0][0] as string expect(saveCall).toMatch(/proceso-de-aprobacion-de-credito/) expect(saveCall).toMatch(/banco-xyz/) expect(saveCall).toMatch(/\.pdf$/) }) it('NO llama a addImage cuando heatmapImageData es null', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) expect(mockDoc.addImage).not.toHaveBeenCalled() }) it('llama a addImage cuando heatmapImageData está disponible', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') const fakeDataUrl = 'data:image/jpeg;base64,/9j/fake' await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: fakeDataUrl }) expect(mockDoc.addImage).toHaveBeenCalledWith(fakeDataUrl, 'JPEG', expect.any(Number), expect.any(Number), expect.any(Number), expect.any(Number)) }) it('llama a addPageNumbers (setPage para el footer)', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) // addPageNumbers itera setPage para cada página expect(mockDoc.setPage).toHaveBeenCalled() }) it('llama a autoTable con head que contiene la moneda', async () => { const autoTable = (await import('jspdf-autotable')).default as ReturnType const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) expect(autoTable).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ head: expect.arrayContaining([ expect.arrayContaining([ expect.objectContaining({ content: expect.stringContaining('USD') }), ]), ]), }) ) }) it('con warnings en el resultado: no lanza error', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') const simWithWarnings = { ...mockSimulation, result: { ...mockSimulation.result, warnings: ['Loop detectado en task1'] }, } await expect(exportToPdf({ process: mockProcess, simulation: simWithWarnings, resources: [], heatmapImageData: null, })).resolves.not.toThrow() }) })