Files
inq-roi-simulador-web/tests/lib/export/csv-export-roi.test.ts
Marcos Benítez 4f51c74717
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
auth con avatar
2026-06-18 22:31:22 -03:00

219 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests del export CSV para el tab "Comparación + ROI" (Sprint 1 — Etapa 4).
* Verifica las 16 columnas, la metadata extendida, sufijos de archivo y
* compatibilidad con el comportamiento anterior (tabs actual y automatizado).
*/
import { describe, it, expect } from 'vitest'
import { generateCsvContent } from '@/lib/export/csv-export'
import type { Process, Simulation, Activity, SimulationResult } from '@/domain/types'
// ─── Fixtures ─────────────────────────────────────────────────────────────────
function makeProcess(extras: Partial<Process> = {}): Process {
return {
id: 'p1', name: 'Proceso de Crédito', clientName: 'Banco XYZ',
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', ...extras,
}
}
const perActivityActual: SimulationResult['perActivity'] = [
{
activityId: 'a1', bpmnElementId: 'task_recv', activityName: 'Recibir solicitud',
expectedDirectCost: 200, expectedIndirectCost: 40, expectedTotalCost: 240,
percentOfTotal: 60, executionProbability: 1.0, expectedExecutions: 1.0,
executionTimeMinutes: 60, resourceCostBreakdown: [],
},
{
activityId: 'a2', bpmnElementId: 'task_valid', activityName: 'Validar datos',
expectedDirectCost: 100, expectedIndirectCost: 20, expectedTotalCost: 120,
percentOfTotal: 40, executionProbability: 0.8, expectedExecutions: 0.8,
executionTimeMinutes: 30, resourceCostBreakdown: [],
},
]
const perActivityAutomated: SimulationResult['perActivity'] = [
{
activityId: 'a1', bpmnElementId: 'task_recv', activityName: 'Recibir solicitud',
expectedDirectCost: 20, expectedIndirectCost: 4, expectedTotalCost: 24,
percentOfTotal: 40, executionProbability: 1.0, expectedExecutions: 1.0,
executionTimeMinutes: 5, resourceCostBreakdown: [],
},
{
activityId: 'a2', bpmnElementId: 'task_valid', activityName: 'Validar datos',
expectedDirectCost: 100, expectedIndirectCost: 20, expectedTotalCost: 120,
percentOfTotal: 60, executionProbability: 0.8, expectedExecutions: 0.8,
executionTimeMinutes: 30, resourceCostBreakdown: [],
},
]
const mockSimulation: Simulation = {
id: 'sim1', processId: 'p1',
executedAt: new Date('2026-05-13T10:00:00Z').getTime(),
result: {
totalCost: 360, totalDirectCost: 300, totalIndirectCost: 60, totalTimeMinutes: 84,
perActivity: perActivityActual, perResource: [], warnings: [],
},
resultAutomated: {
totalCost: 144, totalDirectCost: 120, totalIndirectCost: 24, totalTimeMinutes: 8,
perActivity: perActivityAutomated, perResource: [], warnings: [],
},
}
const mockActivities: Activity[] = [
{
id: 'a1', processId: 'p1', bpmnElementId: 'task_recv', name: 'Recibir solicitud', type: 'task',
directCostFixed: 200, executionTimeMinutes: 60, assignedResources: [],
automatable: true, automatedCostFixed: 20, automatedTimeMinutes: 5,
automatedAssignedResources: [],
},
{
id: 'a2', processId: 'p1', bpmnElementId: 'task_valid', name: 'Validar datos', type: 'task',
directCostFixed: 100, executionTimeMinutes: 30, assignedResources: [],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [],
},
]
// ─── CSV ROI: estructura de columnas ──────────────────────────────────────────
describe('CSV tab=roi — 16 columnas', () => {
it('tiene exactamente 16 columnas en el header', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
// Papa.unparse envuelve en comillas — contar separadores + 1
const cols = headerLine.split('","').length
expect(cols).toBe(16)
})
it('tiene los headers esperados (muestra)', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv).toContain('ID BPMN')
expect(csv).toContain('Automatizable')
expect(csv).toContain('Costo actual total')
expect(csv).toContain('Costo automatizado total')
expect(csv).toContain('Ahorro')
expect(csv).toContain('Tiempo automatizado')
expect(csv).toContain('Recursos asignados')
})
})
describe('CSV tab=roi — metadata extendida', () => {
it('contiene campos de ROI en la metadata', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv).toContain('# Ahorro por ejecución')
expect(csv).toContain('# Ahorro anual')
expect(csv).toContain('# Payback (meses)')
expect(csv).toContain('# ROI anualizado')
expect(csv).toContain('# Frecuencia anual')
expect(csv).toContain('# Horizonte de análisis')
expect(csv).toContain('# Inversión en automatización')
})
it('costo total actual y automatizado están en la metadata', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv).toContain('# Costo total actual')
expect(csv).toContain('# Costo total automatizado')
// Valores concretos del fixture: 360 y 144
expect(csv).toContain('360.00')
expect(csv).toContain('144.00')
})
it('metadata incluye el horizonte y frecuencia configurados', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv).toContain('# Frecuencia anual: 1200')
expect(csv).toContain('# Horizonte de análisis: 3 años')
})
})
describe('CSV tab=roi — datos de actividades', () => {
it('actividad automatable tiene "true" en la columna Automatizable', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
// task_recv es automatable=true
expect(csv).toContain('"true"')
})
it('actividad NO automatable tiene "false" en la columna Automatizable', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv).toContain('"false"')
})
it('ahorro de task_recv: 240 - 24 = 216 aparece en los datos', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv).toContain('216.00')
})
it('actividad no automatable tiene ahorro 0 (costos iguales)', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
// task_valid: 120 - 120 = 0
expect(csv).toContain('0.00')
})
it('N+1 filas (N actividades + 1 header)', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const dataLines = csvNoBom.split('\r\n').filter((l) => l.trim() && !l.startsWith('#'))
expect(dataLines).toHaveLength(perActivityActual.length + 1)
})
})
describe('CSV tab=roi — separadores y BOM', () => {
it('BOM UTF-8 presente', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
expect(csv.charCodeAt(0)).toBe(0xFEFF)
})
it('USD: separador coma', () => {
const csv = generateCsvContent({ process: makeProcess({ currency: 'USD' }), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
expect(headerLine).toContain('","')
})
it('PYG: separador punto y coma', () => {
const csv = generateCsvContent({ process: makeProcess({ currency: 'PYG' }), simulation: mockSimulation, resources: [], tab: 'roi', activities: mockActivities })
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
expect(headerLine).toContain('";"')
})
})
// ─── CSV tab=automatizado ─────────────────────────────────────────────────────
describe('CSV tab=automatizado', () => {
it('usa los datos del escenario automatizado (totalCost=144)', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'automatizado' })
expect(csv).toContain('# Escenario: Automatizado')
expect(csv).toContain('144.00')
})
it('tiene 10 columnas (mismo formato que actual)', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'automatizado' })
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
const cols = headerLine.split('","').length
expect(cols).toBe(10)
})
})
// ─── CSV tab=actual — regresión ───────────────────────────────────────────────
describe('CSV tab=actual (regresión)', () => {
it('tab=actual funciona igual que antes (escenario actual, 10 cols)', () => {
const csv = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'actual' })
expect(csv).toContain('# Escenario: Actual')
expect(csv).toContain('360.00')
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const headerLine = csvNoBom.split('\r\n').find((l) => !l.startsWith('#') && l.trim())!
expect(headerLine.split('","').length).toBe(10)
})
it('tab=undefined → igual que tab=actual', () => {
const csvDefault = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [] })
const csvActual = generateCsvContent({ process: makeProcess(), simulation: mockSimulation, resources: [], tab: 'actual' })
expect(csvDefault).toBe(csvActual)
})
})