229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
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());
|
|
});
|
|
});
|
|
|