import { buildContinuousUsageHistory, filterSamplesByRange, } from '../src/training/continuousUsage'; import { type HeartRateSample } from '../src/training/trainingTypes'; function sample( timestamp: number, bpm: number, deviceName = 'Magene H003', ): HeartRateSample { return { timestamp, bpm, contactDetected: null, rrIntervalsMs: [], deviceName, }; } describe('buildContinuousUsageHistory', () => { it('groups continuous samples into usage blocks separated by long gaps', () => { const blocks = buildContinuousUsageHistory( [ sample(1_000, 120), sample(2_000, 130), sample(10 * 60_000, 140), sample(10 * 60_000 + 1_000, 150), ], { gapMs: 5 * 60_000 }, ); expect(blocks).toHaveLength(2); expect(blocks[0]).toMatchObject({ startedAt: 10 * 60_000, endedAt: 10 * 60_000 + 1_000, sampleCount: 2, averageBpm: 145, peakBpm: 150, minBpm: 140, }); expect(blocks[1]).toMatchObject({ startedAt: 1_000, endedAt: 2_000, sampleCount: 2, averageBpm: 125, peakBpm: 130, minBpm: 120, }); }); it('sorts samples and ignores invalid BPM values', () => { const blocks = buildContinuousUsageHistory( [sample(5_000, 150), sample(1_000, 0), sample(3_000, 120)], { gapMs: 10_000 }, ); expect(blocks).toHaveLength(1); expect(blocks[0].startedAt).toBe(3_000); expect(blocks[0].endedAt).toBe(5_000); expect(blocks[0].sampleCount).toBe(2); expect(blocks[0].samples.map(item => item.bpm)).toEqual([120, 150]); }); it('uses the most recent block first for history display', () => { const blocks = buildContinuousUsageHistory( [sample(1_000, 110), sample(30 * 60_000, 160)], { gapMs: 5 * 60_000 }, ); expect(blocks.map(block => block.startedAt)).toEqual([30 * 60_000, 1_000]); }); it('keeps a stable block id while live samples extend the same usage block', () => { const initial = buildContinuousUsageHistory( [sample(1_000, 120), sample(2_000, 130)], { gapMs: 5 * 60_000 }, ); const updated = buildContinuousUsageHistory( [sample(1_000, 120), sample(2_000, 130), sample(3_000, 140)], { gapMs: 5 * 60_000 }, ); expect(updated[0].id).toBe(initial[0].id); expect(updated[0].sampleCount).toBe(3); }); }); describe('filterSamplesByRange', () => { it('returns samples inside an inclusive custom date/time range', () => { const samples = [ sample(Date.UTC(2026, 5, 10, 8, 59), 110), sample(Date.UTC(2026, 5, 10, 9, 0), 120), sample(Date.UTC(2026, 5, 10, 9, 30), 130), sample(Date.UTC(2026, 5, 10, 10, 0), 140), sample(Date.UTC(2026, 5, 10, 10, 1), 150), ]; const filtered = filterSamplesByRange(samples, { start: Date.UTC(2026, 5, 10, 9, 0), end: Date.UTC(2026, 5, 10, 10, 0), }); expect(filtered.map(item => item.bpm)).toEqual([120, 130, 140]); }); it('supports ranges that cross multiple days', () => { const samples = [ sample(Date.UTC(2026, 5, 10, 23, 50), 120), sample(Date.UTC(2026, 5, 11, 0, 10), 130), sample(Date.UTC(2026, 5, 11, 1, 0), 140), ]; const filtered = filterSamplesByRange(samples, { start: Date.UTC(2026, 5, 10, 23, 45), end: Date.UTC(2026, 5, 11, 0, 15), }); expect(filtered.map(item => item.bpm)).toEqual([120, 130]); }); });