Los cambios en reporte de eficiencia
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
This commit is contained in:
126
tests/features/report/efficiency.test.ts
Normal file
126
tests/features/report/efficiency.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Tests de las fórmulas "Ahorro esperado" y "Eficiencia" de ComparisonTable (Sprint 5, Etapa 5).
|
||||
* expectedSavings = savings × executionProbability
|
||||
* efficiency = expectedSavings / executionTimeMinutes
|
||||
* Reglas de null: no automatable → null; savings <= 0 → null; efficiency además requiere
|
||||
* executionTimeMinutes > 0.
|
||||
*
|
||||
* Las fórmulas viven inline en el useMemo de ComparisonTable.tsx (no hay función pura
|
||||
* exportada — por scope de la etapa no se modifica el dominio). Estos tests replican
|
||||
* exactamente esa lógica para verificar las fórmulas y las reglas de null/ranking.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
interface Activity {
|
||||
savings: number
|
||||
automatable: boolean
|
||||
executionProbability: number
|
||||
executionTimeMinutes: number
|
||||
}
|
||||
|
||||
function computeExpectedSavings(act: Activity): number | null {
|
||||
return act.automatable && act.savings > 0 ? act.savings * act.executionProbability : null
|
||||
}
|
||||
|
||||
function computeEfficiency(act: Activity): number | null {
|
||||
return act.automatable && act.savings > 0 && act.executionTimeMinutes > 0
|
||||
? (act.savings * act.executionProbability) / act.executionTimeMinutes
|
||||
: null
|
||||
}
|
||||
|
||||
describe('ComparisonTable — fórmula Ahorro esperado', () => {
|
||||
it('calcula savings × executionProbability', () => {
|
||||
const act: Activity = { savings: 540, automatable: true, executionProbability: 0.8, executionTimeMinutes: 60 }
|
||||
expect(computeExpectedSavings(act)).toBeCloseTo(432)
|
||||
})
|
||||
|
||||
it('probability 1.0 → expectedSavings === savings', () => {
|
||||
const act: Activity = { savings: 270, automatable: true, executionProbability: 1.0, executionTimeMinutes: 30 }
|
||||
expect(computeExpectedSavings(act)).toBe(270)
|
||||
})
|
||||
|
||||
it('no automatable → null', () => {
|
||||
const act: Activity = { savings: 540, automatable: false, executionProbability: 1.0, executionTimeMinutes: 60 }
|
||||
expect(computeExpectedSavings(act)).toBeNull()
|
||||
})
|
||||
|
||||
it('savings <= 0 → null', () => {
|
||||
const act: Activity = { savings: 0, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }
|
||||
expect(computeExpectedSavings(act)).toBeNull()
|
||||
const negative: Activity = { savings: -100, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }
|
||||
expect(computeExpectedSavings(negative)).toBeNull()
|
||||
})
|
||||
|
||||
it('executionProbability 0 → resultado 0, no null (0 sigue siendo un valor válido salvo que savings <= 0)', () => {
|
||||
const act: Activity = { savings: 540, automatable: true, executionProbability: 0, executionTimeMinutes: 60 }
|
||||
expect(computeExpectedSavings(act)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ComparisonTable — fórmula Eficiencia', () => {
|
||||
it('calcula (savings × executionProbability) / executionTimeMinutes', () => {
|
||||
const act: Activity = { savings: 540, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }
|
||||
expect(computeEfficiency(act)).toBeCloseTo(9.0)
|
||||
})
|
||||
|
||||
it('considera executionProbability fraccionaria', () => {
|
||||
const act: Activity = { savings: 300, automatable: true, executionProbability: 0.5, executionTimeMinutes: 30 }
|
||||
expect(computeEfficiency(act)).toBeCloseTo(5.0)
|
||||
})
|
||||
|
||||
it('no automatable → null', () => {
|
||||
const act: Activity = { savings: 540, automatable: false, executionProbability: 1.0, executionTimeMinutes: 60 }
|
||||
expect(computeEfficiency(act)).toBeNull()
|
||||
})
|
||||
|
||||
it('savings <= 0 → null', () => {
|
||||
const act: Activity = { savings: -50, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }
|
||||
expect(computeEfficiency(act)).toBeNull()
|
||||
})
|
||||
|
||||
it('executionTimeMinutes === 0 → null aunque savings sea válido', () => {
|
||||
const act: Activity = { savings: 540, automatable: true, executionProbability: 1.0, executionTimeMinutes: 0 }
|
||||
expect(computeEfficiency(act)).toBeNull()
|
||||
})
|
||||
|
||||
it('executionTimeMinutes === 0 pero expectedSavings sigue siendo válido (no null)', () => {
|
||||
const act: Activity = { savings: 540, automatable: true, executionProbability: 1.0, executionTimeMinutes: 0 }
|
||||
expect(computeExpectedSavings(act)).toBe(540)
|
||||
expect(computeEfficiency(act)).toBeNull()
|
||||
})
|
||||
|
||||
it('executionProbability 0 → resultado 0, no null', () => {
|
||||
const act: Activity = { savings: 540, automatable: true, executionProbability: 0, executionTimeMinutes: 60 }
|
||||
expect(computeEfficiency(act)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ComparisonTable — ranking con valores null', () => {
|
||||
function rankByEfficiency(acts: Activity[]): (number | null)[] {
|
||||
return acts
|
||||
.map(computeEfficiency)
|
||||
.sort((a, b) => (b ?? -Infinity) - (a ?? -Infinity))
|
||||
}
|
||||
|
||||
it('filas con efficiency null quedan siempre al final del sort descendente', () => {
|
||||
const acts: Activity[] = [
|
||||
{ savings: -10, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }, // null
|
||||
{ savings: 600, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }, // 10
|
||||
{ savings: 60, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }, // 1
|
||||
]
|
||||
const ranked = rankByEfficiency(acts)
|
||||
expect(ranked[0]).toBeCloseTo(10)
|
||||
expect(ranked[1]).toBeCloseTo(1)
|
||||
expect(ranked[ranked.length - 1]).toBeNull()
|
||||
})
|
||||
|
||||
it('fila nula siempre por debajo de una fila con efficiency 0.01', () => {
|
||||
const acts: Activity[] = [
|
||||
{ savings: -5, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }, // null
|
||||
{ savings: 0.6, automatable: true, executionProbability: 1.0, executionTimeMinutes: 60 }, // 0.01
|
||||
]
|
||||
const ranked = rankByEfficiency(acts)
|
||||
expect(ranked[0]).toBeCloseTo(0.01)
|
||||
expect(ranked[1]).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -286,17 +286,23 @@ describe('ComparisonTable', () => {
|
||||
currency="USD"
|
||||
/>
|
||||
)
|
||||
// Buscar el th que contiene "Ahorro" (hay múltiples elementos con ese texto)
|
||||
// Buscar el th que contiene "Ahorro" exacto (no "Ahorro esperado" — hay múltiples
|
||||
// columnas cuyo texto incluye "Ahorro" desde Etapa 5)
|
||||
const headers = screen.getAllByRole('columnheader')
|
||||
const ahorroHeader = headers.find((h) => /Ahorro/i.test(h.textContent ?? ''))
|
||||
const ahorroHeader = headers.find((h) => /^Ahorro$/i.test((h.textContent ?? '').trim()))
|
||||
expect(ahorroHeader).toBeDefined()
|
||||
|
||||
// Antes del click: orden desc (a1=540 primero)
|
||||
// Sort por defecto es 'efficiency' (Etapa 5). Con esta fixture, executionProbability
|
||||
// y executionTimeMinutes son iguales en todas las filas, así que ordenar por
|
||||
// efficiency-desc y por savings-desc da el mismo orden visual (a1 primero).
|
||||
const rowsBefore = screen.getAllByRole('row').slice(1)
|
||||
const firstNameBefore = rowsBefore[0].textContent
|
||||
|
||||
// Primer click: cambia la columna activa a 'savings' (desc) — el orden no cambia
|
||||
// para esta fixture porque coincide con el de 'efficiency'.
|
||||
fireEvent.click(ahorroHeader!)
|
||||
// Segundo click: misma columna ya activa → invierte a asc (a3/null al final, a2 primero)
|
||||
fireEvent.click(ahorroHeader!)
|
||||
// Después del click: orden asc (a3=0 primero)
|
||||
const rowsAfter = screen.getAllByRole('row').slice(1)
|
||||
const firstNameAfter = rowsAfter[0].textContent
|
||||
|
||||
|
||||
Reference in New Issue
Block a user