import { describe, expect, it } from 'vitest'; import { formatDate, formatDateRange, formatRelative } from '@/lib/utils/format-date'; describe('formatDate', () => { const REF = '2026-05-12T14:30:45.000Z'; it('returns fallback for null/undefined/invalid', () => { expect(formatDate(null)).toBe('-'); expect(formatDate(undefined)).toBe('-'); expect(formatDate('not a date')).toBe('-'); expect(formatDate(NaN)).toBe('-'); expect(formatDate(null, 'date.medium', { fallback: 'N/A' })).toBe('N/A'); }); it('formats date.iso correctly in UTC', () => { expect(formatDate(REF, 'date.iso', { tz: 'UTC' })).toBe('2026-05-12'); }); it('formats date.iso correctly in a different timezone', () => { // 14:30 UTC = 00:30 next day in Sydney (UTC+10) expect(formatDate(REF, 'date.iso', { tz: 'Australia/Sydney' })).toBe('2026-05-13'); }); it('formats datetime.iso as full ISO string', () => { expect(formatDate(REF, 'datetime.iso')).toBe('2026-05-12T14:30:45.000Z'); }); it('formats date.medium in en-GB', () => { // Intl output may vary slightly across Node versions; assert it // contains the expected day + month + year and uses no "/". const out = formatDate(REF, 'date.medium', { tz: 'UTC' }); expect(out).toMatch(/12\s/); // day expect(out).toContain('2026'); expect(out.toLowerCase()).toContain('may'); }); it('respects timezone - datetime.short in different zones', () => { const ny = formatDate(REF, 'datetime.short', { tz: 'America/New_York' }); const utc = formatDate(REF, 'datetime.short', { tz: 'UTC' }); expect(ny).not.toBe(utc); expect(utc).toContain('14:30'); }); it('formats time-only correctly', () => { expect(formatDate(REF, 'time', { tz: 'UTC' })).toBe('14:30'); }); it('accepts Date instances + epoch ms', () => { const d = new Date(REF); expect(formatDate(d, 'date.iso', { tz: 'UTC' })).toBe('2026-05-12'); expect(formatDate(d.getTime(), 'date.iso', { tz: 'UTC' })).toBe('2026-05-12'); }); }); describe('formatDateRange', () => { it('handles missing start/end', () => { expect(formatDateRange(null, null)).toBe('-'); expect(formatDateRange('2026-05-12', null)).toMatch(/→$/); expect(formatDateRange(null, '2026-05-12')).toMatch(/^→/); }); it('collapses year when start + end are in same year', () => { const out = formatDateRange('2026-05-12', '2026-08-14', { tz: 'UTC' }); // Start should NOT include year; end should. expect(out).toContain('→'); expect(out).toContain('2026'); // Crude check: there's only one "2026" in the output. expect((out.match(/2026/g) ?? []).length).toBe(1); }); it('keeps year on both when years differ', () => { const out = formatDateRange('2025-12-20', '2026-01-05', { tz: 'UTC' }); expect((out.match(/202\d/g) ?? []).length).toBe(2); }); }); describe('formatRelative', () => { const NOW = new Date('2026-05-12T12:00:00.000Z'); it('returns "just now" for tiny deltas', () => { expect(formatRelative(new Date(NOW.getTime() - 5_000), { now: NOW })).toBe('just now'); }); it('uses minutes for past deltas under an hour', () => { const ten = new Date(NOW.getTime() - 10 * 60_000); const out = formatRelative(ten, { now: NOW }); expect(out).toMatch(/minute/); }); it('uses hours for past deltas in the same day', () => { const three = new Date(NOW.getTime() - 3 * 3600_000); const out = formatRelative(three, { now: NOW }); expect(out).toMatch(/hour/); }); it('uses days for past deltas across days', () => { const yesterday = new Date(NOW.getTime() - 86_400_000); const out = formatRelative(yesterday, { now: NOW }); expect(out).toMatch(/day|yesterday/i); }); it('uses future tense for future dates', () => { const tomorrow = new Date(NOW.getTime() + 86_400_000); const out = formatRelative(tomorrow, { now: NOW }); expect(out).toMatch(/day|tomorrow/i); }); it('returns fallback for invalid input', () => { expect(formatRelative(null)).toBe('-'); }); });