mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createSessionTimer, type SessionTimerState } from '../session-timer';
|
|
import type { SessionLimits } from '../limits';
|
|
|
|
const START = new Date('2026-10-28T09:00:00Z').getTime();
|
|
const SECOND = 1000;
|
|
const MINUTE = 60 * SECOND;
|
|
|
|
const idleOnly = (minutes: number, warnSeconds: number): SessionLimits => ({ idle: { minutes, warnSeconds }, max: null });
|
|
|
|
function setup(limits: SessionLimits) {
|
|
const states: SessionTimerState[] = [];
|
|
const onExpire = vi.fn();
|
|
const timer = createSessionTimer({ limits, lastActivity: Date.now(), onChange: (s) => states.push(s), onExpire });
|
|
return { timer, states, onExpire };
|
|
}
|
|
|
|
describe('session timer', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(START);
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('warns warnSeconds before the idle time runs out, then signs out at zero', () => {
|
|
const { timer, onExpire } = setup(idleOnly(1, 30));
|
|
expect(timer.state()).toMatchObject({ phase: 'active', kind: 'idle', deadline: START + MINUTE });
|
|
|
|
vi.advanceTimersByTime(30 * SECOND - 1);
|
|
expect(timer.state().phase).toBe('active');
|
|
vi.advanceTimersByTime(1);
|
|
expect(timer.state()).toMatchObject({ phase: 'warning', kind: 'idle', deadline: START + MINUTE });
|
|
expect(onExpire).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(30 * SECOND - 1);
|
|
expect(onExpire).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(onExpire).toHaveBeenCalledWith('idle');
|
|
expect(timer.state().phase).toBe('expired');
|
|
timer.dispose();
|
|
});
|
|
|
|
it('starts the idle time again on activity before the warning', () => {
|
|
const { timer, onExpire } = setup(idleOnly(1, 30));
|
|
vi.advanceTimersByTime(20 * SECOND);
|
|
timer.activity();
|
|
// Without the reset the warning would have started at 30 s.
|
|
vi.advanceTimersByTime(29 * SECOND);
|
|
expect(timer.state().phase).toBe('active');
|
|
vi.advanceTimersByTime(1 * SECOND);
|
|
expect(timer.state().phase).toBe('warning');
|
|
expect(timer.state().deadline).toBe(START + 20 * SECOND + MINUTE);
|
|
vi.advanceTimersByTime(30 * SECOND);
|
|
expect(onExpire).toHaveBeenCalledTimes(1);
|
|
timer.dispose();
|
|
});
|
|
|
|
it('keeps the warning up on local activity until the user chooses to stay', () => {
|
|
const { timer, onExpire } = setup(idleOnly(1, 30));
|
|
vi.advanceTimersByTime(40 * SECOND);
|
|
expect(timer.state().phase).toBe('warning');
|
|
|
|
timer.activity();
|
|
expect(timer.state().phase).toBe('warning');
|
|
|
|
timer.stay();
|
|
expect(timer.state()).toMatchObject({ phase: 'active', deadline: START + 40 * SECOND + MINUTE });
|
|
vi.advanceTimersByTime(MINUTE - 1);
|
|
expect(onExpire).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(onExpire).toHaveBeenCalledWith('idle');
|
|
timer.dispose();
|
|
});
|
|
|
|
it('ends the warning when another tab reports activity', () => {
|
|
const { timer, onExpire } = setup(idleOnly(1, 30));
|
|
vi.advanceTimersByTime(45 * SECOND);
|
|
expect(timer.state().phase).toBe('warning');
|
|
|
|
timer.remoteActivity(Date.now());
|
|
expect(timer.state().phase).toBe('active');
|
|
vi.advanceTimersByTime(MINUTE - 1);
|
|
expect(onExpire).not.toHaveBeenCalled();
|
|
timer.dispose();
|
|
});
|
|
|
|
it('signs out after a sleep even when the wake-up arrives as activity first', () => {
|
|
const { timer, onExpire } = setup(idleOnly(15, 60));
|
|
// The machine slept: the clock jumped, no timer ran.
|
|
vi.setSystemTime(START + 2 * 60 * MINUTE);
|
|
timer.activity();
|
|
expect(onExpire).toHaveBeenCalledWith('idle');
|
|
expect(timer.state().phase).toBe('expired');
|
|
timer.remoteActivity(Date.now());
|
|
timer.stay();
|
|
expect(onExpire).toHaveBeenCalledTimes(1);
|
|
timer.dispose();
|
|
});
|
|
|
|
it('runs no clock without limits', () => {
|
|
const { timer, onExpire } = setup({ idle: null, max: null });
|
|
expect(timer.state()).toEqual({ phase: 'active', kind: null, deadline: null });
|
|
vi.advanceTimersByTime(365 * 24 * 60 * MINUTE);
|
|
expect(onExpire).not.toHaveBeenCalled();
|
|
timer.dispose();
|
|
});
|
|
|
|
it('follows new limits', () => {
|
|
const { timer, onExpire } = setup({ idle: null, max: null });
|
|
vi.advanceTimersByTime(10 * MINUTE);
|
|
timer.setLimits(idleOnly(5, 60));
|
|
// The last activity was ten minutes ago: already past five minutes.
|
|
expect(onExpire).toHaveBeenCalledWith('idle');
|
|
timer.dispose();
|
|
});
|
|
|
|
it('warns before the maximum session length and does not move it on activity', () => {
|
|
const at = START + 2 * 60 * MINUTE;
|
|
const { timer, onExpire } = setup({ idle: { minutes: 30, warnSeconds: 60 }, max: { at, hours: 2, warnSeconds: 60 } });
|
|
|
|
for (let i = 0; i < 119; i++) {
|
|
vi.advanceTimersByTime(MINUTE);
|
|
timer.activity();
|
|
}
|
|
expect(timer.state()).toMatchObject({ phase: 'warning', kind: 'max', deadline: at });
|
|
timer.stay();
|
|
expect(timer.state()).toMatchObject({ phase: 'warning', kind: 'max' });
|
|
vi.advanceTimersByTime(MINUTE);
|
|
expect(onExpire).toHaveBeenCalledWith('max');
|
|
timer.dispose();
|
|
});
|
|
|
|
it('counts a maximum far away without running its timer early', () => {
|
|
const at = START + 365 * 24 * 60 * MINUTE;
|
|
const { timer, onExpire } = setup({ idle: null, max: { at, hours: 365 * 24, warnSeconds: 60 } });
|
|
vi.advanceTimersByTime(30 * 24 * 60 * MINUTE);
|
|
expect(onExpire).not.toHaveBeenCalled();
|
|
expect(timer.state().phase).toBe('active');
|
|
timer.dispose();
|
|
});
|
|
});
|