import { describe, it, expect } from 'vitest' import { relativeTime, truncateMiddle, debounce } from './utils' describe('relativeTime', () => { it('returns "never" for null/undefined/empty', () => { expect(relativeTime(null)).toBe('never') expect(relativeTime(undefined)).toBe('never') expect(relativeTime('')).toBe('never') }) it('returns "just now" for future timestamps', () => { const future = new Date(Date.now() + 10_000).toISOString() expect(relativeTime(future)).toBe('just now') }) it('formats seconds/minutes/hours/days', () => { const now = Date.now() expect(relativeTime(new Date(now - 5_000).toISOString())).toBe('5s ago') expect(relativeTime(new Date(now - 120_000).toISOString())).toBe('2m ago') expect(relativeTime(new Date(now - 3_600_000).toISOString())).toBe('1h ago') expect(relativeTime(new Date(now - 86_400_000 * 2).toISOString())).toBe('2d ago') }) }) describe('truncateMiddle', () => { it('returns the string unchanged when at or under maxLen', () => { expect(truncateMiddle('short', 36)).toBe('short') expect(truncateMiddle('exactly36chars_exactly36chars_xxxxx', 36)).toBe( 'exactly36chars_exactly36chars_xxxxx' ) }) it('truncates in the middle, preserving both ends', () => { const out = truncateMiddle('investigation:nomos/dragonfly-memlock-overrun', 20) expect(out).toContain('…') // keep = floor((20 - 1) / 2) = 9 chars from each end expect(out.startsWith('investiga')).toBe(true) expect(out.endsWith('-overrun')).toBe(true) expect(out.length).toBeLessThanOrEqual(20) }) }) describe('debounce', () => { it('collapses rapid calls into one trailing invocation', async () => { let calls = 0 const fn = debounce(() => calls++, 20) fn() fn() fn() expect(calls).toBe(0) await new Promise((r) => setTimeout(r, 40)) expect(calls).toBe(1) }) })