Files
inq-roi-simulador-web/tests/lib/export/csv-export.test.ts
Marcos Benítez bed161c92a feat: MVP Fase 0 — Process Cost Platform v0.1.0
Plataforma completa de análisis de costos operativos basada en BPMN 2.0.

Funcionalidades incluidas:
- Import de archivos BPMN 2.0 por drag & drop + 3 procesos de ejemplo
- Visualización read-only del diagrama (bpmn-js)
- Configuración de actividades (costo, tiempo, recursos asignados)
- CRUD de recursos (rol, persona, sistema, equipo, insumo)
- Configuración global (moneda, overhead %, nombre, cliente)
- Motor de simulación determinístico agregado con propagación de gateways XOR/AND
- Reporte visual con mapa de calor en dos modos (relativo/absoluto)
- Gráficos: KPIs, top actividades, composición, costo por recurso
- Export PDF (jsPDF + html2canvas) y CSV (papaparse)
- Persistencia en IndexedDB (Dexie)
- 268 tests unitarios/integración + 6 E2E con Playwright
- Deploy: https://process-cost-platform.pages.dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:58:40 -03:00

221 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.
import { describe, it, expect } from 'vitest'
import { generateCsvContent } from '@/lib/export/csv-export'
import type { Process, Simulation, Resource, SimulationResult } from '@/domain/types'
// ─── Fixtures ─────────────────────────────────────────────────────────────────
function makeProcess(currency: string, extras: Partial<Process> = {}): Process {
return {
id: 'p1', name: 'Proceso de Ventas', clientName: 'Empresa ABC',
bpmnXml: '', currency, overheadPercentage: 0.2,
createdAt: 0, updatedAt: 0, ...extras,
}
}
function makeSimulation(perActivity: SimulationResult['perActivity'] = []): Simulation {
const totalDirect = perActivity.reduce((s, a) => s + a.expectedDirectCost, 0)
const totalIndirect = perActivity.reduce((s, a) => s + a.expectedIndirectCost, 0)
return {
id: 'sim1', processId: 'p1',
executedAt: new Date('2026-05-13T10:00:00Z').getTime(),
result: {
totalCost: totalDirect + totalIndirect,
totalDirectCost: totalDirect,
totalIndirectCost: totalIndirect,
totalTimeMinutes: 90,
perActivity,
perResource: [],
warnings: [],
},
}
}
const mockActivities: SimulationResult['perActivity'] = [
{
activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud',
expectedDirectCost: 500, expectedIndirectCost: 100, expectedTotalCost: 600,
percentOfTotal: 60, executionProbability: 1.0, expectedExecutions: 1.0,
executionTimeMinutes: 30, resourceCostBreakdown: [],
},
{
activityId: 'a2', bpmnElementId: 'task2', activityName: 'Análisis de crédito',
expectedDirectCost: 250, expectedIndirectCost: 50, expectedTotalCost: 300,
percentOfTotal: 30, executionProbability: 0.8, expectedExecutions: 0.8,
executionTimeMinutes: 60, resourceCostBreakdown: [],
},
{
activityId: 'a3', bpmnElementId: 'task3', activityName: 'Notificación al cliente',
expectedDirectCost: 83.33, expectedIndirectCost: 16.67, expectedTotalCost: 100,
percentOfTotal: 10, executionProbability: 0.5, expectedExecutions: 0.5,
executionTimeMinutes: 15, resourceCostBreakdown: [],
},
]
const resources: Resource[] = []
// ─── BOM UTF-8 ────────────────────────────────────────────────────────────────
describe('CSV — BOM UTF-8', () => {
it('comienza con BOM UTF-8 (\\uFEFF)', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv.charCodeAt(0)).toBe(0xFEFF)
})
it('BOM está presente para PYG también', () => {
const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources })
expect(csv.charCodeAt(0)).toBe(0xFEFF)
})
})
// ─── Separadores ──────────────────────────────────────────────────────────────
describe('CSV — separadores', () => {
it('USD: separador de columnas es coma ","', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
// Los headers deben estar separados por coma
const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))!
expect(headerLine).toContain(',')
expect(headerLine).not.toContain(';')
})
it('PYG: separador de columnas es punto y coma ";"', () => {
const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources })
const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))!
expect(headerLine).toContain(';')
expect(headerLine).not.toContain(',"Actividad"')
})
it('BRL: separador es punto y coma', () => {
const csv = generateCsvContent({ process: makeProcess('BRL'), simulation: makeSimulation(mockActivities), resources })
const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))!
expect(headerLine).toContain(';')
})
it('EUR: separador es punto y coma', () => {
const csv = generateCsvContent({ process: makeProcess('EUR'), simulation: makeSimulation(mockActivities), resources })
const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))!
expect(headerLine).toContain(';')
})
})
// ─── Separadores decimales ─────────────────────────────────────────────────────
describe('CSV — separadores decimales', () => {
it('USD: decimal es punto "." → "500.00"', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('500.00')
})
it('PYG: decimal es coma "," → "500,00"', () => {
const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('500,00')
})
it('BRL: decimal es coma', () => {
const csv = generateCsvContent({ process: makeProcess('BRL'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('500,00')
})
})
// ─── Headers ──────────────────────────────────────────────────────────────────
describe('CSV — headers', () => {
it('contiene "Actividad" en los headers', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('Actividad')
})
it('header de costo incluye la moneda: "Costo total (USD)"', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('Costo total (USD)')
})
it('header de costo incluye la moneda: "Costo total (PYG)"', () => {
const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('Costo total (PYG)')
})
it('contiene todos los headers requeridos', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('ID BPMN')
expect(csv).toContain('Tiempo (min)')
expect(csv).toContain('% del total')
expect(csv).toContain('Ejecuciones esperadas')
expect(csv).toContain('Recursos asignados')
})
})
// ─── Cantidad de filas ────────────────────────────────────────────────────────
describe('CSV — cantidad de filas', () => {
it('tiene exactamente N filas de datos + header + metadata', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
// Quitar BOM antes de procesar para no confundir el filtro de comentarios
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const lines = csvNoBom.split('\r\n').filter((l) => l.trim() && !l.startsWith('#'))
// 1 header + N actividades
expect(lines).toHaveLength(mockActivities.length + 1)
})
it('0 actividades → solo header', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation([]), resources })
const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv
const lines = csvNoBom.split('\r\n').filter((l) => l.trim() && !l.startsWith('#'))
expect(lines).toHaveLength(1) // solo el header
})
})
// ─── Metadata de cabecera ─────────────────────────────────────────────────────
describe('CSV — metadata', () => {
it('la metadata comienza con "# Proceso:"', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('# Proceso: Proceso de Ventas')
})
it('la metadata contiene el nombre del cliente', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('# Cliente: Empresa ABC')
})
it('la metadata contiene el costo total', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources })
expect(csv).toContain('# Costo total (USD)')
})
})
// ─── Caracteres especiales / UTF-8 ───────────────────────────────────────────
describe('CSV — UTF-8 y caracteres especiales', () => {
it('nombres con acentos se preservan correctamente', () => {
const activitiesWithAccents: SimulationResult['perActivity'] = [{
activityId: 'a1', bpmnElementId: 't1',
activityName: 'Revisión y Análisis Crítico',
expectedDirectCost: 100, expectedIndirectCost: 20, expectedTotalCost: 120,
percentOfTotal: 100, executionProbability: 1, expectedExecutions: 1,
executionTimeMinutes: 30, resourceCostBreakdown: [],
}]
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(activitiesWithAccents), resources })
expect(csv).toContain('Revisión y Análisis Crítico')
})
it('nombre del proceso con ñ se preserva', () => {
const process = makeProcess('USD', { name: 'Gestión de la Compañía' })
const csv = generateCsvContent({ process, simulation: makeSimulation([]), resources })
expect(csv).toContain('Gestión de la Compañía')
})
it('actividad con nombre en blanco no rompe el CSV', () => {
const activitiesBlank: SimulationResult['perActivity'] = [{
activityId: 'a1', bpmnElementId: 't1', activityName: '',
expectedDirectCost: 0, expectedIndirectCost: 0, expectedTotalCost: 0,
percentOfTotal: 0, executionProbability: 1, expectedExecutions: 1,
executionTimeMinutes: 0, resourceCostBreakdown: [],
}]
expect(() => generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(activitiesBlank), resources })).not.toThrow()
})
})
// ─── Recursos vacíos ──────────────────────────────────────────────────────────
describe('CSV — recursos vacíos', () => {
it('actividad sin recursos → celda de recursos vacía (no rompe)', () => {
const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources: [] })
// No debe crashear y el contenido es válido
expect(csv.length).toBeGreaterThan(0)
expect(csv).toContain('Recibir solicitud')
})
})