Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Tipos: Resource.processId? eliminado (vestigial); ResourceType ya tenía los 5 valores correctos, sin cambios. resource-repo.ts: agregado getAllWithCreator() con JOIN a users!created_by (mismo patrón que Etapa 6) y tipo ResourceWithCreator. delete() ahora valida que no haya asignaciones activas en activity_resource_assignments antes de borrar — lanza error explicativo si las hay. Verifiqué ambos contra Supabase real: el JOIN funciona (FK reconocida por PostgREST) y el conteo de guarda también. Nuevo — src/features/resources/: resource-display.tsx — labels, colores/íconos por tipo (Lucide: User/Briefcase/Server/Wrench/Package) y ResourceTypeAvatar compartido ResourceFormSheet.tsx — Sheet reutilizable para crear/editar, usado tanto en el catálogo como en el panel del workspace ResourceCatalogPage.tsx — página /recursos: búsqueda, filtro por tipo, tabla con badge de tipo, costo, "creado por", editar/eliminar con confirmación Nuevo — src/components/ui/sheet.tsx: componente shadcn Sheet (no existía), construido sobre @radix-ui/react-dialog que ya estaba instalado — no agregué dependencias nuevas. ResourcesPanel.tsx rediseñado: ahora solo muestra recursos usados en el proceso actual (derivado de assignedResources), con conteo de actividades por recurso. CRUD completo se mudó a /recursos; el panel solo permite "Nuevo" (catálogo global) y tiene el link "Ver catálogo completo". Router: ruta /recursos registrada. ActivityPanel no se tocó, como indicaba el scope.
166 lines
6.0 KiB
TypeScript
166 lines
6.0 KiB
TypeScript
/**
|
|
* Tests unitarios de drawResourcesSection (Sprint 2, Etapa 6).
|
|
*
|
|
* drawResourcesSection toma doc: any y autoTable: Function directamente,
|
|
* por lo que no necesita mock de jspdf — se pasa un objeto plano.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import type { ActivitySimResult, Resource, Activity } from '@/domain/types'
|
|
|
|
// ─── Mock doc (jsPDF API mínima usada por drawResourcesSection) ───────────────
|
|
|
|
const mockDoc = {
|
|
setFont: vi.fn(),
|
|
setFontSize: vi.fn(),
|
|
setTextColor: vi.fn(),
|
|
setDrawColor: vi.fn(),
|
|
setLineWidth: vi.fn(),
|
|
text: vi.fn(),
|
|
line: vi.fn(),
|
|
}
|
|
|
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
|
|
const mockResource: Resource = {
|
|
id: 'r1', name: 'Analista Senior', type: 'role', costPerHour: 60,
|
|
}
|
|
|
|
const mockActivity: Activity = {
|
|
id: 'a1', processId: 'p1', bpmnElementId: 'task1', name: 'Recibir solicitud', type: 'task',
|
|
directCostFixed: 600, executionTimeMinutes: 60,
|
|
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }],
|
|
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
|
|
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 5, units: 3 }],
|
|
}
|
|
|
|
const activityWithResources: ActivitySimResult = {
|
|
activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud',
|
|
expectedDirectCost: 660, expectedIndirectCost: 0, expectedTotalCost: 660,
|
|
percentOfTotal: 100, executionProbability: 1.0, expectedExecutions: 1.0,
|
|
executionTimeMinutes: 60,
|
|
resourceCostBreakdown: [{ resourceId: 'r1', resourceName: 'Analista Senior', cost: 60 }],
|
|
}
|
|
|
|
const activityWithoutResources: ActivitySimResult = {
|
|
activityId: 'a2', bpmnElementId: 'task2', activityName: 'Archivar',
|
|
expectedDirectCost: 100, expectedIndirectCost: 0, expectedTotalCost: 100,
|
|
percentOfTotal: 0, executionProbability: 1.0, expectedExecutions: 1.0,
|
|
executionTimeMinutes: 10, resourceCostBreakdown: [],
|
|
}
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
describe('drawResourcesSection', () => {
|
|
let autoTableMock: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
autoTableMock = vi.fn()
|
|
})
|
|
|
|
it('no lanza error con actividades que tienen recursos', async () => {
|
|
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
|
|
expect(() =>
|
|
drawResourcesSection(
|
|
mockDoc,
|
|
autoTableMock,
|
|
{ perActivity: [activityWithResources] },
|
|
[mockResource],
|
|
[mockActivity],
|
|
'USD',
|
|
40
|
|
)
|
|
).not.toThrow()
|
|
expect(autoTableMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('no llama a autoTable si todas las actividades tienen resourceCostBreakdown vacío', async () => {
|
|
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
|
|
drawResourcesSection(
|
|
mockDoc,
|
|
autoTableMock,
|
|
{ perActivity: [activityWithoutResources] },
|
|
[mockResource],
|
|
[mockActivity],
|
|
'USD',
|
|
40
|
|
)
|
|
expect(autoTableMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('el header de autoTable incluye la moneda correcta', async () => {
|
|
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
|
|
drawResourcesSection(
|
|
mockDoc,
|
|
autoTableMock,
|
|
{ perActivity: [activityWithResources] },
|
|
[mockResource],
|
|
[mockActivity],
|
|
'PYG',
|
|
40
|
|
)
|
|
const callArgs = autoTableMock.mock.calls[0][1] as { head: unknown[][] }
|
|
const headFlat = JSON.stringify(callArgs.head)
|
|
expect(headFlat).toContain('PYG')
|
|
})
|
|
|
|
it('llama a autoTable exactamente una vez aunque haya varias actividades con recursos', async () => {
|
|
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
|
|
const secondActivity: ActivitySimResult = {
|
|
...activityWithResources,
|
|
activityId: 'a3', activityName: 'Evaluar solicitud',
|
|
resourceCostBreakdown: [{ resourceId: 'r1', resourceName: 'Analista Senior', cost: 30 }],
|
|
}
|
|
drawResourcesSection(
|
|
mockDoc,
|
|
autoTableMock,
|
|
{ perActivity: [activityWithResources, secondActivity] },
|
|
[mockResource],
|
|
[mockActivity],
|
|
'USD',
|
|
40
|
|
)
|
|
expect(autoTableMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
// ─── Escenario automatizado usa automatedAssignedResources ────────────────────
|
|
|
|
describe('drawResourcesSection — escenario automatizado', () => {
|
|
let autoTableMock: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
autoTableMock = vi.fn()
|
|
})
|
|
|
|
it('escenario automatizado: pasa utilizationMinutes y units de automatedAssignedResources', async () => {
|
|
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
|
|
drawResourcesSection(
|
|
mockDoc, autoTableMock,
|
|
{ perActivity: [activityWithResources] },
|
|
[mockResource], [mockActivity], 'USD', 40,
|
|
'automatizado'
|
|
)
|
|
const body = (autoTableMock.mock.calls[0][1] as any).body as any[][]
|
|
const row = body[0]
|
|
// Min. (índice 4) y Uds. (índice 5) deben venir de automatedAssignedResources: { utilMin: 5, units: 3 }
|
|
expect(row[4].content).toBe('5')
|
|
expect(row[5].content).toBe('3')
|
|
})
|
|
|
|
it('escenario actual: pasa utilizationMinutes y units de assignedResources (sin regresión)', async () => {
|
|
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
|
|
drawResourcesSection(
|
|
mockDoc, autoTableMock,
|
|
{ perActivity: [activityWithResources] },
|
|
[mockResource], [mockActivity], 'USD', 40,
|
|
'actual'
|
|
)
|
|
const body = (autoTableMock.mock.calls[0][1] as any).body as any[][]
|
|
const row = body[0]
|
|
// assignedResources: { utilMin: 60, units: 1 }
|
|
expect(row[4].content).toBe('60')
|
|
expect(row[5].content).toBe('1')
|
|
})
|
|
})
|