import { parseDuration, formatDuration, isValidDurationString } from '../dist/utils/time.js'; describe('Time utilities', () => { describe('parseDuration', () => { describe('numeric inputs', () => { test('should parse positive numbers as milliseconds', () => { expect(parseDuration(1000)).toBe(1000); expect(parseDuration(5000)).toBe(5000); expect(parseDuration(0)).toBe(0); }); test('should throw error for negative numbers', () => { expect(() => parseDuration(-1000)).toThrow('Duration cannot be negative'); expect(() => parseDuration(-1)).toThrow('Duration cannot be negative'); }); test('should throw error for numbers exceeding MAX_SAFE_INTEGER', () => { expect(() => parseDuration(Number.MAX_SAFE_INTEGER + 1)).toThrow('Duration too large'); }); }); describe('string inputs', () => { describe('valid duration strings', () => { test('should parse seconds correctly', () => { expect(parseDuration('5s')).toBe(5000); expect(parseDuration('30s')).toBe(30000); expect(parseDuration('1s')).toBe(1000); }); test('should parse minutes correctly', () => { expect(parseDuration('5m')).toBe(300000); expect(parseDuration('1m')).toBe(60000); expect(parseDuration('10m')).toBe(600000); }); test('should parse hours correctly', () => { expect(parseDuration('1h')).toBe(3600000); expect(parseDuration('2h')).toBe(7200000); }); test('should parse days correctly', () => { expect(parseDuration('1d')).toBe(86400000); expect(parseDuration('2d')).toBe(172800000); }); test('should parse decimal values', () => { expect(parseDuration('1.5s')).toBe(1500); expect(parseDuration('2.5m')).toBe(150000); }); test('should be case-insensitive for units', () => { expect(parseDuration('5S')).toBe(5000); expect(parseDuration('5M')).toBe(300000); }); }); describe('numeric strings', () => { test('should parse numeric strings as milliseconds', () => { expect(parseDuration('1000')).toBe(1000); expect(parseDuration('5000')).toBe(5000); }); test('should throw error for negative numeric strings', () => { expect(() => parseDuration('-1000')).toThrow('Duration cannot be negative'); }); }); describe('invalid inputs', () => { test('should throw error for empty string', () => { expect(() => parseDuration('')).toThrow('Duration cannot be empty'); expect(() => parseDuration(' ')).toThrow('Duration cannot be empty'); }); test('should throw error for invalid format', () => { expect(() => parseDuration('abc')).toThrow('Invalid duration format'); expect(() => parseDuration('5x')).toThrow('Invalid duration format'); expect(() => parseDuration('s5')).toThrow('Invalid duration format'); }); test('should throw error for negative duration values', () => { expect(() => parseDuration('-5s')).toThrow('Invalid duration format'); expect(() => parseDuration('-10m')).toThrow('Invalid duration format'); }); describe('edge case validation', () => { test('should handle duration strings that exceed MAX_SAFE_INTEGER when multiplied', () => { // Test a very large number that when multiplied by day multiplier exceeds MAX_SAFE_INTEGER const largeValue = Math.floor(Number.MAX_SAFE_INTEGER / (24 * 60 * 60 * 1000)) + 1; expect(() => parseDuration(`${largeValue}d`)).toThrow('Duration too large'); }); test('should handle duration strings with whitespace around units', () => { expect(parseDuration('5 s')).toBe(5000); expect(parseDuration('10 m')).toBe(600000); expect(parseDuration('2 h')).toBe(7200000); expect(parseDuration('1 d')).toBe(86400000); }); test('should throw error for duration strings with negative values in unit format', () => { expect(() => parseDuration('-1.5s')).toThrow('Invalid duration format'); expect(() => parseDuration('-10m')).toThrow('Invalid duration format'); expect(() => parseDuration('-2.5h')).toThrow('Invalid duration format'); }); test('should handle various decimal formats', () => { expect(parseDuration('0.5s')).toBe(500); expect(parseDuration('0.1m')).toBe(6000); expect(parseDuration('1.0h')).toBe(3600000); expect(parseDuration('0.25d')).toBe(21600000); }); }); }); }); describe('invalid types', () => { test('should throw error for non-string/number inputs', () => { expect(() => parseDuration(null)).toThrow('Duration must be a string or number'); expect(() => parseDuration(undefined)).toThrow('Duration must be a string or number'); expect(() => parseDuration({})).toThrow('Duration must be a string or number'); expect(() => parseDuration([])).toThrow('Duration must be a string or number'); }); }); }); describe('formatDuration', () => { test('should format milliseconds to ms for values < 1000', () => { expect(formatDuration(0)).toBe('0ms'); expect(formatDuration(999)).toBe('999ms'); expect(formatDuration(500)).toBe('500ms'); }); test('should format to seconds', () => { expect(formatDuration(1000)).toBe('1s'); expect(formatDuration(5000)).toBe('5s'); expect(formatDuration(59000)).toBe('59s'); }); test('should format to minutes', () => { expect(formatDuration(60000)).toBe('1m'); expect(formatDuration(300000)).toBe('5m'); expect(formatDuration(3540000)).toBe('59m'); }); test('should format to hours', () => { expect(formatDuration(3600000)).toBe('1h'); expect(formatDuration(7200000)).toBe('2h'); expect(formatDuration(86340000)).toBe('23h'); }); test('should format to days', () => { expect(formatDuration(86400000)).toBe('1d'); expect(formatDuration(172800000)).toBe('2d'); expect(formatDuration(604800000)).toBe('7d'); }); test('should throw error for negative values', () => { expect(() => formatDuration(-1000)).toThrow('Duration cannot be negative'); }); test('should format edge case durations correctly', () => { expect(formatDuration(1)).toBe('1ms'); expect(formatDuration(999)).toBe('999ms'); expect(formatDuration(1001)).toBe('1s'); expect(formatDuration(59999)).toBe('59s'); expect(formatDuration(60001)).toBe('1m'); expect(formatDuration(3599999)).toBe('59m'); expect(formatDuration(3600001)).toBe('1h'); expect(formatDuration(86399999)).toBe('23h'); expect(formatDuration(86400001)).toBe('1d'); }); test('should format very large durations to days', () => { expect(formatDuration(Number.MAX_SAFE_INTEGER)).toBe(`${Math.floor(Number.MAX_SAFE_INTEGER / (24 * 60 * 60 * 1000))}d`); }); }); describe('isValidDurationString', () => { test('should return true for valid duration formats', () => { expect(isValidDurationString('5s')).toBe(true); expect(isValidDurationString('10m')).toBe(true); expect(isValidDurationString('2h')).toBe(true); expect(isValidDurationString('1d')).toBe(true); expect(isValidDurationString('1000')).toBe(true); }); test('should return false for invalid formats', () => { expect(isValidDurationString('abc')).toBe(false); expect(isValidDurationString('5x')).toBe(false); expect(isValidDurationString('')).toBe(false); expect(isValidDurationString('-5s')).toBe(false); }); test('should return false for various invalid input types', () => { expect(isValidDurationString(null)).toBe(false); expect(isValidDurationString(undefined)).toBe(false); expect(isValidDurationString({})).toBe(false); expect(isValidDurationString([])).toBe(false); expect(isValidDurationString(true)).toBe(false); expect(isValidDurationString(false)).toBe(false); }); test('should return false for durations that would overflow', () => { const largeValue = Math.floor(Number.MAX_SAFE_INTEGER / (24 * 60 * 60 * 1000)) + 1; expect(isValidDurationString(`${largeValue}d`)).toBe(false); // Test overflow in unit-based durations (not pure numeric strings which have different validation) expect(isValidDurationString(`${Number.MAX_SAFE_INTEGER}d`)).toBe(false); }); test('should return false for edge case invalid formats', () => { expect(isValidDurationString('5.5.5s')).toBe(false); expect(isValidDurationString('5ss')).toBe(false); expect(isValidDurationString('s')).toBe(false); expect(isValidDurationString('5')).toBe(true); // Valid numeric string expect(isValidDurationString('5.m')).toBe(false); expect(isValidDurationString('.5m')).toBe(false); }); test('should handle decimal validation correctly', () => { expect(isValidDurationString('1.5s')).toBe(true); expect(isValidDurationString('0.5m')).toBe(true); expect(isValidDurationString('2.0h')).toBe(true); expect(isValidDurationString('1.25d')).toBe(true); }); test('should validate case insensitive units', () => { expect(isValidDurationString('5S')).toBe(true); expect(isValidDurationString('5M')).toBe(true); expect(isValidDurationString('5H')).toBe(true); expect(isValidDurationString('5D')).toBe(true); }); }); describe('integration tests', () => { test('parseDuration and formatDuration should work together', () => { const testValues = ['5s', '10m', '2h', '1d']; testValues.forEach(value => { const parsed = parseDuration(value); const formatted = formatDuration(parsed); expect(formatted).toBe(value); }); }); test('should handle round-trip conversion with decimal values', () => { // Note: formatDuration floors values, so we test known good round-trips const testCases = [ { input: '1.5s', parsed: 1500, formatted: '1s' }, { input: '2.5m', parsed: 150000, formatted: '2m' }, { input: '1.5h', parsed: 5400000, formatted: '1h' } ]; testCases.forEach(({ input, parsed, formatted }) => { expect(parseDuration(input)).toBe(parsed); expect(formatDuration(parsed)).toBe(formatted); }); }); test('should validate and format edge boundary values', () => { // Test values at unit boundaries expect(formatDuration(999)).toBe('999ms'); expect(formatDuration(1000)).toBe('1s'); expect(formatDuration(59999)).toBe('59s'); expect(formatDuration(60000)).toBe('1m'); expect(formatDuration(3599999)).toBe('59m'); expect(formatDuration(3600000)).toBe('1h'); expect(formatDuration(86399999)).toBe('23h'); expect(formatDuration(86400000)).toBe('1d'); }); }); });