Files
inq-roi-simulador-web/tests/domain/simulation-sprint3.test.ts
Marcos Benítez 67e259e6ed feat(sprint-3): biblioteca, recursos TO-BE, ROI mejorado e identidad de marca
Sprint 3 completo — 7 etapas:

- Etapa 1-2: biblioteca de procesos con agrupación por cliente (Dexie v4,
  LibraryPage, library-store, groupId/tags en Process)
- Etapa 3: modal de configuración global por proceso (horizonte, frecuencia,
  inversión, moneda) con validación y persistencia
- Etapa 4: panel de recursos rediseñado con soporte de unidades y minutos
  de utilización (utilizationMinutes + units)
- Etapa 5: recursos en escenario automatizado — motor de simulación,
  UI de edición en ActivityPanel, comparación AS-IS/TO-BE en reporte
- Etapa 6: PDF recursos automatizados, automatedAssignedResources requerido,
  561 tests pasando
- Etapa 7: identidad de marca — AppHeader gradiente naranja/magenta con logo
  InQuality en 5 páginas, favicon con logo real, logo/marca navegan a home

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:49:43 -03:00

100 lines
4.2 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.
import { describe, it, expect } from 'vitest'
import { runSimulation } from '@/domain/simulation'
import type { Activity, Resource, SimulationInput } from '@/domain/types'
// BPMN de un solo nodo — execProb = 1.0, sin gateways
const SINGLE_TASK_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
<process id="proc1" isExecutable="false">
<startEvent id="start"><outgoing>f1</outgoing></startEvent>
<task id="taskA" name="Tarea A"><incoming>f1</incoming><outgoing>f2</outgoing></task>
<endEvent id="end"><incoming>f2</incoming></endEvent>
<sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
<sequenceFlow id="f2" sourceRef="taskA" targetRef="end"/>
</process>
</definitions>`
const makeResource = (id: string, costPerHour: number): Resource => ({
id, processId: 'proc1', name: `Recurso ${id}`, type: 'role', costPerHour,
})
const baseActivity = (overrides: Partial<Activity> = {}): Activity => ({
id: 'act-taskA', processId: 'proc1', bpmnElementId: 'taskA',
name: 'Tarea A', type: 'task',
directCostFixed: 0, executionTimeMinutes: 0,
assignedResources: [],
automatable: true,
automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [],
...overrides,
})
const baseInput = (activity: Activity, resources: Resource[] = []): SimulationInput => ({
processXml: SINGLE_TASK_BPMN,
activities: new Map([['taskA', activity]]),
gateways: new Map(),
resources: new Map(resources.map((r) => [r.id, r])),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
scenario: 'automated',
})
describe('Sprint 3 — automatedAssignedResources en motor de simulación', () => {
it('Caso 1: solo costo fijo, sin recursos TO-BE — resourceCostBreakdown vacío', () => {
const act = baseActivity({ automatedCostFixed: 100, automatedTimeMinutes: 60, automatedAssignedResources: [] })
const result = runSimulation(baseInput(act))
const actResult = result.perActivity[0]
expect(actResult.expectedDirectCost).toBeCloseTo(100)
expect(actResult.resourceCostBreakdown).toHaveLength(0)
})
it('Caso 2: solo recursos TO-BE — costo = costPerHour × (min/60) × units × execProb', () => {
const r1 = makeResource('r1', 30)
const act = baseActivity({
automatedCostFixed: 0, automatedTimeMinutes: 30,
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 2 }],
})
const result = runSimulation(baseInput(act, [r1]))
// 30 $/h × (60min/60) × 2 units × 1.0 execProb = 60
const actResult = result.perActivity[0]
expect(actResult.expectedDirectCost).toBeCloseTo(60)
expect(actResult.resourceCostBreakdown).toHaveLength(1)
expect(actResult.resourceCostBreakdown[0].resourceId).toBe('r1')
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(60)
})
it('Caso 3: mixto — costo fijo + recurso TO-BE se suman', () => {
const r1 = makeResource('r1', 60)
const act = baseActivity({
automatedCostFixed: 50, automatedTimeMinutes: 30,
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }],
})
const result = runSimulation(baseInput(act, [r1]))
// fijo=50 + 60*(30/60)*1=30 → total=80
const actResult = result.perActivity[0]
expect(actResult.expectedDirectCost).toBeCloseTo(80)
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(30)
})
it('Caso 4: actividad NO automatable — usa assignedResources, ignora automatedAssignedResources', () => {
const r1 = makeResource('r1', 60)
const r2 = makeResource('r2', 100)
const act = baseActivity({
automatable: false,
directCostFixed: 0,
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }],
automatedAssignedResources: [{ resourceId: 'r2', utilizationMinutes: 60, units: 1 }],
})
const result = runSimulation(baseInput(act, [r1, r2]))
// escenario automated pero automatable=false → usa r1 (assignedResources), no r2
const actResult = result.perActivity[0]
expect(actResult.resourceCostBreakdown).toHaveLength(1)
expect(actResult.resourceCostBreakdown[0].resourceId).toBe('r1')
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(60)
})
})