/** * BLE Error Handling Unit Tests * Tests for BLEError class, parseBLEError, and error utilities */ import { BLEError, BLEErrorCode, BLEErrorSeverity, BLERecoveryAction, BLE_ERROR_MESSAGES, BLE_RECOVERY_ACTIONS, parseBLEError, isBLEError, getErrorInfo, getRecoveryActionLabel, BLELogger, } from '../errors'; describe('BLEError', () => { describe('constructor', () => { it('should create error with code only', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); expect(error.code).toBe(BLEErrorCode.CONNECTION_FAILED); expect(error.message).toBe(BLE_ERROR_MESSAGES[BLEErrorCode.CONNECTION_FAILED].message); expect(error.userMessage.title).toBe('Connection Failed'); expect(error.name).toBe('BLEError'); }); it('should create error with custom message', () => { const error = new BLEError(BLEErrorCode.CONNECTION_TIMEOUT, { message: 'Custom timeout message', }); expect(error.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT); expect(error.message).toBe('Custom timeout message'); }); it('should create error with device info', () => { const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { deviceId: 'device-123', deviceName: 'WP_497_81a14c', }); expect(error.deviceId).toBe('device-123'); expect(error.deviceName).toBe('WP_497_81a14c'); }); it('should wrap original error', () => { const originalError = new Error('Native BLE error'); const error = new BLEError(BLEErrorCode.COMMAND_FAILED, { originalError, }); expect(error.originalError).toBe(originalError); expect(error.message).toBe('Native BLE error'); }); it('should set timestamp', () => { const beforeCreate = Date.now(); const error = new BLEError(BLEErrorCode.UNKNOWN_ERROR); const afterCreate = Date.now(); expect(error.timestamp).toBeGreaterThanOrEqual(beforeCreate); expect(error.timestamp).toBeLessThanOrEqual(afterCreate); }); it('should set recovery actions from mapping', () => { const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT); expect(error.recoveryActions).toEqual( BLE_RECOVERY_ACTIONS[BLEErrorCode.WIFI_PASSWORD_INCORRECT] ); expect(error.recoveryActions).toContain(BLERecoveryAction.CHECK_WIFI_PASSWORD); }); }); describe('severity', () => { it('should set CRITICAL severity for permission errors', () => { const permissionError = new BLEError(BLEErrorCode.PERMISSION_DENIED); const bluetoothError = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED); const locationError = new BLEError(BLEErrorCode.LOCATION_DISABLED); expect(permissionError.severity).toBe(BLEErrorSeverity.CRITICAL); expect(bluetoothError.severity).toBe(BLEErrorSeverity.CRITICAL); expect(locationError.severity).toBe(BLEErrorSeverity.CRITICAL); }); it('should set ERROR severity for connection failures', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); expect(error.severity).toBe(BLEErrorSeverity.ERROR); }); it('should set WARNING severity for busy/in-progress states', () => { const busyError = new BLEError(BLEErrorCode.DEVICE_BUSY); const progressError = new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS); expect(busyError.severity).toBe(BLEErrorSeverity.WARNING); expect(progressError.severity).toBe(BLEErrorSeverity.WARNING); }); it('should set INFO severity for expected states', () => { const connectedError = new BLEError(BLEErrorCode.ALREADY_CONNECTED); const cancelledError = new BLEError(BLEErrorCode.OPERATION_CANCELLED); expect(connectedError.severity).toBe(BLEErrorSeverity.INFO); expect(cancelledError.severity).toBe(BLEErrorSeverity.INFO); }); it('should allow custom severity override', () => { const error = new BLEError(BLEErrorCode.UNKNOWN_ERROR, { severity: BLEErrorSeverity.CRITICAL, }); expect(error.severity).toBe(BLEErrorSeverity.CRITICAL); }); }); describe('toLogString', () => { it('should format error with code', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); const logString = error.toLogString(); expect(logString).toContain('[BLE]'); expect(logString).toContain('[BLE_CONNECTION_FAILED]'); }); it('should include device name when available', () => { const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { deviceName: 'WP_497_81a14c', }); expect(error.toLogString()).toContain('[WP_497_81a14c]'); }); it('should include device ID when name not available', () => { const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { deviceId: 'device-123', }); expect(error.toLogString()).toContain('[device-123]'); }); }); describe('isRetryable', () => { it('should return true for retryable errors', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); expect(error.isRetryable()).toBe(true); }); it('should return false for non-retryable errors', () => { const error = new BLEError(BLEErrorCode.ALREADY_CONNECTED); expect(error.isRetryable()).toBe(false); }); }); describe('isSkippable', () => { it('should return true for skippable errors', () => { const error = new BLEError(BLEErrorCode.COMMAND_FAILED); expect(error.isSkippable()).toBe(true); }); it('should return false for non-skippable errors', () => { const error = new BLEError(BLEErrorCode.PERMISSION_DENIED); expect(error.isSkippable()).toBe(false); }); }); }); describe('parseBLEError', () => { describe('permission errors', () => { it('should parse permission denied error', () => { const error = parseBLEError(new Error('Bluetooth permission not granted')); expect(error.code).toBe(BLEErrorCode.PERMISSION_DENIED); }); it('should parse bluetooth disabled error', () => { const error = parseBLEError(new Error('Bluetooth is disabled')); expect(error.code).toBe(BLEErrorCode.BLUETOOTH_DISABLED); }); it('should parse location disabled error', () => { const error = parseBLEError(new Error('Location services required')); expect(error.code).toBe(BLEErrorCode.LOCATION_DISABLED); }); }); describe('connection errors', () => { it('should parse timeout error', () => { const error = parseBLEError(new Error('Connection timeout')); expect(error.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT); }); it('should parse connection in progress error', () => { const error = parseBLEError(new Error('Connection already in progress')); expect(error.code).toBe(BLEErrorCode.CONNECTION_IN_PROGRESS); }); it('should parse device not found error', () => { const error = parseBLEError(new Error('Device not found')); expect(error.code).toBe(BLEErrorCode.DEVICE_NOT_FOUND); }); it('should parse disconnect error', () => { const error = parseBLEError(new Error('Device disconnected unexpectedly')); expect(error.code).toBe(BLEErrorCode.DEVICE_DISCONNECTED); }); }); describe('WiFi errors', () => { it('should parse incorrect password error', () => { const error = parseBLEError(new Error('WiFi password incorrect')); expect(error.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT); }); it('should parse network not found error', () => { const error = parseBLEError(new Error('WiFi network not found')); expect(error.code).toBe(BLEErrorCode.WIFI_NETWORK_NOT_FOUND); }); it('should parse scan in progress error', () => { const error = parseBLEError(new Error('WiFi scan in progress')); expect(error.code).toBe(BLEErrorCode.WIFI_SCAN_IN_PROGRESS); }); it('should parse invalid character error', () => { const error = parseBLEError(new Error('SSID contains invalid character')); expect(error.code).toBe(BLEErrorCode.WIFI_INVALID_CREDENTIALS); }); }); describe('authentication errors', () => { it('should parse PIN unlock failed error', () => { const error = parseBLEError(new Error('Failed to unlock device with PIN')); expect(error.code).toBe(BLEErrorCode.PIN_UNLOCK_FAILED); }); }); describe('sensor errors', () => { it('should parse sensor not responding error', () => { const error = parseBLEError(new Error('Sensor not responding')); expect(error.code).toBe(BLEErrorCode.SENSOR_NOT_RESPONDING); }); it('should parse no response error', () => { const error = parseBLEError(new Error('No response from device')); expect(error.code).toBe(BLEErrorCode.SENSOR_NOT_RESPONDING); }); }); describe('service errors', () => { it('should parse service not found error', () => { const error = parseBLEError(new Error('BLE service not found')); expect(error.code).toBe(BLEErrorCode.SERVICE_NOT_FOUND); }); it('should parse characteristic not found error', () => { const error = parseBLEError(new Error('Characteristic not found')); expect(error.code).toBe(BLEErrorCode.CHARACTERISTIC_NOT_FOUND); }); }); describe('cancelled operations', () => { it('should parse cancelled error', () => { const error = parseBLEError(new Error('Operation was cancelled')); expect(error.code).toBe(BLEErrorCode.OPERATION_CANCELLED); }); it('should parse Android error code 2', () => { const error = parseBLEError(new Error('[2] Operation cancelled')); expect(error.code).toBe(BLEErrorCode.OPERATION_CANCELLED); }); }); describe('context preservation', () => { it('should preserve device context', () => { const error = parseBLEError(new Error('Connection failed'), { deviceId: 'device-123', deviceName: 'WP_497_81a14c', }); expect(error.deviceId).toBe('device-123'); expect(error.deviceName).toBe('WP_497_81a14c'); }); it('should preserve original error', () => { const originalError = new Error('Native error'); const error = parseBLEError(originalError); expect(error.originalError).toBe(originalError); }); }); describe('unknown errors', () => { it('should return UNKNOWN_ERROR for unrecognized error', () => { const error = parseBLEError(new Error('Some completely unknown error')); expect(error.code).toBe(BLEErrorCode.UNKNOWN_ERROR); }); it('should handle non-Error objects', () => { const error = parseBLEError('string error'); expect(error.code).toBe(BLEErrorCode.UNKNOWN_ERROR); }); it('should handle null/undefined', () => { const error1 = parseBLEError(null); const error2 = parseBLEError(undefined); expect(error1.code).toBe(BLEErrorCode.UNKNOWN_ERROR); expect(error2.code).toBe(BLEErrorCode.UNKNOWN_ERROR); }); }); }); describe('isBLEError', () => { it('should return true for BLEError instances', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); expect(isBLEError(error)).toBe(true); }); it('should return false for regular Error', () => { const error = new Error('Regular error'); expect(isBLEError(error)).toBe(false); }); it('should return false for non-errors', () => { expect(isBLEError(null)).toBe(false); expect(isBLEError(undefined)).toBe(false); expect(isBLEError('string')).toBe(false); expect(isBLEError({})).toBe(false); }); }); describe('getErrorInfo', () => { it('should extract info from BLEError', () => { const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT); const info = getErrorInfo(error); expect(info.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT); expect(info.title).toBe('Wrong Password'); expect(info.severity).toBe(BLEErrorSeverity.ERROR); expect(info.isRetryable).toBe(true); }); it('should parse and extract info from regular Error', () => { const error = new Error('Connection timeout'); const info = getErrorInfo(error); expect(info.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT); expect(info.title).toBe('Connection Timeout'); expect(info.isRetryable).toBe(true); }); it('should return recovery actions', () => { const error = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED); const info = getErrorInfo(error); expect(info.recoveryActions).toContain(BLERecoveryAction.ENABLE_BLUETOOTH); }); }); describe('getRecoveryActionLabel', () => { it('should return correct labels for all actions', () => { expect(getRecoveryActionLabel(BLERecoveryAction.RETRY)).toBe('Retry'); expect(getRecoveryActionLabel(BLERecoveryAction.SKIP)).toBe('Skip'); expect(getRecoveryActionLabel(BLERecoveryAction.CANCEL)).toBe('Cancel'); expect(getRecoveryActionLabel(BLERecoveryAction.ENABLE_BLUETOOTH)).toBe('Enable Bluetooth'); expect(getRecoveryActionLabel(BLERecoveryAction.ENABLE_LOCATION)).toBe('Enable Location'); expect(getRecoveryActionLabel(BLERecoveryAction.GRANT_PERMISSIONS)).toBe('Grant Permission'); expect(getRecoveryActionLabel(BLERecoveryAction.MOVE_CLOSER)).toBe('Move Closer'); expect(getRecoveryActionLabel(BLERecoveryAction.CHECK_SENSOR_POWER)).toBe('Check Sensor'); expect(getRecoveryActionLabel(BLERecoveryAction.CHECK_WIFI_PASSWORD)).toBe('Check Password'); expect(getRecoveryActionLabel(BLERecoveryAction.TRY_DIFFERENT_NETWORK)).toBe('Try Different Network'); expect(getRecoveryActionLabel(BLERecoveryAction.CONTACT_SUPPORT)).toBe('Contact Support'); }); it('should return OK for unknown action', () => { expect(getRecoveryActionLabel('unknown' as BLERecoveryAction)).toBe('OK'); }); }); describe('BLE_ERROR_MESSAGES', () => { it('should have messages for all error codes', () => { const errorCodes = Object.values(BLEErrorCode); errorCodes.forEach(code => { expect(BLE_ERROR_MESSAGES[code]).toBeDefined(); expect(BLE_ERROR_MESSAGES[code].title).toBeDefined(); expect(BLE_ERROR_MESSAGES[code].message).toBeDefined(); }); }); }); describe('BLE_RECOVERY_ACTIONS', () => { it('should have recovery actions for all error codes', () => { const errorCodes = Object.values(BLEErrorCode); errorCodes.forEach(code => { expect(BLE_RECOVERY_ACTIONS[code]).toBeDefined(); expect(Array.isArray(BLE_RECOVERY_ACTIONS[code])).toBe(true); }); }); }); describe('BLELogger', () => { let consoleLogSpy: jest.SpyInstance; let consoleWarnSpy: jest.SpyInstance; let consoleErrorSpy: jest.SpyInstance; beforeEach(() => { BLELogger.enable(); consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); }); afterEach(() => { consoleLogSpy.mockRestore(); consoleWarnSpy.mockRestore(); consoleErrorSpy.mockRestore(); }); describe('enable/disable', () => { it('should log when enabled', () => { BLELogger.enable(); BLELogger.log('Test message'); expect(consoleLogSpy).toHaveBeenCalled(); }); it('should not log when disabled', () => { BLELogger.disable(); BLELogger.log('Test message'); expect(consoleLogSpy).not.toHaveBeenCalled(); }); }); describe('log', () => { it('should log with prefix', () => { BLELogger.log('Test message'); expect(consoleLogSpy).toHaveBeenCalledWith('[BLE] Test message', ''); }); it('should log with data', () => { BLELogger.log('Test message', { key: 'value' }); expect(consoleLogSpy).toHaveBeenCalledWith('[BLE] Test message', { key: 'value' }); }); }); describe('warn', () => { it('should warn with prefix', () => { BLELogger.warn('Warning message'); expect(consoleWarnSpy).toHaveBeenCalledWith('[BLE] Warning message', ''); }); }); describe('error', () => { it('should error with prefix', () => { BLELogger.error('Error message'); expect(consoleErrorSpy).toHaveBeenCalledWith('[BLE] Error message', ''); }); it('should format BLEError using toLogString', () => { const bleError = new BLEError(BLEErrorCode.CONNECTION_FAILED, { deviceName: 'WP_497_81a14c', }); BLELogger.error('Connection error', bleError); // BLELogger adds [BLE] prefix, and toLogString also starts with [BLE] // So the output is [BLE] [BLE] [...] - this is the expected behavior expect(consoleErrorSpy).toHaveBeenCalledWith(`[BLE] ${bleError.toLogString()}`); }); }); describe('logBatchProgress', () => { it('should log batch progress', () => { BLELogger.logBatchProgress(1, 5, 'WP_497', 'connecting...'); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('[1/5]') ); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('WP_497') ); }); it('should show success indicator', () => { BLELogger.logBatchProgress(1, 5, 'WP_497', 'done', true); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('✓') ); }); it('should show failure indicator', () => { BLELogger.logBatchProgress(1, 5, 'WP_497', 'failed', false); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('✗') ); }); it('should include duration when provided', () => { BLELogger.logBatchProgress(1, 5, 'WP_497', 'done', true, 2500); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('2.5s') ); }); }); describe('logBatchSummary', () => { it('should log batch summary', () => { BLELogger.logBatchSummary(10, 8, 2, 15000); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('8/10 succeeded') ); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('2 failed') ); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('15.0s') ); }); }); });