import { describe, it, expect } from 'vitest'; import { capChips, doneStats, backlogSeries, throughputSeries, areaPath, type KpiTask, } from '../src/server/board/viewmodel.js'; const day = 24 * 3600 * 1000; const iso = (msAgo: number) => new Date(Date.now() - msAgo).toISOString(); describe('capChips', () => { it('shows all chips when at most max', () => { const chips = [{ name: 'claude', minutes: 5 }, { name: 'codex', minutes: 2 }]; expect(capChips(chips, 3)).toEqual({ visible: chips, hidden: 0 }); }); it('caps at max and reports the hidden count', () => { const chips = ['a', 'b', 'c', 'd', 'e'].map((name) => ({ name, minutes: 1 })); const r = capChips(chips, 3); expect(r.visible.map((c) => c.name)).toEqual(['a', 'b', 'c']); expect(r.hidden).toBe(2); }); }); describe('doneStats', () => { it('computes share and weekly count, excluding cancelled from total', () => { const tasks: KpiTask[] = [ { id: '1', status: 'done', updatedAt: iso(2 * day) }, { id: '2', status: 'done', updatedAt: iso(10 * day) }, { id: '3', status: 'open' }, { id: '4', status: 'cancelled' }, ]; const r = doneStats(tasks); expect(r.done).toBe(2); expect(r.total).toBe(3); // cancelled excluded expect(r.pct).toBe(67); expect(r.doneThisWeek).toBe(1); }); it('handles empty input', () => { expect(doneStats([])).toEqual({ done: 0, total: 0, pct: 0, doneThisWeek: 0 }); }); }); describe('backlogSeries', () => { it('returns one value per day, oldest first, ending today', () => { const tasks: KpiTask[] = [ { id: '1', status: 'open', createdAt: iso(3 * day) }, { id: '2', status: 'done', createdAt: iso(13 * day), updatedAt: iso(1 * day) }, ]; const s = backlogSeries(tasks, 14); expect(s).toHaveLength(14); expect(s[13]).toBe(1); // today: only the open task is backlog expect(s[0]).toBe(1); // 13 days ago: only task 2 existed and was not done yet }); }); describe('throughputSeries', () => { it('counts done tasks per day', () => { const tasks: KpiTask[] = [ { id: '1', status: 'done', updatedAt: iso(0) }, { id: '2', status: 'done', updatedAt: iso(0) }, { id: '3', status: 'done', updatedAt: iso(5 * day) }, { id: '4', status: 'open', updatedAt: iso(0) }, ]; const s = throughputSeries(tasks, 14); expect(s).toHaveLength(14); expect(s[13]).toBe(2); // today expect(s[8]).toBe(1); // 5 days ago expect(s.reduce((a, b) => a + b, 0)).toBe(3); }); }); describe('areaPath', () => { it('builds line and area paths scaled to width/height', () => { const { line, area } = areaPath([0, 5, 10], 100, 50); expect(line).toBe('M0,50 L50,25 L100,0'); expect(area).toBe('M0,50 L50,25 L100,0 L100,50 L0,50 Z'); }); it('flattens when all values are equal (no division by zero)', () => { const { line } = areaPath([3, 3, 3], 90, 30); expect(line).toBe('M0,15 L45,15 L90,15'); }); });