import { describe, expect, test, beforeAll, afterAll, spyOn } from 'bun:test'; import { leftpad, rightpad, formatPercent, formatTime, formatDate, formatDateTime, formatCurrency, formatNumber, formatPercentage, formatDiff, daysAgo, formatByThousands, } from './formatters'; // Mock browser globals const originalNavigator = globalThis.navigator; beforeAll(() => { // @ts-expect-error - mocking navigator for tests globalThis.navigator = { language: 'en-US', languages: ['en-US'], }; }); afterAll(() => { globalThis.navigator = originalNavigator; }); describe('leftpad', () => { test('pads number with leading zeros', () => { expect(leftpad(5, 3)).toBe('005'); expect(leftpad(42, 4)).toBe('0042'); expect(leftpad(123, 3)).toBe('123'); }); test('pads string with leading zeros', () => { expect(leftpad('5', 3)).toBe('005'); expect(leftpad('ab', 4)).toBe('00ab'); }); test('handles value already at or exceeding size', () => { expect(leftpad(12345, 3)).toBe('12345'); expect(leftpad('abc', 3)).toBe('abc'); }); test('handles default value of 0', () => { expect(leftpad(undefined, 3)).toBe('000'); }); }); describe('rightpad', () => { test('pads number with trailing zeros', () => { expect(rightpad(5, 3)).toBe('500'); expect(rightpad(42, 4)).toBe('4200'); }); test('pads string with trailing zeros', () => { expect(rightpad('5', 3)).toBe('500'); expect(rightpad('ab', 4)).toBe('ab00'); }); test('handles value already at or exceeding size', () => { expect(rightpad(12345, 3)).toBe('12345'); }); test('handles default value of 0', () => { expect(rightpad(undefined, 3)).toBe('000'); }); }); describe('formatPercent', () => { test('formats number with 2 decimal places', () => { expect(formatPercent(5.5)).toBe('05.50'); expect(formatPercent(12.34)).toBe('12.34'); expect(formatPercent(99.9)).toBe('99.90'); }); test('formats string numbers', () => { expect(formatPercent('5.5')).toBe('05.50'); expect(formatPercent('12.34')).toBe('12.34'); }); test('handles whole numbers', () => { expect(formatPercent(5)).toBe('05.00'); expect(formatPercent('12')).toBe('12.00'); }); test('handles zero', () => { expect(formatPercent(0)).toBe('00.00'); expect(formatPercent('0')).toBe('00.00'); }); test('returns empty string for null/undefined', () => { expect(formatPercent(null as unknown as number)).toBe(''); expect(formatPercent(undefined as unknown as number)).toBe(''); }); }); describe('formatPercentage', () => { test('formats number as percentage string', () => { expect(formatPercentage(5.5)).toBe('5.50%'); expect(formatPercentage(12.345)).toBe('12.35%'); expect(formatPercentage(0.1)).toBe('0.10%'); }); test('formats string numbers', () => { expect(formatPercentage('5.5')).toBe('5.50%'); expect(formatPercentage('99')).toBe('99.00%'); }); test('returns default for falsy values', () => { expect(formatPercentage(0)).toBe('00.00%'); expect(formatPercentage('')).toBe('00.00%'); }); }); describe('formatDiff', () => { test('formats positive difference with plus sign', () => { expect(formatDiff(5)).toBe('+5'); expect(formatDiff(10.7)).toBe('+11'); }); test('formats negative difference without extra sign', () => { expect(formatDiff(-5)).toBe('-5'); expect(formatDiff(-10.7)).toBe('-11'); }); test('adds percent suffix when isPercent is true', () => { expect(formatDiff(5, true)).toBe('+5%'); expect(formatDiff(-3, true)).toBe('-3%'); }); test('shows decimals when decimals is true', () => { expect(formatDiff(5.55, false, true)).toBe('+5.55'); expect(formatDiff(-3.14, true, true)).toBe('-3.14%'); }); test('returns empty string for falsy values', () => { expect(formatDiff(0)).toBe(''); expect(formatDiff('')).toBe(''); }); }); describe('daysAgo', () => { const oneDay = 1000 * 60 * 60 * 24; test('calculates days between two timestamps', () => { const now = Date.now(); expect(daysAgo(now, now - oneDay)).toBe(1); expect(daysAgo(now, now - oneDay * 5)).toBe(5); }); test('order of arguments does not matter (absolute difference)', () => { const now = Date.now(); const past = now - oneDay * 3; expect(daysAgo(now, past)).toBe(3); expect(daysAgo(past, now)).toBe(3); }); test('same timestamp returns 0', () => { const now = Date.now(); expect(daysAgo(now, now)).toBe(0); }); test('partial days are rounded up', () => { const now = Date.now(); const halfDayAgo = now - oneDay * 0.5; expect(daysAgo(now, halfDayAgo)).toBe(1); }); }); describe('formatByThousands', () => { test('formats millions with M suffix', () => { const result = formatByThousands(1500000); expect(result).toContain('M'); expect(result).toContain('1'); }); test('formats thousands with K suffix', () => { const result = formatByThousands(5000); expect(result).toContain('K'); expect(result).toContain('5'); }); test('formats small numbers without suffix', () => { const result = formatByThousands(500); expect(result).not.toContain('K'); expect(result).not.toContain('M'); }); test('handles zero', () => { const result = formatByThousands(0); expect(result).toContain('0'); }); test('handles exactly 1000', () => { const result = formatByThousands(1000); expect(result).toContain('K'); }); test('handles exactly 1000000', () => { const result = formatByThousands(1000000); expect(result).toContain('M'); }); }); describe('formatTime', () => { test('formats timestamp to time string', () => { const mockToLocaleTimeString = spyOn(Date.prototype, 'toLocaleTimeString').mockReturnValue('10:30:00 AM'); const result = formatTime(1706180400000); expect(result).toBe('10:30:00 AM'); mockToLocaleTimeString.mockRestore(); }); test('returns empty string for falsy values', () => { expect(formatTime(0)).toBe(''); expect(formatTime('')).toBe(''); }); }); describe('formatDate', () => { test('formats timestamp to date string', () => { const mockToLocaleDateString = spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('1/25/2024'); const result = formatDate(1706180400000); expect(result).toBe('1/25/2024'); mockToLocaleDateString.mockRestore(); }); test('returns empty string for falsy values', () => { expect(formatDate(0)).toBe(''); expect(formatDate('')).toBe(''); }); }); describe('formatDateTime', () => { test('formats timestamp to date and time string', () => { const mockToLocaleDateString = spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('1/25/2024'); const mockToLocaleTimeString = spyOn(Date.prototype, 'toLocaleTimeString').mockReturnValue('10:30:00 AM'); const result = formatDateTime(1706180400000); expect(result).toBe('1/25/2024 10:30:00 AM'); mockToLocaleDateString.mockRestore(); mockToLocaleTimeString.mockRestore(); }); test('returns empty string for falsy values', () => { expect(formatDateTime(0)).toBe(''); expect(formatDateTime('')).toBe(''); }); }); describe('formatCurrency', () => { const OriginalNumberFormat = Intl.NumberFormat; afterAll(() => { globalThis.Intl.NumberFormat = OriginalNumberFormat; }); test('formats number as currency', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '$1,234.56'; }; const result = formatCurrency(1234.56, 'USD'); expect(result).toBe('$1,234.56'); }); test('handles string input', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '$99.00'; }; const result = formatCurrency('99', 'USD'); expect(result).toBe('$99.00'); }); test('handles zero and NaN', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '$0.00'; }; expect(formatCurrency(0, 'USD')).toBe('$0.00'); expect(formatCurrency(NaN, 'USD')).toBe('$0.00'); }); test('respects decimals parameter', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '€10.000'; }; const result = formatCurrency(10, 'EUR', 3); expect(result).toBe('€10.000'); }); }); describe('formatNumber', () => { const OriginalNumberFormat = Intl.NumberFormat; afterAll(() => { globalThis.Intl.NumberFormat = OriginalNumberFormat; }); test('formats number with locale formatting', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '1,234,567'; }; const result = formatNumber(1234567); expect(result).toBe('1,234,567'); }); test('handles string input', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '999'; }; const result = formatNumber('999'); expect(result).toBe('999'); }); test('handles zero and NaN', () => { // @ts-expect-error - mocking Intl.NumberFormat globalThis.Intl.NumberFormat = class { format = () => '0'; }; expect(formatNumber(0)).toBe('0'); expect(formatNumber(NaN)).toBe('0'); }); });