Files
gruperly/apps/backend/test/analytics-risk.test.ts
Jose Selesan 2e184845e1 feat: add absence notification and analytics features
- Implemented AbsenceNotifyView for notifying absence with session details.
- Created AnalyticsView for displaying group analytics and managing students at risk.
- Added ClassAttendanceView for marking attendance in classes.
- Defined new schemas for analytics and attendance in shared package.
2026-09-23 16:24:45 -03:00

79 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import {
classifyRisk,
lastAttendedAt,
monthlyAttendanceRate,
trailingAbsentStreak,
} from '@/modules/analytics/lib/risk';
const day = 24 * 60 * 60 * 1000;
const at = (offsetDays: number) => new Date(Date.now() - offsetDays * day);
function entry(status: 'PRESENT' | 'ABSENT' | 'EXCUSED', offsetDays: number) {
return { status, occurredAt: at(offsetDays) };
}
describe('trailingAbsentStreak', () => {
it('devuelve 0 sin registros', () => {
expect(trailingAbsentStreak([])).toBe(0);
});
it('cuenta ausencias consecutivas sin justificar desde la más reciente', () => {
expect(trailingAbsentStreak([entry('ABSENT', 0)])).toBe(1);
expect(trailingAbsentStreak([entry('ABSENT', 0), entry('ABSENT', 7), entry('ABSENT', 14)])).toBe(3);
});
it('rompe la cadena con PRESENT o EXCUSED', () => {
expect(
trailingAbsentStreak([entry('ABSENT', 0), entry('ABSENT', 7), entry('PRESENT', 14)]),
).toBe(2);
expect(trailingAbsentStreak([entry('EXCUSED', 0), entry('ABSENT', 7)])).toBe(0);
expect(trailingAbsentStreak([entry('PRESENT', 0)])).toBe(0);
});
});
describe('lastAttendedAt', () => {
it('devuelve la clase PRESENT más reciente', () => {
const result = lastAttendedAt([
entry('PRESENT', 0),
entry('ABSENT', 7),
entry('ABSENT', 14),
]);
expect(result).not.toBeNull();
expect(result?.getTime()).toBe(at(0).getTime());
});
it('devuelve null si nunca asistió', () => {
expect(lastAttendedAt([entry('ABSENT', 0), entry('EXCUSED', 7)])).toBeNull();
expect(lastAttendedAt([])).toBeNull();
});
});
describe('monthlyAttendanceRate', () => {
it('razón de PRESENT sobre el total de sesiones', () => {
expect(monthlyAttendanceRate([entry('PRESENT', 0), entry('PRESENT', 7), entry('ABSENT', 14)], 5)).toBe(40);
});
it('devuelve 0 sin sesiones o sin presentes', () => {
expect(monthlyAttendanceRate([], 3)).toBe(0);
expect(monthlyAttendanceRate([entry('ABSENT', 0)], 3)).toBe(0);
});
});
describe('classifyRisk', () => {
it('HIGH con 3 o más ausencias consecutivas', () => {
expect(classifyRisk(3, 80)).toBe('HIGH');
expect(classifyRisk(5, 20)).toBe('HIGH');
});
it('MEDIUM con 2 ausencias consecutivas o asistencia menor al 50%', () => {
expect(classifyRisk(2, 90)).toBe('MEDIUM');
expect(classifyRisk(1, 40)).toBe('MEDIUM');
expect(classifyRisk(2, 40)).toBe('MEDIUM');
});
it('sin riesgo si no cumple ningún umbral', () => {
expect(classifyRisk(1, 60)).toBeNull();
expect(classifyRisk(0, 60)).toBeNull();
});
});