aboutsummaryrefslogtreecommitdiff
path: root/src/lib/shared/routine-metrics.test.ts
blob: 4a84edae9ff1d535c24c2b0b054444f9a2377a1f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

import { describe, it, expect } from 'vitest';
import {
	computeRoutineState,
	computeHealthScore,
	parseIntervalToDays,
} from './routine-metrics';

const DAY_MS = 24 * 60 * 60 * 1000;
const CREATED = '2026-09-01T00:00:00.000Z';

const routine = {
	id: 1,
	version: 1,
	name: 'clean kitchen',
	frequencyDays: 7,
	createdAt: CREATED,
};

describe('computeRoutineState', () => {
	it('is ok and healthy before the due date', () => {
		const now = Date.parse(CREATED) + 3 * DAY_MS;
		const state = computeRoutineState(routine, [], now);
		expect(state.status).toBe('ok');
		expect(state.overdueDays).toBe(0);
		expect(state.health).toBe(100);
		expect(state.dueAt).toBe(
			new Date(Date.parse(CREATED) + 7 * DAY_MS).toISOString(),
		);
	});

	it('uses the last completion as reference once done', () => {
		const doneAt = new Date(Date.parse(CREATED) + 5 * DAY_MS).toISOString();
		const now = Date.parse(doneAt) + 2 * DAY_MS;
		const state = computeRoutineState(routine, [{ routineId: 1, doneAt }], now);
		expect(state.lastDoneAt).toBe(doneAt);
		expect(state.status).toBe('ok');
	});

	it('is due at exactly the frequency', () => {
		const now = Date.parse(CREATED) + 7 * DAY_MS;
		const state = computeRoutineState(routine, [], now);
		expect(state.status).toBe('due');
		expect(state.health).toBe(100);
	});

	it('is very urgent (veryLate) when two weeks old for a weekly routine', () => {
		const now = Date.parse(CREATED) + 15 * DAY_MS;
		const state = computeRoutineState(routine, [], now);
		expect(state.status).toBe('veryLate');
		expect(state.overdueDays).toBe(8);
		expect(state.health).toBe(0);
	});

	it('is late when overdue by half a frequency', () => {
		const now = Date.parse(CREATED) + 10.5 * DAY_MS;
		const state = computeRoutineState(routine, [], now);
		expect(state.status).toBe('late');
	});

	it('health of 75 means a quarter of the period overdue', () => {
		const now = Date.parse(CREATED) + 7 * DAY_MS + 1.75 * DAY_MS;
		const state = computeRoutineState(routine, [], now);
		expect(state.health).toBe(75);
	});
});

describe('parseIntervalToDays', () => {
	it('converts units to days', () => {
		expect(parseIntervalToDays('1d')).toBe(1);
		expect(parseIntervalToDays('2w')).toBe(14);
		expect(parseIntervalToDays('3m')).toBe(90);
		expect(parseIntervalToDays('1y')).toBe(365);
	});

	it('accepts whitespace and uppercase', () => {
		expect(parseIntervalToDays(' 2W ')).toBe(14);
	});

	it('rejects invalid input', () => {
		expect(parseIntervalToDays('2')).toBeNull();
		expect(parseIntervalToDays('w')).toBeNull();
		expect(parseIntervalToDays('2x')).toBeNull();
		expect(parseIntervalToDays('-1d')).toBeNull();
		expect(parseIntervalToDays('')).toBeNull();
	});
});

describe('computeHealthScore', () => {
	it('is 100 with no routines', () => {
		expect(computeHealthScore([])).toBe(100);
	});

	it('averages routine healths', () => {
		const now = Date.parse(CREATED) + 8 * DAY_MS;
		const a = computeRoutineState(routine, [], now); // overdue 1 day
		const b = computeRoutineState(
			routine,
			[],
			Date.parse(CREATED) + 3 * DAY_MS,
		);
		expect(computeHealthScore([a, b])).toBe(
			Math.round((a.health + b.health) / 2),
		);
	});
});