/** * The two formatters the jobs tab and the chat lean on. * * Pure functions with an injectable `now`, so none of this needs fake timers — * which is also why they take one: a formatter that reads the clock itself is a * formatter you can only test by lying to the runtime. */ import { describe, expect, it } from 'vitest'; import { formatRelativeTime, formatWhen } from '../src/lib/utils'; const NOW = new Date('2026-08-21T12:00:00Z'); const ago = (ms: number) => new Date(NOW.getTime() - ms); const SECOND = 1000; const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; describe('formatRelativeTime', () => { it('collapses the first minute to "now"', () => { expect(formatRelativeTime(ago(0), NOW)).toBe('now'); expect(formatRelativeTime(ago(59 * SECOND), NOW)).toBe('now'); }); it('reads a timestamp slightly in the future as "now" rather than negative', () => { // Clock skew between the server row and the browser, and optimistic rows. expect(formatRelativeTime(new Date(NOW.getTime() + 5 * SECOND), NOW)).toBe('now'); }); it('counts minutes, then hours', () => { expect(formatRelativeTime(ago(MINUTE), NOW)).toBe('1 min'); expect(formatRelativeTime(ago(59 * MINUTE), NOW)).toBe('59 min'); expect(formatRelativeTime(ago(HOUR), NOW)).toBe('1 h'); expect(formatRelativeTime(ago(23 * HOUR), NOW)).toBe('23 h'); }); it('switches to a weekday inside the week and a date beyond it', () => { // Rounding down matters at the boundary: 24h ago is a day, not "24 h". expect(formatRelativeTime(ago(DAY), NOW)).not.toMatch(/h$/); expect(formatRelativeTime(ago(DAY), NOW)).toMatch(/^\w+/); const old = formatRelativeTime(ago(30 * DAY), NOW); expect(old).toMatch(/\d/); }); }); describe('formatWhen', () => { it('names today, tomorrow and yesterday', () => { const noon = new Date(2026, 7, 21, 12, 0); expect(formatWhen(new Date(2026, 7, 21, 14, 0), noon)).toMatch(/^Today /); expect(formatWhen(new Date(2026, 7, 22, 9, 0), noon)).toMatch(/^Tomorrow /); expect(formatWhen(new Date(2026, 7, 20, 9, 0), noon)).toMatch(/^Yesterday /); }); it('compares calendar days, not elapsed hours', () => { // 23:00 tonight and 01:00 tomorrow are two hours apart. An elapsed-time // comparison calls both "Today"; only one of them is. const lateTonight = new Date(2026, 7, 21, 23, 0); const earlyTomorrow = new Date(2026, 7, 22, 1, 0); expect(formatWhen(lateTonight, lateTonight)).toMatch(/^Today /); expect(formatWhen(earlyTomorrow, lateTonight)).toMatch(/^Tomorrow /); }); it('survives a daylight-saving boundary', () => { // Europe/Madrid springs forward on 29 March 2026: that day is 23 hours long, // so a naive divide-by-86400000 would call the next morning "Today". const beforeChange = new Date(2026, 2, 28, 12, 0); const afterChange = new Date(2026, 2, 29, 12, 0); expect(formatWhen(afterChange, beforeChange)).toMatch(/^Tomorrow /); }); it('uses a weekday inside the week and a date beyond it', () => { const monday = new Date(2026, 7, 17, 12, 0); expect(formatWhen(new Date(2026, 7, 20, 14, 0), monday)).not.toMatch(/^(Today|Tomorrow)/); expect(formatWhen(new Date(2026, 8, 30, 14, 0), monday)).toMatch(/\d/); }); });