/** * Tests Etapa 8 — PDFs Actual y Automatizado con identidad InQ. * * Verifica: * - drawScenarioPage1: título diferenciador por tab, sin gradiente * - drawScenarioKpiGrid: paleta naranja InQ (#FEF3E2), label #92400E * - drawAnalysisSection: título "ANÁLISIS DE COMPOSICIÓN" uppercase naranja * - drawMethodologySection: título "NOTA METODOLÓGICA" uppercase naranja + caja * - exportToPdf: no llama addPage con landscape en página 1 (página 2 BPMN intacta) * - PDF ROI: sin cambios (sigue siendo 4 addPage calls) */ import { describe, it, expect, vi, beforeEach } from 'vitest' 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((t: string) => [t]), lastAutoTable: { finalY: 150 }, internal: { getNumberOfPages: vi.fn().mockReturnValue(4), pageSize: { getWidth: vi.fn().mockReturnValue(210), getHeight: vi.fn().mockReturnValue(297), }, }, } 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() })) import type { Process, Simulation } from '@/domain/types' const mockProcess: Process = { id: 'p1', name: 'Proceso Test', clientName: 'Cliente Test', bpmnXml: '', currency: 'USD', overheadPercentage: 0.2, annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000, createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, } const mockSimulation: Simulation = { id: 's1', processId: 'p1', executedAt: new Date('2026-05-17T10:00:00Z').getTime(), result: { totalCost: 600, totalDirectCost: 500, totalIndirectCost: 100, totalTimeMinutes: 90, perActivity: [ { activityId: 'a1', bpmnElementId: 't1', activityName: 'Tarea A', expectedDirectCost: 400, expectedIndirectCost: 80, expectedTotalCost: 400, percentOfTotal: 66, executionProbability: 1, expectedExecutions: 1, executionTimeMinutes: 30, resourceCostBreakdown: [] }, ], perResource: [], warnings: [], }, resultAutomated: { totalCost: 60, totalDirectCost: 50, totalIndirectCost: 10, totalTimeMinutes: 10, perActivity: [ { activityId: 'a1', bpmnElementId: 't1', activityName: 'Tarea A', expectedDirectCost: 40, expectedIndirectCost: 10, expectedTotalCost: 40, percentOfTotal: 100, executionProbability: 1, expectedExecutions: 1, executionTimeMinutes: 5, resourceCostBreakdown: [] }, ], perResource: [], warnings: [], }, } describe('PDF Actual/Automatizado — Etapa 8', () => { beforeEach(() => { vi.clearAllMocks() mockDoc.internal.pageSize.getWidth.mockReturnValue(210) mockDoc.internal.pageSize.getHeight.mockReturnValue(297) mockDoc.internal.getNumberOfPages.mockReturnValue(4) mockDoc.splitTextToSize.mockImplementation((t: string) => [t]) }) // ─── drawScenarioPage1 ──────────────────────────────────────────────────── describe('drawScenarioPage1 — tab actual', () => { it('dibuja "ANÁLISIS DE COSTOS — ESCENARIO ACTUAL" en el header', async () => { const { drawScenarioPage1 } = await import('@/lib/export/pdf-sections') drawScenarioPage1(mockDoc as any, 'actual', mockProcess, mockSimulation.executedAt, mockSimulation.result, 'USD') const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) expect(textCalls).toContain('ANÁLISIS DE COSTOS — ESCENARIO ACTUAL') }) it('dibuja "InQ ROI" en el header', async () => { const { drawScenarioPage1 } = await import('@/lib/export/pdf-sections') drawScenarioPage1(mockDoc as any, 'actual', mockProcess, mockSimulation.executedAt, mockSimulation.result, 'USD') const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) expect(textCalls).toContain('InQ ROI') }) it('NO usa setFontSize(28) ni gradiente — no es KPI Hero', async () => { const { drawScenarioPage1 } = await import('@/lib/export/pdf-sections') drawScenarioPage1(mockDoc as any, 'actual', mockProcess, mockSimulation.executedAt, mockSimulation.result, 'USD') const fontSizes = mockDoc.setFontSize.mock.calls.map((c: number[]) => c[0]) expect(fontSizes).not.toContain(28) // No debe llamar addImage (no hay gradiente canvas) expect(mockDoc.addImage).not.toHaveBeenCalled() }) }) describe('drawScenarioPage1 — tab automatizado', () => { it('dibuja "ANÁLISIS DE COSTOS — ESCENARIO AUTOMATIZADO"', async () => { const { drawScenarioPage1 } = await import('@/lib/export/pdf-sections') drawScenarioPage1(mockDoc as any, 'automatizado', mockProcess, mockSimulation.executedAt, mockSimulation.result, 'USD') const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) expect(textCalls).toContain('ANÁLISIS DE COSTOS — ESCENARIO AUTOMATIZADO') }) it('dibuja metadata strip con label "PROCESO"', async () => { const { drawScenarioPage1 } = await import('@/lib/export/pdf-sections') drawScenarioPage1(mockDoc as any, 'automatizado', mockProcess, mockSimulation.executedAt, mockSimulation.result, 'USD') const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) expect(textCalls).toContain('PROCESO') expect(textCalls).toContain('MONEDA') }) }) // ─── drawScenarioKpiGrid ────────────────────────────────────────────────── describe('drawScenarioKpiGrid — paleta naranja InQ', () => { it('usa roundedRect con fondo #FEF3E2 [254,243,226] para cada card', async () => { const { drawScenarioKpiGrid } = await import('@/lib/export/pdf-sections') drawScenarioKpiGrid(mockDoc as any, mockSimulation.result, 'USD', 20, 170, 50) const fillCalls = mockDoc.setFillColor.mock.calls const hasOrangeLight = fillCalls.some((c: number[]) => c[0] === 254 && c[1] === 243 && c[2] === 226) expect(hasOrangeLight).toBe(true) }) it('usa setTextColor con #92400E [146,64,14] para labels', async () => { const { drawScenarioKpiGrid } = await import('@/lib/export/pdf-sections') drawScenarioKpiGrid(mockDoc as any, mockSimulation.result, 'USD', 20, 170, 50) const textColorCalls = mockDoc.setTextColor.mock.calls const hasOrangeDarker = textColorCalls.some((c: number[]) => c[0] === 146 && c[1] === 64 && c[2] === 14) expect(hasOrangeDarker).toBe(true) }) it('dibuja "COSTO TOTAL ESPERADO" como label del primer card', async () => { const { drawScenarioKpiGrid } = await import('@/lib/export/pdf-sections') drawScenarioKpiGrid(mockDoc as any, mockSimulation.result, 'USD', 20, 170, 50) const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) expect(textCalls).toContain('COSTO TOTAL ESPERADO') }) }) // ─── drawAnalysisSection ───────────────────────────────────────────────── describe('drawAnalysisSection — título uppercase naranja', () => { it('dibuja "ANÁLISIS DE COMPOSICIÓN" (uppercase)', async () => { const { drawAnalysisSection } = await import('@/lib/export/pdf-sections') drawAnalysisSection(mockDoc as any, mockSimulation.result, 'USD', 50) const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) expect(textCalls).toContain('ANÁLISIS DE COMPOSICIÓN') }) it('usa setTextColor #92400E [146,64,14] para el título', async () => { const { drawAnalysisSection } = await import('@/lib/export/pdf-sections') drawAnalysisSection(mockDoc as any, mockSimulation.result, 'USD', 50) const textColorCalls = mockDoc.setTextColor.mock.calls const hasOrangeDarker = textColorCalls.some((c: number[]) => c[0] === 146 && c[1] === 64 && c[2] === 14) expect(hasOrangeDarker).toBe(true) }) }) // ─── drawMethodologySection ─────────────────────────────────────────────── describe('drawMethodologySection — título uppercase naranja + caja', () => { it('dibuja "NOTA METODOLÓGICA" (uppercase)', async () => { const { drawMethodologySection } = await import('@/lib/export/pdf-sections') drawMethodologySection(mockDoc as any, mockProcess, 50) const textCalls = mockDoc.text.mock.calls.map((c: unknown[]) => c[0]) const hasTitle = textCalls.some(t => String(t).includes('NOTA METODOLÓGICA') || String(t).includes('NOTA METODOLOGICA')) expect(hasTitle).toBe(true) }) it('dibuja caja contenedora (rect con fondo slate-50 [248,250,252])', async () => { const { drawMethodologySection } = await import('@/lib/export/pdf-sections') drawMethodologySection(mockDoc as any, mockProcess, 50) const fillCalls = mockDoc.setFillColor.mock.calls const hasSlate50 = fillCalls.some((c: number[]) => c[0] === 248 && c[1] === 250 && c[2] === 252) expect(hasSlate50).toBe(true) }) }) // ─── exportToPdf integración ────────────────────────────────────────────── describe('exportToPdf — Actual/Automatizado, no-regresión ROI', () => { it('PDF actual: 3 addPage calls (páginas 2,3,4); primer addPage es landscape', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null, tab: 'actual', }) expect(mockDoc.addPage).toHaveBeenCalledTimes(3) expect(mockDoc.addPage.mock.calls[0]).toEqual(['a4', 'landscape']) }) it('PDF automatizado: primer addPage es landscape', async () => { const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null, tab: 'automatizado', }) expect(mockDoc.addPage.mock.calls[0]).toEqual(['a4', 'landscape']) }) it('PDF ROI: sigue siendo 4 addPage calls (no-regresión)', async () => { mockDoc.internal.getNumberOfPages.mockReturnValue(5) const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null, tab: 'roi', activities: [], }) expect(mockDoc.addPage).toHaveBeenCalledTimes(4) }) }) })