Files
psysonic/src/utils/playback/playbackScheduleDelay.test.ts
T
cucadmuh cd47a4b0fa fix(player): clamp custom delay input and align preview with armed timer (#967)
* fix(player): clamp custom delay input and align preview with armed timer

Reject absurd custom minute values that overflow setTimeout, share one
delay helper between the modal preview and schedule actions, and refresh
countdown ticks when a new deadline is armed.

* docs: note PR #967 in changelog and settings credits
2026-06-03 23:37:17 +03:00

66 lines
2.4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
MAX_PLAYBACK_SCHEDULE_DELAY_MS,
MAX_PLAYBACK_SCHEDULE_MINUTES,
isValidPlaybackSchedulePreviewTimestamp,
parsePlaybackDelayCustomMinutes,
scheduleDeadlineMs,
scheduleDelayMsFromSeconds,
scheduleSecondsFromCustomMinutes,
} from './playbackScheduleDelay';
describe('parsePlaybackDelayCustomMinutes', () => {
it('accepts fractional minutes', () => {
expect(parsePlaybackDelayCustomMinutes('0.1')).toBe(0.1);
expect(parsePlaybackDelayCustomMinutes('0,01')).toBe(0.01);
});
it('rejects empty, zero, and non-numeric input', () => {
expect(parsePlaybackDelayCustomMinutes('')).toBeNull();
expect(parsePlaybackDelayCustomMinutes('0')).toBeNull();
expect(parsePlaybackDelayCustomMinutes('abc')).toBeNull();
});
it('rejects values above the setTimeout-safe minute cap', () => {
expect(parsePlaybackDelayCustomMinutes('99999999999')).toBeNull();
expect(parsePlaybackDelayCustomMinutes(String(MAX_PLAYBACK_SCHEDULE_MINUTES + 1))).toBeNull();
});
});
describe('scheduleSecondsFromCustomMinutes', () => {
it('rounds fractional minutes to whole seconds', () => {
expect(scheduleSecondsFromCustomMinutes(0.1)).toBe(6);
expect(scheduleSecondsFromCustomMinutes(0.01)).toBe(1);
});
it('never returns zero', () => {
expect(scheduleSecondsFromCustomMinutes(0.001)).toBe(1);
});
});
describe('scheduleDelayMsFromSeconds', () => {
it('enforces the minimum delay and caps at the browser limit', () => {
expect(scheduleDelayMsFromSeconds(0)).toBe(500);
expect(scheduleDelayMsFromSeconds(6)).toBe(6000);
expect(scheduleDelayMsFromSeconds(MAX_PLAYBACK_SCHEDULE_DELAY_MS / 1000)).toBe(
MAX_PLAYBACK_SCHEDULE_DELAY_MS,
);
});
});
describe('scheduleDeadlineMs', () => {
it('uses the same clamped delay as the armed timer', () => {
const startedAt = 1_700_000_000_000;
expect(scheduleDeadlineMs(startedAt, 6)).toBe(startedAt + 6000);
expect(scheduleDeadlineMs(startedAt, scheduleSecondsFromCustomMinutes(0.01))).toBe(startedAt + 1000);
});
});
describe('isValidPlaybackSchedulePreviewTimestamp', () => {
it('accepts ordinary future timestamps and rejects invalid ones', () => {
expect(isValidPlaybackSchedulePreviewTimestamp(Date.now() + 60_000)).toBe(true);
expect(isValidPlaybackSchedulePreviewTimestamp(Number.NaN)).toBe(false);
expect(isValidPlaybackSchedulePreviewTimestamp(Number.POSITIVE_INFINITY)).toBe(false);
});
});