/** * Tests for services/formatting.js * * Tests the formatting utility functions used for displaying * dates, durations, modes, and numbers throughout the UI. */ // Stub ResearchStates (used by formatStatus) window.RESEARCH_STATUS = { IN_PROGRESS: 'in_progress', COMPLETED: 'completed', FAILED: 'failed', SUSPENDED: 'suspended', CANCELLED: 'cancelled', QUEUED: 'queued', PENDING: 'pending', ERROR: 'error', }; window.RESEARCH_TERMINAL_STATES = new Set([ 'completed', 'suspended', 'failed', 'error', 'cancelled', ]); // Load constants first (provides ResearchStates), then formatting import '@js/config/constants.js'; import '@js/services/formatting.js'; const fmt = window.formatting; describe('formatting', () => { describe('formatMode', () => { it('maps quick to Quick Summary', () => { expect(fmt.formatMode('quick')).toBe('Quick Summary'); }); it('maps detailed to Detailed Report', () => { expect(fmt.formatMode('detailed')).toBe('Detailed Report'); }); it('capitalizes unknown modes', () => { expect(fmt.formatMode('custom')).toBe('Custom'); }); }); describe('formatDate', () => { it('returns Unknown for falsy input', () => { expect(fmt.formatDate(null)).toBe('Unknown'); expect(fmt.formatDate('')).toBe('Unknown'); expect(fmt.formatDate(undefined)).toBe('Unknown'); }); it('formats a valid ISO date', () => { const result = fmt.formatDate('2025-01-15T10:30:00Z'); // Should contain month, day, and year expect(result).toMatch(/Jan/); expect(result).toMatch(/15/); expect(result).toMatch(/2025/); }); it('appends duration in minutes and seconds', () => { const result = fmt.formatDate('2025-01-15T10:30:00Z', 125); expect(result).toContain('2m 5s'); }); it('appends duration in seconds only when < 60s', () => { const result = fmt.formatDate('2025-01-15T10:30:00Z', 45); expect(result).toContain('45s'); expect(result).not.toContain('m'); }); it('does not append duration when null', () => { const result = fmt.formatDate('2025-01-15T10:30:00Z', null); expect(result).not.toContain('s)'); }); }); describe('formatDuration', () => { it('returns Unknown for falsy input', () => { expect(fmt.formatDuration(null)).toBe('Unknown'); expect(fmt.formatDuration(undefined)).toBe('Unknown'); expect(fmt.formatDuration(0)).toBe('Unknown'); }); it('returns Unknown for NaN', () => { expect(fmt.formatDuration(NaN)).toBe('Unknown'); }); it('formats seconds only when < 60', () => { expect(fmt.formatDuration(45)).toBe('45s'); }); it('formats minutes and seconds', () => { expect(fmt.formatDuration(125)).toBe('2m 5s'); }); it('handles exact minutes', () => { expect(fmt.formatDuration(120)).toBe('2m 0s'); }); it('floors fractional seconds', () => { expect(fmt.formatDuration(45.7)).toBe('45s'); }); }); describe('capitalizeFirstLetter', () => { it('capitalizes lowercase string', () => { expect(fmt.capitalizeFirstLetter('hello')).toBe('Hello'); }); it('returns empty string for falsy input', () => { expect(fmt.capitalizeFirstLetter('')).toBe(''); expect(fmt.capitalizeFirstLetter(null)).toBe(''); expect(fmt.capitalizeFirstLetter(undefined)).toBe(''); }); it('preserves already capitalized strings', () => { expect(fmt.capitalizeFirstLetter('Hello')).toBe('Hello'); }); it('handles single character', () => { expect(fmt.capitalizeFirstLetter('a')).toBe('A'); }); }); describe('formatNumber', () => { it('returns "0" for null/undefined', () => { expect(fmt.formatNumber(null)).toBe('0'); expect(fmt.formatNumber(undefined)).toBe('0'); }); it('returns number as-is when < 1000', () => { expect(fmt.formatNumber(999)).toBe('999'); }); it('adds comma separators for thousands', () => { expect(fmt.formatNumber(1000)).toBe('1,000'); expect(fmt.formatNumber(1234567)).toBe('1,234,567'); }); it('handles zero', () => { expect(fmt.formatNumber(0)).toBe('0'); }); }); describe('formatStatus', () => { it('delegates to ResearchStates.formatStatus', () => { expect(fmt.formatStatus('completed')).toBe('Completed'); expect(fmt.formatStatus('in_progress')).toBe('In Progress'); }); }); describe('formatCurrency', () => { it('uses 6 decimal places for tiny amounts (< $0.01)', () => { expect(fmt.formatCurrency(0.000123)).toBe('$0.000123'); expect(fmt.formatCurrency(0.005)).toBe('$0.005000'); }); it('uses 4 decimal places for amounts < $1 (boundary: 0.01)', () => { expect(fmt.formatCurrency(0.01)).toBe('$0.0100'); expect(fmt.formatCurrency(0.5)).toBe('$0.5000'); expect(fmt.formatCurrency(0.9999)).toBe('$0.9999'); }); it('uses 2 decimal places for amounts >= $1', () => { expect(fmt.formatCurrency(1)).toBe('$1.00'); expect(fmt.formatCurrency(12.345)).toBe('$12.35'); expect(fmt.formatCurrency(1000)).toBe('$1000.00'); }); it('zero uses the tiny-amount branch', () => { expect(fmt.formatCurrency(0)).toBe('$0.000000'); }); }); describe('generateChartColors', () => { it('returns empty arrays for count=0', () => { const r = fmt.generateChartColors(0); expect(r.background).toEqual([]); expect(r.border).toEqual([]); }); it('returns `count` colors', () => { const r = fmt.generateChartColors(3); expect(r.background).toHaveLength(3); expect(r.border).toHaveLength(3); }); it('cycles through the palette when count exceeds base length', () => { const r = fmt.generateChartColors(15); expect(r.background).toHaveLength(15); // First and 11th entries should be identical (palette of 10) expect(r.background[0]).toBe(r.background[10]); }); it('border colors are opaque versions of background colors', () => { const r = fmt.generateChartColors(2); for (let i = 0; i < 2; i++) { // background should use alpha 0.8, border alpha 1 expect(r.background[i]).toContain('0.8'); expect(r.border[i]).not.toContain('0.8'); expect(r.border[i]).toContain(', 1)'); } }); }); });