Files
inq-roi-simulador-web/tests/lib/export/pdf-orientation.test.ts
Marcos Benítez 2f4633a33c feat(sprint-1.5): rediseño visual PDF ROI + identidad InQ (Etapas 4-7)
- Header/footer compartidos con paleta InQ naranja en los 3 PDFs (Etapa 4)
- Layout híbrido portrait+landscape en PDFs Actual/Automatizado:
  página 2 landscape para diagrama BPMN heatmap (Etapa 5)
- Página 1 PDF ROI rediseñada: header denso, banner narrativo, KPI Hero
  con gradiente naranja→magenta, grid 2×2 de KPIs secundarios, Top 3
  actividades por ahorro (Etapa 6)
- Páginas 2-5 PDF ROI con identidad InQ: tabla con paleta naranja,
  análisis del ahorro, nota metodológica, trayectoria del ahorro
  acumulado con gráfico SVG nativo (Etapa 7)
- 500 tests verdes (+ 34 tests nuevos del Sprint 1.5)
- BACKLOG.md, TECH_DEBT.md, OBSERVATIONS.md: sistema de planificación
  reemplaza TODO.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:45:01 -03:00

211 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests de orientación de páginas PDF — Etapa 5 Sprint 1.5.
*
* Verifica que:
* - PDF Actual/Automatizado: portrait → landscape → portrait
* - PDF ROI: todas portrait (no tiene página BPMN)
* - addPage se llama con el argumento correcto en cada caso
*
* Nota: estos tests usan mocks de jsPDF y verifican las llamadas a addPage.
* La orientación real del PDF se valida en los tests E2E (etapa-5-pdfs.spec.ts).
*/
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((t: string) => [t]),
lastAutoTable: { finalY: 150 },
internal: {
getNumberOfPages: vi.fn().mockReturnValue(3),
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: 'Test Process',
clientName: 'Test Client',
bpmnXml: '', currency: 'USD',
overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 50000,
createdAt: 0, updatedAt: 0,
}
const mockSimulation: Simulation = {
id: 'sim1', processId: 'p1',
executedAt: new Date('2026-05-17T10:00:00Z').getTime(),
result: {
totalCost: 1200, totalDirectCost: 1000, totalIndirectCost: 200,
totalTimeMinutes: 90,
perActivity: [{
activityId: 'a1', bpmnElementId: 'task1', activityName: 'Tarea 1',
expectedDirectCost: 1200, expectedIndirectCost: 200, expectedTotalCost: 1200,
percentOfTotal: 100, executionProbability: 1.0, expectedExecutions: 1.0,
executionTimeMinutes: 30, resourceCostBreakdown: [],
}],
perResource: [], warnings: [],
},
resultAutomated: {
totalCost: 300, totalDirectCost: 250, totalIndirectCost: 50,
totalTimeMinutes: 20,
perActivity: [{
activityId: 'a1', bpmnElementId: 'task1', activityName: 'Tarea 1',
expectedDirectCost: 300, expectedIndirectCost: 50, expectedTotalCost: 300,
percentOfTotal: 100, executionProbability: 1.0, expectedExecutions: 1.0,
executionTimeMinutes: 10, resourceCostBreakdown: [],
}],
perResource: [], warnings: [],
},
}
// ─── Tests ─────────────────────────────────────────────────────────────────────
describe('PDF orientation — Etapa 5', () => {
beforeEach(() => {
vi.clearAllMocks()
// Simular que getWidth/getHeight devuelven 210×297 por defecto (portrait)
// En landscape, jsPDF realmente cambia los valores internamente
mockDoc.internal.pageSize.getWidth.mockReturnValue(210)
mockDoc.internal.pageSize.getHeight.mockReturnValue(297)
})
describe('PDF Actual/Automatizado — mezcla portrait-landscape-portrait-portrait', () => {
it('llama addPage exactamente 3 veces para 4 páginas', 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)
})
it('primera addPage usa landscape (página BPMN)', async () => {
const { exportToPdf } = await import('@/lib/export/pdf-export')
await exportToPdf({
process: mockProcess,
simulation: mockSimulation,
resources: [],
heatmapImageData: null,
tab: 'actual',
})
const firstCall = mockDoc.addPage.mock.calls[0]
expect(firstCall[0]).toBe('a4')
expect(firstCall[1]).toBe('landscape')
})
it('segunda addPage usa portrait (análisis + tabla)', async () => {
const { exportToPdf } = await import('@/lib/export/pdf-export')
await exportToPdf({
process: mockProcess,
simulation: mockSimulation,
resources: [],
heatmapImageData: null,
tab: 'actual',
})
const secondCall = mockDoc.addPage.mock.calls[1]
expect(secondCall[0]).toBe('a4')
expect(secondCall[1]).toBe('portrait')
})
it('tercera addPage usa portrait (nota metodológica)', async () => {
const { exportToPdf } = await import('@/lib/export/pdf-export')
await exportToPdf({
process: mockProcess,
simulation: mockSimulation,
resources: [],
heatmapImageData: null,
tab: 'actual',
})
const thirdCall = mockDoc.addPage.mock.calls[2]
expect(thirdCall[0]).toBe('a4')
expect(thirdCall[1]).toBe('portrait')
})
it('tab automatizado también usa landscape en página 2', async () => {
const { exportToPdf } = await import('@/lib/export/pdf-export')
await exportToPdf({
process: mockProcess,
simulation: mockSimulation,
resources: [],
heatmapImageData: null,
tab: 'automatizado',
})
const firstCall = mockDoc.addPage.mock.calls[0]
expect(firstCall[1]).toBe('landscape')
})
})
describe('PDF ROI — todas portrait', () => {
it('llama addPage 4 veces para 5 páginas (todas sin landscape)', 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)
// Ninguna llamada debe usar landscape
const hasLandscape = mockDoc.addPage.mock.calls.some(
(c: unknown[]) => c[1] === 'landscape'
)
expect(hasLandscape).toBe(false)
})
})
describe('drawHeatmapImage — dimensiones dinámicas', () => {
it('usa getWidth() del doc, no constante hardcodeada', async () => {
const { drawHeatmapImage } = await import('@/lib/export/pdf-sections')
// Simular página landscape (297mm wide)
mockDoc.internal.pageSize.getWidth.mockReturnValue(297)
mockDoc.internal.pageSize.getHeight.mockReturnValue(210)
const fakeDataUrl = 'data:image/jpeg;base64,/9j/test'
drawHeatmapImage(mockDoc as any, fakeDataUrl, 40)
expect(mockDoc.internal.pageSize.getWidth).toHaveBeenCalled()
// La imagen debe ser más ancha que en portrait (170mm)
const imgCallArgs = mockDoc.addImage.mock.calls[0]
// args: [dataUrl, format, x, y, width, height]
const imgWidth = imgCallArgs[4]
expect(imgWidth).toBeGreaterThan(170) // más ancho que portrait (297 - 40 = 257mm)
})
})
})