/** * Tests unitarios de drawResourcesSection (Sprint 2, Etapa 6). * * drawResourcesSection toma doc: any y autoTable: Function directamente, * por lo que no necesita mock de jspdf — se pasa un objeto plano. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import type { ActivitySimResult, Resource, Activity } from '@/domain/types' // ─── Mock doc (jsPDF API mínima usada por drawResourcesSection) ─────────────── const mockDoc = { setFont: vi.fn(), setFontSize: vi.fn(), setTextColor: vi.fn(), setDrawColor: vi.fn(), setLineWidth: vi.fn(), text: vi.fn(), line: vi.fn(), } // ─── Fixtures ───────────────────────────────────────────────────────────────── const mockResource: Resource = { id: 'r1', processId: 'p1', name: 'Analista Senior', type: 'role', costPerHour: 60, } const mockActivity: Activity = { id: 'a1', processId: 'p1', bpmnElementId: 'task1', name: 'Recibir solicitud', type: 'task', directCostFixed: 600, executionTimeMinutes: 60, assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }], automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0, } const activityWithResources: ActivitySimResult = { activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud', expectedDirectCost: 660, expectedIndirectCost: 0, expectedTotalCost: 660, percentOfTotal: 100, executionProbability: 1.0, expectedExecutions: 1.0, executionTimeMinutes: 60, resourceCostBreakdown: [{ resourceId: 'r1', resourceName: 'Analista Senior', cost: 60 }], } const activityWithoutResources: ActivitySimResult = { activityId: 'a2', bpmnElementId: 'task2', activityName: 'Archivar', expectedDirectCost: 100, expectedIndirectCost: 0, expectedTotalCost: 100, percentOfTotal: 0, executionProbability: 1.0, expectedExecutions: 1.0, executionTimeMinutes: 10, resourceCostBreakdown: [], } // ─── Tests ──────────────────────────────────────────────────────────────────── describe('drawResourcesSection', () => { let autoTableMock: ReturnType beforeEach(() => { vi.clearAllMocks() autoTableMock = vi.fn() }) it('no lanza error con actividades que tienen recursos', async () => { const { drawResourcesSection } = await import('@/lib/export/pdf-sections') expect(() => drawResourcesSection( mockDoc, autoTableMock, { perActivity: [activityWithResources] }, [mockResource], [mockActivity], 'USD', 40 ) ).not.toThrow() expect(autoTableMock).toHaveBeenCalledTimes(1) }) it('no llama a autoTable si todas las actividades tienen resourceCostBreakdown vacío', async () => { const { drawResourcesSection } = await import('@/lib/export/pdf-sections') drawResourcesSection( mockDoc, autoTableMock, { perActivity: [activityWithoutResources] }, [mockResource], [mockActivity], 'USD', 40 ) expect(autoTableMock).not.toHaveBeenCalled() }) it('el header de autoTable incluye la moneda correcta', async () => { const { drawResourcesSection } = await import('@/lib/export/pdf-sections') drawResourcesSection( mockDoc, autoTableMock, { perActivity: [activityWithResources] }, [mockResource], [mockActivity], 'PYG', 40 ) const callArgs = autoTableMock.mock.calls[0][1] as { head: unknown[][] } const headFlat = JSON.stringify(callArgs.head) expect(headFlat).toContain('PYG') }) it('llama a autoTable exactamente una vez aunque haya varias actividades con recursos', async () => { const { drawResourcesSection } = await import('@/lib/export/pdf-sections') const secondActivity: ActivitySimResult = { ...activityWithResources, activityId: 'a3', activityName: 'Evaluar solicitud', resourceCostBreakdown: [{ resourceId: 'r1', resourceName: 'Analista Senior', cost: 30 }], } drawResourcesSection( mockDoc, autoTableMock, { perActivity: [activityWithResources, secondActivity] }, [mockResource], [mockActivity], 'USD', 40 ) expect(autoTableMock).toHaveBeenCalledTimes(1) }) })