import { describe, expect, it, vi } from 'vitest'; import { createSessionActivityTracker } from './sessionActivity.js'; function harness() { const target = new EventTarget(); const calls = []; let authenticated = true; const tracker = createSessionActivityTracker({ target, throttleMs: 1000, isAuthenticated: () => authenticated, onActivity: () => { calls.push('request'); return Promise.resolve(); } }); return { target, calls, tracker, setAuthenticated: (value) => { authenticated = value; } }; } describe('session activity tracker', () => { it('sends one immediate request and one trailing request for a burst', async () => { vi.useFakeTimers(); const { target, calls, tracker } = harness(); tracker.start(); target.dispatchEvent(new Event('pointerdown')); await vi.runOnlyPendingTimersAsync(); expect(calls).toHaveLength(1); target.dispatchEvent(new Event('pointermove')); target.dispatchEvent(new Event('keydown')); await vi.advanceTimersByTimeAsync(999); expect(calls).toHaveLength(1); await vi.advanceTimersByTimeAsync(1); expect(calls).toHaveLength(2); await vi.advanceTimersByTimeAsync(60_000); expect(calls).toHaveLength(2); tracker.stop(); vi.useRealTimers(); }); it('stays idle, avoids overlap, then stops on the current generation 401', async () => { vi.useFakeTimers(); const target = new EventTarget(); let resolveFirst; let rejectSecond; let authenticated = true; const calls = []; const onActivity = vi.fn(() => new Promise((resolve, reject) => { calls.push({ resolve, reject }); if (calls.length === 1) resolveFirst = resolve; else rejectSecond = reject; })); const tracker = createSessionActivityTracker({ target, throttleMs: 1000, isAuthenticated: () => authenticated, onActivity }); tracker.start(); await vi.advanceTimersByTimeAsync(3_600_000); expect(onActivity).not.toHaveBeenCalled(); target.dispatchEvent(new Event('click')); target.dispatchEvent(new Event('pointermove')); await vi.runOnlyPendingTimersAsync(); expect(onActivity).toHaveBeenCalledTimes(1); target.dispatchEvent(new Event('keydown')); await vi.advanceTimersByTimeAsync(1000); expect(onActivity).toHaveBeenCalledTimes(1); resolveFirst(); await Promise.resolve(); await vi.runOnlyPendingTimersAsync(); expect(onActivity).toHaveBeenCalledTimes(2); rejectSecond({ status: 401 }); await Promise.resolve(); await vi.runOnlyPendingTimersAsync(); expect(tracker.isActive()).toBe(false); target.dispatchEvent(new Event('click')); await vi.advanceTimersByTimeAsync(10_000); expect(onActivity).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); it('removes listeners and makes no calls while unauthenticated', async () => { vi.useFakeTimers(); const { target, calls, tracker, setAuthenticated } = harness(); setAuthenticated(false); tracker.start(); target.dispatchEvent(new Event('wheel')); await vi.runOnlyPendingTimersAsync(); expect(calls).toHaveLength(0); tracker.stop(); setAuthenticated(true); target.dispatchEvent(new Event('wheel')); await vi.runOnlyPendingTimersAsync(); expect(calls).toHaveLength(0); vi.useRealTimers(); }); });