135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
calculateCascadeWarnings,
|
|
getNextWarning,
|
|
checkWarnings,
|
|
getCascadeIntervals,
|
|
formatMinutesBefore,
|
|
CASCADE_PRESETS,
|
|
} from './cascade';
|
|
|
|
describe('cascade', () => {
|
|
const ONE_MIN = 60 * 1000;
|
|
const ONE_HOUR = 60 * ONE_MIN;
|
|
|
|
describe('CASCADE_PRESETS', () => {
|
|
it('aggressive has 9 intervals', () => {
|
|
expect(CASCADE_PRESETS.aggressive).toHaveLength(9);
|
|
});
|
|
|
|
it('standard has 5 intervals', () => {
|
|
expect(CASCADE_PRESETS.standard).toHaveLength(5);
|
|
});
|
|
|
|
it('none has 0 intervals', () => {
|
|
expect(CASCADE_PRESETS.none).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('calculateCascadeWarnings', () => {
|
|
it('creates warnings for all intervals', () => {
|
|
const targetTime = Date.now() + 3 * ONE_HOUR;
|
|
const warnings = calculateCascadeWarnings(targetTime, [120, 60, 30, 15, 5]);
|
|
expect(warnings).toHaveLength(5);
|
|
});
|
|
|
|
it('marks past warnings as fired', () => {
|
|
const now = Date.now();
|
|
const targetTime = now + 10 * ONE_MIN; // 10 min from now
|
|
const warnings = calculateCascadeWarnings(targetTime, [30, 15, 5], now);
|
|
// 30m and 15m warnings are in the past (target is only 10m away)
|
|
const firedCount = warnings.filter((w) => w.fired).length;
|
|
expect(firedCount).toBe(2);
|
|
});
|
|
|
|
it('returns empty array for empty intervals', () => {
|
|
const warnings = calculateCascadeWarnings(Date.now() + ONE_HOUR, []);
|
|
expect(warnings).toHaveLength(0);
|
|
});
|
|
|
|
it('sorts warnings by minutesBefore descending (earliest first)', () => {
|
|
const targetTime = Date.now() + 5 * ONE_HOUR;
|
|
const warnings = calculateCascadeWarnings(targetTime, [5, 60, 15, 120]);
|
|
expect(warnings[0].minutesBefore).toBe(120);
|
|
expect(warnings[3].minutesBefore).toBe(5);
|
|
});
|
|
|
|
it('all warnings are in the future when target is far away', () => {
|
|
const targetTime = Date.now() + 24 * ONE_HOUR;
|
|
const warnings = calculateCascadeWarnings(targetTime, [120, 60, 30]);
|
|
expect(warnings.every((w) => !w.fired)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getNextWarning', () => {
|
|
it('returns first unfired warning', () => {
|
|
const targetTime = Date.now() + 2 * ONE_HOUR;
|
|
const warnings = calculateCascadeWarnings(targetTime, [120, 60, 30, 15, 5]);
|
|
const next = getNextWarning(warnings);
|
|
expect(next).not.toBeNull();
|
|
expect(next!.fired).toBe(false);
|
|
});
|
|
|
|
it('returns null when all warnings are fired', () => {
|
|
const targetTime = Date.now() - ONE_MIN; // in the past
|
|
const warnings = calculateCascadeWarnings(targetTime, [5, 1]);
|
|
const next = getNextWarning(warnings);
|
|
expect(next).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('checkWarnings', () => {
|
|
it('fires warnings whose time has come', () => {
|
|
const now = Date.now();
|
|
const targetTime = now + 20 * ONE_MIN;
|
|
const warnings = calculateCascadeWarnings(targetTime, [30, 15, 5], now);
|
|
// At now, 30m warning is past (target - 30m < now)
|
|
// Advance time to 10m before target (5m warning should fire)
|
|
const laterNow = targetTime - 10 * ONE_MIN;
|
|
const fired = checkWarnings(warnings, laterNow);
|
|
expect(fired.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('does not re-fire already fired warnings', () => {
|
|
const now = Date.now();
|
|
const targetTime = now + 2 * ONE_MIN;
|
|
const warnings = calculateCascadeWarnings(targetTime, [5], now);
|
|
// 5m warning is already past since target is 2m away
|
|
const fired1 = checkWarnings(warnings, now);
|
|
const fired2 = checkWarnings(warnings, now + 1000);
|
|
// Second check should not re-fire
|
|
expect(fired2).toHaveLength(0);
|
|
expect(fired1.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
describe('getCascadeIntervals', () => {
|
|
it('returns preset intervals for standard', () => {
|
|
const intervals = getCascadeIntervals({ preset: 'standard', intervals: [] });
|
|
expect(intervals).toEqual(CASCADE_PRESETS.standard);
|
|
});
|
|
|
|
it('returns custom intervals sorted descending', () => {
|
|
const intervals = getCascadeIntervals({ preset: 'custom', intervals: [5, 30, 10] });
|
|
expect(intervals).toEqual([30, 10, 5]);
|
|
});
|
|
});
|
|
|
|
describe('formatMinutesBefore', () => {
|
|
it('formats minutes under 60', () => {
|
|
expect(formatMinutesBefore(5)).toBe('5m');
|
|
expect(formatMinutesBefore(15)).toBe('15m');
|
|
});
|
|
|
|
it('formats exact hours', () => {
|
|
expect(formatMinutesBefore(60)).toBe('1h');
|
|
expect(formatMinutesBefore(120)).toBe('2h');
|
|
});
|
|
|
|
it('formats hours and minutes', () => {
|
|
expect(formatMinutesBefore(90)).toBe('1h 30m');
|
|
expect(formatMinutesBefore(150)).toBe('2h 30m');
|
|
});
|
|
});
|
|
});
|