import { describe, it, expect, vi } from 'vitest'; import { formatMinutesAfter, adjustForTimezone, timeToMinutes, formatMinutes, toLocalDatetimeInputValue, formatElapsedTime, formatDateTime, toUTCFromLocalInput, getDurationMinutes, clampToDay, parseTime, getDaysAgo, getToday, getLocalOffsetFromReference, parseBackendDateTime } from '../../../src/utils/time'; describe('formatMinutesAfter', () => { it('formats minutes correctly with default reference (04:00)', () => { expect(formatMinutesAfter(15)).toBe('04:15'); expect(formatMinutesAfter(-15)).toBe('03:45'); }); it('formats minutes correctly with custom reference', () => { expect(formatMinutesAfter(15, '07:00')).toBe('07:15'); expect(formatMinutesAfter(-15, '07:00')).toBe('06:45'); }); it('handles wrap around midnight', () => { expect(formatMinutesAfter(1425, '04:00')).toBe('03:45'); expect(formatMinutesAfter(-240, '04:00')).toBe('00:00'); expect(formatMinutesAfter(-255, '04:00')).toBe('23:45'); }); it('returns N/A for null minutes', () => { expect(formatMinutesAfter(null)).toBe('N/A'); }); it('handles large number of minutes', () => { expect(formatMinutesAfter(1440, '04:00')).toBe('04:00'); }); }); describe('timeToMinutes', () => { it('converts HH:MM to total minutes', () => { expect(timeToMinutes('00:00')).toBe(0); expect(timeToMinutes('04:00')).toBe(240); expect(timeToMinutes('23:59')).toBe(1439); }); }); describe('formatMinutes', () => { it('formats minutes into h m string', () => { expect(formatMinutes(90)).toBe('1h 30m'); expect(formatMinutes(45)).toBe('45m'); expect(formatMinutes(0)).toBe('0m'); }); }); describe('adjustForTimezone', () => { it('adjusts minutes by timezone offset', () => { const offset = new Date().getTimezoneOffset(); expect(adjustForTimezone(0)).toBe(-offset); }); }); describe('toLocalDatetimeInputValue', () => { it('formats date to YYYY-MM-DDTHH:mm', () => { const date = new Date(2024, 5, 9, 14, 30); const result = toLocalDatetimeInputValue(date); expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); }); }); describe('formatElapsedTime', () => { it('formats elapsed time correctly', () => { const now = new Date(); const then = new Date(now.getTime() - 90 * 60000); // 90 mins ago const iso = then.toISOString(); expect(formatElapsedTime(iso)).toBe('1h 30m'); const then2 = new Date(now.getTime() - 45 * 60000); // 45 mins ago const iso2 = then2.toISOString(); expect(formatElapsedTime(iso2)).toBe('45m'); }); it('handles ISO strings with spaces instead of T', () => { const now = new Date(); const then = new Date(now.getTime() - 10 * 60000); const iso = then.toISOString().replace('T', ' '); expect(formatElapsedTime(iso)).toBe('10m'); }); }); describe('formatDateTime', () => { it('formats date object to YYYY-MM-DD HH:mm', () => { const date = new Date(2024, 5, 9, 14, 30); const result = formatDateTime(date); expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); }); it('formats ISO string to YYYY-MM-DD HH:mm', () => { const iso = '2024-06-09T14:30:00Z'; const result = formatDateTime(iso); expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); }); it('handles strings without Z by appending it', () => { const iso = '2024-06-09T14:30:00'; const result = formatDateTime(iso); expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); }); }); describe('toUTCFromLocalInput', () => { it('converts local datetime input to ISO UTC string', () => { const input = '2024-06-09T14:30'; const result = toUTCFromLocalInput(input); expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/); }); it('returns empty string for invalid input', () => { expect(toUTCFromLocalInput('invalid')).toBe(''); }); }); describe('getDurationMinutes', () => { it('calculates duration correctly between two dates', () => { const start = '2024-06-09T10:00:00Z'; const end = '2024-06-09T11:30:00Z'; expect(getDurationMinutes(start, end)).toBe(90); }); it('calculates duration correctly between date objects', () => { const start = new Date('2024-06-09T10:00:00Z'); const end = new Date('2024-06-09T11:30:00Z'); expect(getDurationMinutes(start, end)).toBe(90); }); }); describe('clampToDay', () => { it('clamps to start of day', () => { const date = new Date(2024, 5, 9, 14, 30); const clamped = clampToDay(date, 'start'); expect(clamped.getHours()).toBe(0); expect(clamped.getMinutes()).toBe(0); }); it('clamps to end of day', () => { const date = new Date(2024, 5, 9, 14, 30); const clamped = clampToDay(date, 'end'); expect(clamped.getHours()).toBe(23); expect(clamped.getMinutes()).toBe(59); }); }); describe('parseTime', () => { it('parses HH:MM string to Date object on today', () => { const timeStr = '14:30'; const parsed = parseTime(timeStr); const now = new Date(); expect(parsed.getHours()).toBe(14); expect(parsed.getMinutes()).toBe(30); expect(parsed.getDate()).toBe(now.getDate()); }); it('parses HH:MM string to Date object on a specific date', () => { const baseDate = new Date(2024, 5, 9); const parsed = parseTime('14:30', baseDate); expect(parsed.getFullYear()).toBe(2024); expect(parsed.getMonth()).toBe(5); // June is 5 in JS Date expect(parsed.getDate()).toBe(9); expect(parsed.getHours()).toBe(14); expect(parsed.getMinutes()).toBe(30); }); }); describe('getDaysAgo', () => { it('returns ISO date string for N days ago', () => { const today = getToday(); expect(getDaysAgo(0)).toBe(today); // Note: this depends on the current date, so we can only check format and relative-ish expect(getDaysAgo(1)).toMatch(/^\d{4}-\d{2}-\d{2}$/); }); }); describe('getToday', () => { it('returns today in YYYY-MM-DD format', () => { const today = getToday(); const expected = new Date().toISOString().split('T')[0]; expect(today).toBe(expected); }); }); describe('getLocalOffsetFromReference', () => { it('calculates local offset from reference', () => { const utcMinutes = 1440 / 2; // Midday UTC (720 mins) const reference = '04:00'; // 240 mins // adjustForTimezone(720) = 720 - offset // timeToMinutes('04:00') = 240 // result = (720 - offset) - 240 = 480 - offset const offset = new Date().getTimezoneOffset(); expect(getLocalOffsetFromReference(utcMinutes, reference)).toBe(480 - offset); }); }); describe('parseBackendDateTime', () => { it('parses ISO string correctly', () => { const iso = '2024-06-09T14:30:00Z'; const parsed = parseBackendDateTime(iso); expect(parsed.toISOString()).toBe(new Date(iso).toISOString()); }); it('appends Z to strings without timezone', () => { const iso = '2024-06-09T14:30:00'; const parsed = parseBackendDateTime(iso); expect(parsed.toISOString()).toBe(new Date(iso + 'Z').toISOString()); }); it('handles already Z-terminated strings', () => { const iso = '2024-06-09T14:30:00Z'; const parsed = parseBackendDateTime(iso); expect(parsed.toISOString()).toBe(new Date(iso).toISOString()); }); });