/** * 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', 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, automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 5, units: 3 }], } 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) }) }) // ─── Escenario automatizado usa automatedAssignedResources ──────────────────── describe('drawResourcesSection — escenario automatizado', () => { let autoTableMock: ReturnType beforeEach(() => { autoTableMock = vi.fn() }) it('escenario automatizado: pasa utilizationMinutes y units de automatedAssignedResources', async () => { const { drawResourcesSection } = await import('@/lib/export/pdf-sections') drawResourcesSection( mockDoc, autoTableMock, { perActivity: [activityWithResources] }, [mockResource], [mockActivity], 'USD', 40, 'automatizado' ) const body = (autoTableMock.mock.calls[0][1] as any).body as any[][] const row = body[0] // Min. (índice 4) y Uds. (índice 5) deben venir de automatedAssignedResources: { utilMin: 5, units: 3 } expect(row[4].content).toBe('5') expect(row[5].content).toBe('3') }) it('escenario actual: pasa utilizationMinutes y units de assignedResources (sin regresión)', async () => { const { drawResourcesSection } = await import('@/lib/export/pdf-sections') drawResourcesSection( mockDoc, autoTableMock, { perActivity: [activityWithResources] }, [mockResource], [mockActivity], 'USD', 40, 'actual' ) const body = (autoTableMock.mock.calls[0][1] as any).body as any[][] const row = body[0] // assignedResources: { utilMin: 60, units: 1 } expect(row[4].content).toBe('60') expect(row[5].content).toBe('1') }) })