import { normalizeError, extractErrorMessage, extractErrorStack, retryWithBackoff, withTimeout, isBrowser, isNode, hasFeature, safeAsync } from '../../utils/ErrorHandling'; describe('ErrorHandling Utils', () => { describe('normalizeError', () => { it('should return Error instance as-is', () => { const originalError = new Error('test error'); const result = normalizeError(originalError); expect(result).toBe(originalError); }); it('should convert string to Error', () => { const result = normalizeError('string error'); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('string error'); }); it('should handle unknown error types', () => { const result = normalizeError(null); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error'); }); it('should use default message when provided', () => { const result = normalizeError(null, 'Custom default'); expect(result.message).toBe('Custom default'); }); }); describe('extractErrorMessage', () => { it('should extract message from Error instance', () => { const error = new Error('test message'); expect(extractErrorMessage(error)).toBe('test message'); }); it('should return string as-is', () => { expect(extractErrorMessage('string message')).toBe('string message'); }); it('should extract message property from objects', () => { const errorObj = { message: 'object message', code: 500 }; expect(extractErrorMessage(errorObj)).toBe('object message'); }); it('should return default for unknown types', () => { expect(extractErrorMessage(null)).toBe('Unknown error'); expect(extractErrorMessage(undefined)).toBe('Unknown error'); expect(extractErrorMessage(123)).toBe('Unknown error'); }); }); describe('extractErrorStack', () => { it('should extract stack from Error instance', () => { const error = new Error('test'); const stack = extractErrorStack(error); expect(typeof stack).toBe('string'); expect(stack).toContain('Error: test'); }); it('should return undefined for non-Error types', () => { expect(extractErrorStack('string')).toBeUndefined(); expect(extractErrorStack(null)).toBeUndefined(); }); }); describe('retryWithBackoff', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('should succeed on first attempt', async () => { const operation = jest.fn().mockResolvedValue('success'); const result = await retryWithBackoff(operation, { maxAttempts: 3, delay: 1000, backoff: 'linear' }); expect(result).toBe('success'); expect(operation).toHaveBeenCalledTimes(1); expect(operation).toHaveBeenCalledWith(1); }); it('should retry on failure with linear backoff', async () => { const operation = jest.fn() .mockRejectedValueOnce(new Error('fail 1')) .mockRejectedValueOnce(new Error('fail 2')) .mockResolvedValueOnce('success'); const promise = retryWithBackoff(operation, { maxAttempts: 3, delay: 1000, backoff: 'linear' }); // Fast-forward timers jest.runAllTimers(); const result = await promise; expect(result).toBe('success'); expect(operation).toHaveBeenCalledTimes(3); }); it('should fail after max attempts', async () => { const operation = jest.fn().mockRejectedValue(new Error('always fail')); const promise = retryWithBackoff(operation, { maxAttempts: 2, delay: 1000, backoff: 'linear' }); jest.runAllTimers(); await expect(promise).rejects.toThrow('always fail'); expect(operation).toHaveBeenCalledTimes(2); }); it('should use exponential backoff', async () => { const operation = jest.fn() .mockRejectedValueOnce(new Error('fail')) .mockResolvedValueOnce('success'); const promise = retryWithBackoff(operation, { maxAttempts: 3, delay: 100, backoff: 'exponential' }); jest.runAllTimers(); const result = await promise; expect(result).toBe('success'); }); }); describe('withTimeout', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('should resolve before timeout', async () => { const promise = Promise.resolve('success'); const result = await withTimeout(promise, 1000); expect(result).toBe('success'); }); it('should reject on timeout', async () => { const promise = new Promise(() => {}); // Never resolves const timeoutPromise = withTimeout(promise, 1000); jest.advanceTimersByTime(1000); await expect(timeoutPromise).rejects.toThrow('Operation timed out after 1000ms'); }); }); describe('safeAsync', () => { it('should return operation result on success', async () => { const operation = jest.fn().mockResolvedValue('success'); const result = await safeAsync(operation, 'fallback'); expect(result).toBe('success'); expect(operation).toHaveBeenCalledTimes(1); }); it('should return fallback on error', async () => { const operation = jest.fn().mockRejectedValue(new Error('fail')); const onError = jest.fn(); const result = await safeAsync(operation, 'fallback', onError); expect(result).toBe('fallback'); expect(onError).toHaveBeenCalledWith(expect.any(Error)); }); }); describe('environment detection', () => { describe('isBrowser', () => { it('should return true in jsdom environment', () => { expect(isBrowser()).toBe(true); }); }); describe('isNode', () => { it('should return true when process is available', () => { // In Jest, process is available expect(isNode()).toBe(true); }); }); describe('hasFeature', () => { it('should detect fetch availability', () => { expect(hasFeature('fetch')).toBe(true); }); it('should detect WebSocket availability', () => { expect(hasFeature('WebSocket')).toBe(true); }); it('should detect localStorage availability', () => { expect(hasFeature('localStorage')).toBe(true); }); it('should return false for unknown features', () => { // @ts-expect-error - Testing invalid feature expect(hasFeature('invalidFeature')).toBe(false); }); }); }); });