import { BLEError, BLEErrorCode, BLEErrorSeverity, BLERecoveryAction, BLE_ERROR_MESSAGES, BLE_RECOVERY_ACTIONS, parseBLEError, isBLEError, getErrorInfo, getRecoveryActionLabel, BLELogger, } from '@/services/ble/errors'; describe('BLEError', () => { describe('constructor', () => { it('creates error with correct code and default message', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); expect(error.code).toBe(BLEErrorCode.CONNECTION_FAILED); expect(error.name).toBe('BLEError'); expect(error.userMessage.title).toBe('Connection Failed'); expect(error.userMessage.message).toContain('Could not connect'); expect(error.timestamp).toBeGreaterThan(0); }); it('creates error with custom message', () => { const error = new BLEError(BLEErrorCode.CONNECTION_TIMEOUT, { message: 'Custom timeout message', }); expect(error.message).toBe('Custom timeout message'); expect(error.userMessage.title).toBe('Connection Timeout'); }); it('creates error with device context', () => { const error = new BLEError(BLEErrorCode.DEVICE_NOT_FOUND, { deviceId: 'device-123', deviceName: 'WP_497_81a14c', }); expect(error.deviceId).toBe('device-123'); expect(error.deviceName).toBe('WP_497_81a14c'); }); it('wraps original error', () => { const originalError = new Error('Native BLE error'); const error = new BLEError(BLEErrorCode.COMMAND_FAILED, { originalError, }); expect(error.originalError).toBe(originalError); }); it('sets correct severity for critical errors', () => { const permissionError = new BLEError(BLEErrorCode.PERMISSION_DENIED); const bluetoothError = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED); expect(permissionError.severity).toBe(BLEErrorSeverity.CRITICAL); expect(bluetoothError.severity).toBe(BLEErrorSeverity.CRITICAL); }); it('sets correct severity for warning errors', () => { const busyError = new BLEError(BLEErrorCode.DEVICE_BUSY); const scanError = new BLEError(BLEErrorCode.WIFI_SCAN_IN_PROGRESS); expect(busyError.severity).toBe(BLEErrorSeverity.WARNING); expect(scanError.severity).toBe(BLEErrorSeverity.WARNING); }); it('sets recovery actions from mapping', () => { const connectionError = new BLEError(BLEErrorCode.CONNECTION_FAILED); const passwordError = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT); expect(connectionError.recoveryActions).toContain(BLERecoveryAction.RETRY); expect(connectionError.recoveryActions).toContain(BLERecoveryAction.MOVE_CLOSER); expect(passwordError.recoveryActions).toContain(BLERecoveryAction.CHECK_WIFI_PASSWORD); }); }); describe('toLogString', () => { it('formats error for logging with device name', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED, { deviceName: 'WP_497_81a14c', message: 'Connection failed', }); const logString = error.toLogString(); expect(logString).toContain('[BLE]'); expect(logString).toContain('[BLE_CONNECTION_FAILED]'); expect(logString).toContain('[WP_497_81a14c]'); expect(logString).toContain('Connection failed'); }); it('uses deviceId when deviceName is not available', () => { const error = new BLEError(BLEErrorCode.COMMAND_TIMEOUT, { deviceId: 'device-123', }); const logString = error.toLogString(); expect(logString).toContain('[device-123]'); }); }); describe('isRetryable', () => { it('returns true for retryable errors', () => { const connectionError = new BLEError(BLEErrorCode.CONNECTION_FAILED); const timeoutError = new BLEError(BLEErrorCode.COMMAND_TIMEOUT); expect(connectionError.isRetryable()).toBe(true); expect(timeoutError.isRetryable()).toBe(true); }); it('returns false for non-retryable errors', () => { const cancelledError = new BLEError(BLEErrorCode.OPERATION_CANCELLED); const alreadyConnectedError = new BLEError(BLEErrorCode.ALREADY_CONNECTED); expect(cancelledError.isRetryable()).toBe(false); expect(alreadyConnectedError.isRetryable()).toBe(false); }); }); describe('isSkippable', () => { it('returns true for skippable errors in batch operations', () => { const commandError = new BLEError(BLEErrorCode.COMMAND_FAILED); const rebootError = new BLEError(BLEErrorCode.SENSOR_REBOOT_FAILED); expect(commandError.isSkippable()).toBe(true); expect(rebootError.isSkippable()).toBe(true); }); it('returns false for non-skippable errors', () => { const permissionError = new BLEError(BLEErrorCode.PERMISSION_DENIED); const bluetoothError = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED); expect(permissionError.isSkippable()).toBe(false); expect(bluetoothError.isSkippable()).toBe(false); }); }); }); describe('parseBLEError', () => { it('parses permission denied errors', () => { const error = new Error('Bluetooth permissions not granted'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.PERMISSION_DENIED); }); it('parses bluetooth disabled errors', () => { const error = new Error('Bluetooth is disabled'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.BLUETOOTH_DISABLED); }); it('parses timeout errors', () => { const error = new Error('Connection timeout'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT); }); it('parses connection in progress errors', () => { const error = new Error('Connection already in progress'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.CONNECTION_IN_PROGRESS); }); it('parses WiFi password incorrect errors', () => { const error = new Error('WiFi password is incorrect'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT); }); it('parses WiFi network not found errors', () => { const error = new Error('WiFi network not found'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.WIFI_NETWORK_NOT_FOUND); }); it('parses device disconnected errors', () => { const error = new Error('Device disconnected unexpectedly'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.DEVICE_DISCONNECTED); }); it('parses cancelled operation errors', () => { const error = new Error('Operation was cancelled'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.OPERATION_CANCELLED); }); it('includes device context when provided', () => { const error = new Error('Connection timeout'); const bleError = parseBLEError(error, { deviceId: 'device-123', deviceName: 'WP_497_81a14c', }); expect(bleError.deviceId).toBe('device-123'); expect(bleError.deviceName).toBe('WP_497_81a14c'); }); it('defaults to unknown error for unrecognized messages', () => { const error = new Error('Some random error message'); const bleError = parseBLEError(error); expect(bleError.code).toBe(BLEErrorCode.UNKNOWN_ERROR); }); it('handles non-Error objects', () => { const bleError = parseBLEError('string error'); expect(bleError).toBeInstanceOf(BLEError); expect(bleError.code).toBe(BLEErrorCode.UNKNOWN_ERROR); }); }); describe('isBLEError', () => { it('returns true for BLEError instances', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED); expect(isBLEError(error)).toBe(true); }); it('returns false for regular Error instances', () => { const error = new Error('Regular error'); expect(isBLEError(error)).toBe(false); }); it('returns false for non-error objects', () => { expect(isBLEError('string')).toBe(false); expect(isBLEError(null)).toBe(false); expect(isBLEError(undefined)).toBe(false); expect(isBLEError({})).toBe(false); }); }); describe('getErrorInfo', () => { it('extracts 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.message).toContain('incorrect'); expect(info.severity).toBe(BLEErrorSeverity.ERROR); expect(info.recoveryActions).toContain(BLERecoveryAction.CHECK_WIFI_PASSWORD); expect(info.isRetryable).toBe(true); }); it('parses and extracts info from regular Error', () => { const error = new Error('Bluetooth is disabled'); const info = getErrorInfo(error); expect(info.code).toBe(BLEErrorCode.BLUETOOTH_DISABLED); expect(info.title).toBe('Bluetooth Disabled'); expect(info.severity).toBe(BLEErrorSeverity.CRITICAL); expect(info.recoveryActions).toContain(BLERecoveryAction.ENABLE_BLUETOOTH); }); }); describe('getRecoveryActionLabel', () => { it('returns 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.GRANT_PERMISSIONS)).toBe('Grant Permission'); expect(getRecoveryActionLabel(BLERecoveryAction.MOVE_CLOSER)).toBe('Move Closer'); expect(getRecoveryActionLabel(BLERecoveryAction.CHECK_WIFI_PASSWORD)).toBe('Check Password'); expect(getRecoveryActionLabel(BLERecoveryAction.CONTACT_SUPPORT)).toBe('Contact Support'); }); }); describe('BLE_ERROR_MESSAGES', () => { it('has messages for all error codes', () => { const errorCodes = Object.values(BLEErrorCode); for (const code of errorCodes) { expect(BLE_ERROR_MESSAGES[code]).toBeDefined(); expect(BLE_ERROR_MESSAGES[code].title).toBeTruthy(); expect(BLE_ERROR_MESSAGES[code].message).toBeTruthy(); } }); }); describe('BLE_RECOVERY_ACTIONS', () => { it('has recovery actions for all error codes', () => { const errorCodes = Object.values(BLEErrorCode); for (const code of errorCodes) { expect(BLE_RECOVERY_ACTIONS[code]).toBeDefined(); expect(Array.isArray(BLE_RECOVERY_ACTIONS[code])).toBe(true); } }); }); describe('BLELogger', () => { // Spy on console methods const consoleSpy = { log: jest.spyOn(console, 'log').mockImplementation(), warn: jest.spyOn(console, 'warn').mockImplementation(), error: jest.spyOn(console, 'error').mockImplementation(), }; beforeEach(() => { jest.clearAllMocks(); BLELogger.enable(); }); afterAll(() => { consoleSpy.log.mockRestore(); consoleSpy.warn.mockRestore(); consoleSpy.error.mockRestore(); }); it('logs messages with BLE prefix', () => { BLELogger.log('Test message'); expect(consoleSpy.log).toHaveBeenCalledWith('[BLE] Test message', ''); }); it('logs warnings with BLE prefix', () => { BLELogger.warn('Warning message'); expect(consoleSpy.warn).toHaveBeenCalledWith('[BLE] Warning message', ''); }); it('logs errors with BLE prefix', () => { BLELogger.error('Error message'); expect(consoleSpy.error).toHaveBeenCalledWith('[BLE] Error message', ''); }); it('logs BLEError with formatted string', () => { const error = new BLEError(BLEErrorCode.CONNECTION_FAILED, { deviceName: 'WP_497', }); BLELogger.error('Failed', error); expect(consoleSpy.error).toHaveBeenCalled(); const loggedMessage = consoleSpy.error.mock.calls[0][0]; expect(loggedMessage).toContain('[BLE]'); expect(loggedMessage).toContain('[BLE_CONNECTION_FAILED]'); expect(loggedMessage).toContain('[WP_497]'); }); it('does not log when disabled', () => { BLELogger.disable(); BLELogger.log('Should not appear'); BLELogger.warn('Should not appear'); BLELogger.error('Should not appear'); expect(consoleSpy.log).not.toHaveBeenCalled(); expect(consoleSpy.warn).not.toHaveBeenCalled(); expect(consoleSpy.error).not.toHaveBeenCalled(); }); describe('logBatchProgress', () => { it('logs batch progress with success indicator', () => { BLELogger.logBatchProgress(1, 5, 'WP_497', 'connected', true); expect(consoleSpy.log).toHaveBeenCalled(); const loggedMessage = consoleSpy.log.mock.calls[0][0]; expect(loggedMessage).toContain('[1/5]'); expect(loggedMessage).toContain('WP_497'); expect(loggedMessage).toContain('✓'); expect(loggedMessage).toContain('connected'); }); it('logs batch progress with failure indicator', () => { BLELogger.logBatchProgress(2, 5, 'WP_498', 'connection failed', false); expect(consoleSpy.log).toHaveBeenCalled(); const loggedMessage = consoleSpy.log.mock.calls[0][0]; expect(loggedMessage).toContain('[2/5]'); expect(loggedMessage).toContain('✗'); }); it('logs batch progress with duration', () => { BLELogger.logBatchProgress(3, 5, 'WP_499', 'SUCCESS', true, 8500); expect(consoleSpy.log).toHaveBeenCalled(); const loggedMessage = consoleSpy.log.mock.calls[0][0]; expect(loggedMessage).toContain('(8.5s)'); }); }); describe('logBatchSummary', () => { it('logs batch summary with counts', () => { BLELogger.logBatchSummary(5, 4, 1, 45000); expect(consoleSpy.log).toHaveBeenCalled(); const loggedMessage = consoleSpy.log.mock.calls[0][0]; expect(loggedMessage).toContain('4/5 succeeded'); expect(loggedMessage).toContain('1 failed'); expect(loggedMessage).toContain('45.0s'); }); }); });