import { describe, it, expect } from 'vitest'; import { advancePlaybackIndex } from '../lib/telemetry-track'; import type { TelemetryPoint } from '../lib/telemetry-export'; const BASE = Date.parse('2026-09-28T14:30:00.000Z'); // Minimal TelemetryPoint builder — advancePlaybackIndex only reads timestamp + segmentId. function mk(offsetMs: number, segmentId = 0): TelemetryPoint { return { timestamp: new Date(BASE + offsetMs).toISOString(), latitude: 40, longitude: -74, altitude: 0, speed: 0, heading: 0, gpsAccuracy: 0, leanAngle: 0, pitchAngle: 0, accelX: 0, accelY: 0, accelZ: 0, longitudinalG: 0, lateralG: 0, segmentId, }; } describe('advancePlaybackIndex (replay playback synchronization)', () => { const uniform = Array.from({ length: 10 }, (_, i) => mk(i * 50)); // 20 Hz, 50 ms apart it('advances in step with real recorded time (1x)', () => { // 200 ms of budget over 50 ms samples = 4 steps. const r = advancePlaybackIndex(uniform, 0, 200); expect(r.index).toBe(4); expect(r.remainingMs).toBe(0); }); it('carries leftover budget when it does not cover a full step', () => { // 120 ms consumes two 50 ms steps (100 ms), leaving 20 ms. const r = advancePlaybackIndex(uniform, 0, 120); expect(r.index).toBe(2); expect(r.remainingMs).toBe(20); }); it('does not advance when the budget is below one step', () => { const r = advancePlaybackIndex(uniform, 3, 10); expect(r.index).toBe(3); expect(r.remainingMs).toBe(10); }); it('compresses a multi-second gap to a single 50 ms step so playback never stalls', () => { // Samples 0..2 uniform; sample 3 is 10 s later (disconnected). A 60 ms budget from // index 2 must still cross the gap (treated as a 50 ms step), not wait 10 s. const gapped = [mk(0), mk(50), mk(100), mk(10_100)]; const r = advancePlaybackIndex(gapped, 2, 60); expect(r.index).toBe(3); expect(r.remainingMs).toBe(0); // reached the end }); it('clamps at the final index with no remaining budget', () => { const r = advancePlaybackIndex(uniform, 8, 100_000); expect(r.index).toBe(9); expect(r.remainingMs).toBe(0); }); it('is a no-op on empty input', () => { expect(advancePlaybackIndex([], 0, 1000)).toEqual({ index: 0, remainingMs: 0 }); }); });