128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { timeFormat } from 'd3-time-format';
|
|
import { timeDay } from 'd3-time';
|
|
import { calculateStreak, calculateStats } from '../src/heatmap';
|
|
import type { DayEntry } from '../src/types';
|
|
|
|
const DATE_FORMAT = timeFormat('%Y-%m-%d');
|
|
|
|
function daysAgo(n: number): string {
|
|
const d = new Date();
|
|
d.setHours(0, 0, 0, 0);
|
|
return DATE_FORMAT(timeDay.offset(d, -n));
|
|
}
|
|
|
|
describe('calculateStreak', () => {
|
|
it('returns 0 for empty data', () => {
|
|
expect(calculateStreak([])).toBe(0);
|
|
});
|
|
|
|
it('returns 0 when no days have hours > 0', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: daysAgo(0), hours: 0, count: 0 },
|
|
];
|
|
expect(calculateStreak(days)).toBe(0);
|
|
});
|
|
|
|
it('returns 1 for a single entry today', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: daysAgo(0), hours: 2.0, count: 1 },
|
|
];
|
|
expect(calculateStreak(days)).toBe(1);
|
|
});
|
|
|
|
it('counts consecutive days ending today', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: daysAgo(0), hours: 1.0, count: 1 },
|
|
{ date: daysAgo(1), hours: 2.0, count: 1 },
|
|
{ date: daysAgo(2), hours: 3.0, count: 1 },
|
|
];
|
|
expect(calculateStreak(days)).toBe(3);
|
|
});
|
|
|
|
it('starts from yesterday if today has no entry', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: daysAgo(1), hours: 2.0, count: 1 },
|
|
{ date: daysAgo(2), hours: 3.0, count: 1 },
|
|
];
|
|
expect(calculateStreak(days)).toBe(2);
|
|
});
|
|
|
|
it('breaks streak at gaps', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: daysAgo(0), hours: 1.0, count: 1 },
|
|
{ date: daysAgo(1), hours: 2.0, count: 1 },
|
|
// gap at daysAgo(2)
|
|
{ date: daysAgo(3), hours: 3.0, count: 1 },
|
|
];
|
|
expect(calculateStreak(days)).toBe(2);
|
|
});
|
|
|
|
it('does not count days with 0 hours as tracked', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: daysAgo(0), hours: 1.0, count: 1 },
|
|
{ date: daysAgo(1), hours: 0, count: 0 },
|
|
{ date: daysAgo(2), hours: 2.0, count: 1 },
|
|
];
|
|
expect(calculateStreak(days)).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('calculateStats', () => {
|
|
it('returns zeros for empty data', () => {
|
|
const stats = calculateStats([]);
|
|
expect(stats.totalHours).toBe(0);
|
|
expect(stats.avgHours).toBe(0);
|
|
expect(stats.busiestDay).toBeNull();
|
|
});
|
|
|
|
it('returns zeros when all hours are 0', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: '2025-01-01', hours: 0, count: 0 },
|
|
];
|
|
const stats = calculateStats(days);
|
|
expect(stats.totalHours).toBe(0);
|
|
expect(stats.busiestDay).toBeNull();
|
|
});
|
|
|
|
it('computes total hours correctly', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: '2025-01-01', hours: 2.5, count: 1 },
|
|
{ date: '2025-01-02', hours: 3.5, count: 2 },
|
|
{ date: '2025-01-03', hours: 4.0, count: 1 },
|
|
];
|
|
const stats = calculateStats(days);
|
|
expect(stats.totalHours).toBe(10);
|
|
});
|
|
|
|
it('computes average over days with entries only', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: '2025-01-01', hours: 3.0, count: 1 },
|
|
{ date: '2025-01-02', hours: 0, count: 0 },
|
|
{ date: '2025-01-03', hours: 6.0, count: 2 },
|
|
];
|
|
const stats = calculateStats(days);
|
|
// avg = 9.0 / 2 = 4.5 (not 9/3)
|
|
expect(stats.avgHours).toBe(4.5);
|
|
});
|
|
|
|
it('identifies the busiest day', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: '2025-01-01', hours: 2.0, count: 1 },
|
|
{ date: '2025-01-02', hours: 8.5, count: 3 },
|
|
{ date: '2025-01-03', hours: 4.0, count: 2 },
|
|
];
|
|
const stats = calculateStats(days);
|
|
expect(stats.busiestDay).toEqual({ date: '2025-01-02', hours: 8.5 });
|
|
});
|
|
|
|
it('rounds to one decimal place', () => {
|
|
const days: DayEntry[] = [
|
|
{ date: '2025-01-01', hours: 1.33, count: 1 },
|
|
{ date: '2025-01-02', hours: 2.67, count: 1 },
|
|
];
|
|
const stats = calculateStats(days);
|
|
expect(stats.totalHours).toBe(4);
|
|
expect(stats.avgHours).toBe(2);
|
|
});
|
|
});
|