first
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user