// Mock the native module import const mockNativeInstance = { setRestoreStateCallback: jest.fn(), startScan: jest.fn(), stopScan: jest.fn(), isScanning: jest.fn(), connect: jest.fn(), disconnect: jest.fn(), isConnected: jest.fn(), requestMTU: jest.fn(), requestConnectionPriority: jest.fn(), readRSSI: jest.fn(), discoverServices: jest.fn(), discoverServicesWithCharacteristics: jest.fn(), getServices: jest.fn(), getCharacteristics: jest.fn(), readCharacteristic: jest.fn(), writeCharacteristic: jest.fn(), subscribeToCharacteristic: jest.fn(), unsubscribeFromCharacteristic: jest.fn(), isSubscribedToCharacteristic: jest.fn(), getConnectedDevices: jest.fn(), requestBluetoothEnable: jest.fn(), state: jest.fn(), subscribeToStateChange: jest.fn(), unsubscribeFromStateChange: jest.fn(), openSettings: jest.fn(), restoreStateIdentifier: null, }; jest.mock('../specs/NativeBleNitro', () => ({ __esModule: true, default: mockNativeInstance, BLEState: { Unknown: 0, Resetting: 1, Unsupported: 2, Unauthorized: 3, PoweredOff: 4, PoweredOn: 5 }, AndroidScanMode: { LowLatency: 0, Balanced: 1, LowPower: 2, Opportunistic: 3 }, AndroidConnectionPriority: { Balanced: 0, High: 1, LowPower: 2, }, })); jest.mock('../specs/NativeBleNitroFactory', () => ({ __esModule: true, default: { create: jest.fn(() => mockNativeInstance), }, })); import { AndroidConnectionPriority, BleNitro } from '../index'; // Get reference to the mocked module const mockNative = mockNativeInstance; // Get BLE instance const BleManager = BleNitro.instance(); describe('BleNitro', () => { beforeEach(() => { jest.clearAllMocks(); }); test('startScan calls native with correct parameters', async () => { mockNative.startScan.mockImplementation((filter, callback) => { // eslint-disable-line @typescript-eslint/no-unused-vars // Just call the callback to simulate finding a device }); const scanCallback = jest.fn(); BleManager.startScan({ serviceUUIDs: ['test'] }, scanCallback); expect(mockNative.startScan).toHaveBeenCalledWith( { serviceUUIDs: ['test'], rssiThreshold: -100, allowDuplicates: false, androidScanMode: 1, // AndroidScanMode.Balanced (default) }, expect.any(Function) ); }); test('startScan exposes converted service data to the callback', async () => { // Reset any scanning state left over from previous tests mockNative.stopScan.mockImplementation(() => true); BleManager.stopScan(); const nativeDevice = { id: 'device-1', name: 'Treadmill', rssi: -50, manufacturerData: { companyIdentifiers: [] }, serviceData: { services: [ { uuid: '1826', data: new Uint8Array([0x01, 0x02, 0x03]).buffer, }, ], }, serviceUUIDs: ['1826'], isConnectable: true, isConnected: false, }; mockNative.startScan.mockImplementation((filter, callback) => { // eslint-disable-line @typescript-eslint/no-unused-vars callback(nativeDevice); }); const scanCallback = jest.fn(); BleManager.startScan({}, scanCallback); expect(scanCallback).toHaveBeenCalledTimes(1); const device = scanCallback.mock.calls[0][0]; expect(device.serviceData.services).toHaveLength(1); expect(device.serviceData.services[0].uuid).toBe('00001826-0000-1000-8000-00805f9b34fb'); expect(device.serviceData.services[0].data).toEqual([0x01, 0x02, 0x03]); }); test('stopScan calls native and resolves', async () => { // First start a scan to set _isScanning to true mockNative.startScan.mockImplementation((filter, callback) => { // eslint-disable-line @typescript-eslint/no-unused-vars // Just start scanning }); const scanCallback = jest.fn(); BleManager.startScan({ serviceUUIDs: ['test'] }, scanCallback); // Now stop the scan mockNative.stopScan.mockImplementation(() => true); BleManager.stopScan(); expect(mockNative.stopScan).toHaveBeenCalled(); }); test('connect calls native and resolves with device id', async () => { const deviceId = 'test-device'; mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); const result = await BleManager.connect(deviceId); expect(mockNative.connect).toHaveBeenCalledWith(deviceId, expect.any(Function), undefined, false); expect(result).toBe(deviceId); }); test('connect keeps positional autoConnectAndroid compatibility', async () => { const deviceId = 'test-device-autoconnect'; mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void) => { callback(true, id, ''); }); const result = await BleManager.connect(deviceId, undefined, true); expect(mockNative.connect).toHaveBeenCalledWith(deviceId, expect.any(Function), undefined, true); expect(result).toBe(deviceId); }); test('requestConnectionPriority delegates to native with mapped priority', () => { mockNative.requestConnectionPriority.mockReturnValueOnce(true); const result = BleManager.requestConnectionPriority( 'test-device-priority', AndroidConnectionPriority.High ); expect(mockNative.requestConnectionPriority).toHaveBeenCalledWith('test-device-priority', 1); expect(result).toBe(true); }); test('connect rejects on error', async () => { mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(false, '', 'Connection failed'); }); await expect(BleManager.connect('test')).rejects.toThrow('Connection failed'); }); test('isBluetoothEnabled calls native', () => { mockNative.state.mockReturnValue(5); // PoweredOn const result = BleManager.isBluetoothEnabled(); expect(mockNative.state).toHaveBeenCalled(); expect(result).toBe(true); }); test('writeCharacteristic requires connected device', async () => { const data = [1, 2, 3]; await expect( BleManager.writeCharacteristic('device', 'service', 'char', data) ).rejects.toThrow('Device not connected'); }); test('writeCharacteristic without response returns empty array', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-write'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device-write'; }); // Mock writeCharacteristic with new signature (success, responseData, error) mockNative.writeCharacteristic.mockImplementation((_deviceId: string, _serviceId: string, _charId: string, _data: ArrayBuffer, withResponse: boolean, callback: (success: boolean, responseData: ArrayBuffer, error: string) => void) => { // For withResponse=false, return empty ArrayBuffer const emptyBuffer = new ArrayBuffer(0); callback(true, emptyBuffer, ''); }); const data = [1, 2, 3]; const result = await BleManager.writeCharacteristic('device-write', 'service', 'char', data, false); expect(mockNative.writeCharacteristic).toHaveBeenCalledWith( 'device-write', '0service-0000-1000-8000-00805f9b34fb', '0000char-0000-1000-8000-00805f9b34fb', expect.any(ArrayBuffer), false, expect.any(Function) ); expect(result).toEqual([]); // Empty ByteArray for no response }); test('writeCharacteristic with response returns response data', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-write-resp'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device-write-resp'; }); // Mock writeCharacteristic to return response data mockNative.writeCharacteristic.mockImplementation((_deviceId: string, _serviceId: string, _charId: string, _data: ArrayBuffer, withResponse: boolean, callback: (success: boolean, responseData: ArrayBuffer, error: string) => void) => { // For withResponse=true, return some response data const responseData = new Uint8Array([0xAA, 0xBB, 0xCC]).buffer; callback(true, responseData, ''); }); const data = [1, 2, 3]; const result = await BleManager.writeCharacteristic('device-write-resp', 'service', 'char', data, true); expect(mockNative.writeCharacteristic).toHaveBeenCalledWith( 'device-write-resp', '0service-0000-1000-8000-00805f9b34fb', '0000char-0000-1000-8000-00805f9b34fb', expect.any(ArrayBuffer), true, expect.any(Function) ); expect(result).toEqual([0xAA, 0xBB, 0xCC]); // Response data as ByteArray }); test('getServicesWithCharacteristics discovers and returns services with characteristics', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-svc'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device-svc'; }); // Mock discoverServicesWithCharacteristics mockNative.discoverServicesWithCharacteristics.mockImplementation((_deviceId: string, callback: (success: boolean, error: string) => void) => { callback(true, ''); }); // Mock getServices to return service UUIDs mockNative.getServices.mockReturnValue(['0000180a-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb']); // Mock getCharacteristics per service mockNative.getCharacteristics.mockImplementation((_deviceId: string, serviceId: string) => { if (serviceId === '0000180a-0000-1000-8000-00805f9b34fb') { return ['00002a29-0000-1000-8000-00805f9b34fb']; } return ['00002a19-0000-1000-8000-00805f9b34fb']; }); const result = await BleManager.getServicesWithCharacteristics('device-svc'); expect(mockNative.discoverServicesWithCharacteristics).toHaveBeenCalledWith( 'device-svc', expect.any(Function) ); expect(result).toHaveLength(2); expect(result[0]).toHaveProperty('uuid'); expect(result[0]).toHaveProperty('characteristics'); expect(result[0].characteristics.length).toBeGreaterThan(0); }); test('getServicesWithCharacteristics rejects on discovery failure', async () => { mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-svc-fail'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device-svc-fail'; }); mockNative.discoverServicesWithCharacteristics.mockImplementation((_deviceId: string, callback: (success: boolean, error: string) => void) => { callback(false, 'Discovery failed'); }); await expect(BleManager.getServicesWithCharacteristics('device-svc-fail')).rejects.toThrow('Discovery failed'); }); test('getServicesWithCharacteristics rejects when not connected', async () => { mockNative.isConnected.mockReturnValue(false); await expect(BleManager.getServicesWithCharacteristics('unknown-device')).rejects.toThrow('Device not connected'); }); test('getServicesWithCharacteristics rejects with BleTimeoutError on timeout', async () => { jest.useFakeTimers(); mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-timeout'); mockNative.isConnected.mockImplementation((id: string) => id === 'device-timeout'); // Mock that NEVER calls its callback — simulates hung native discovery mockNative.discoverServicesWithCharacteristics.mockImplementation(() => {}); const promise = BleManager.getServicesWithCharacteristics('device-timeout'); jest.advanceTimersByTime(30_000); await expect(promise).rejects.toThrow('timed out after 30000ms'); jest.useRealTimers(); }); test('readCharacteristic works after connection', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device'; }); // Then read mockNative.readCharacteristic.mockImplementation((_device: string, _service: string, _char: string, callback: (success: boolean, data: ArrayBuffer, error: string) => void) => { const testData = new Uint8Array([85]); callback(true, testData.buffer, ''); // Battery level 85% }); const result = await BleManager.readCharacteristic('device', 'service', 'char'); // UUIDs should be normalized in the call expect(mockNative.readCharacteristic).toHaveBeenCalledWith( 'device', '0service-0000-1000-8000-00805f9b34fb', // 'service' padded to 8 chars '0000char-0000-1000-8000-00805f9b34fb', // 'char' padded to 8 chars expect.any(Function) ); // Result should be number array (ByteArray) expect(Array.isArray(result)).toBe(true); expect(result).toEqual([85]); }); test('disconnect calls native', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device'; }); // Then disconnect mockNative.disconnect.mockImplementation((_id: string, callback: (success: boolean, error: string) => void) => { callback(true, ''); }); const result = await BleManager.disconnect('device'); expect(mockNative.disconnect).toHaveBeenCalledWith('device', expect.any(Function)); expect(result).toBe('device'); }); test('subscribeToCharacteristic calls callback', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device'); // Mock subscription - now uses completion callback (async) mockNative.subscribeToCharacteristic.mockImplementation((_device: string, _service: string, _char: string, updateCallback: (charId: string, data: ArrayBuffer) => void, completionCallback: (success: boolean, error: string) => void) => { // Simulate notification const testData = new Uint8Array([1, 2, 3]); updateCallback('char-id', testData.buffer); // Call completion callback to signal subscription is established completionCallback(true, ''); }); const notificationCallback = jest.fn(); const subscription = await BleManager.subscribeToCharacteristic('device', 'service', 'char', notificationCallback); expect(mockNative.subscribeToCharacteristic).toHaveBeenCalled(); expect(notificationCallback).toHaveBeenCalledWith('char-id', [1, 2, 3]); // Verify subscription object expect(subscription).toHaveProperty('remove'); expect(typeof subscription.remove).toBe('function'); }); test('connect with disconnect event callback', async () => { const deviceId = 'test-device-2'; // Use different device ID to avoid state conflicts const onDisconnect = jest.fn(); mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); // Simulate a disconnect event later if (disconnectCallback) { setTimeout(() => { disconnectCallback(id, true, 'Connection lost'); // interrupted = true }, 10); } }); const result = await BleManager.connect(deviceId, onDisconnect); expect(mockNative.connect).toHaveBeenCalledWith(deviceId, expect.any(Function), expect.any(Function), false); expect(result).toBe(deviceId); // Wait for disconnect callback await new Promise(resolve => setTimeout(resolve, 30)); expect(onDisconnect).toHaveBeenCalledWith(deviceId, true, 'Connection lost'); }); test('isSubscribedToCharacteristic delegates to native with normalized UUIDs', () => { mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(true); const result = BleManager.isSubscribedToCharacteristic('device', 'service', 'char'); expect(result).toBe(true); expect(mockNative.isSubscribedToCharacteristic).toHaveBeenCalledWith( 'device', '0service-0000-1000-8000-00805f9b34fb', '0000char-0000-1000-8000-00805f9b34fb' ); }); test('isSubscribedToCharacteristic returns false when not subscribed', () => { mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(false); const result = BleManager.isSubscribedToCharacteristic('device', 'service', 'char'); expect(result).toBe(false); }); test('isSubscribedToCharacteristic delegates to native for unknown devices', () => { // Native layer handles unknown/disconnected devices by returning false mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(false); const result = BleManager.isSubscribedToCharacteristic('unknown-device', 'service', 'char'); expect(result).toBe(false); expect(mockNative.isSubscribedToCharacteristic).toHaveBeenCalled(); }); test('isSubscribedToCharacteristic propagates native exception', () => { mockNative.isSubscribedToCharacteristic.mockImplementationOnce(() => { throw new Error('Native error'); }); expect(() => BleManager.isSubscribedToCharacteristic('device', 'service', 'char') ).toThrow('Native error'); }); test('readRSSI requires connected device', async () => { await expect( BleManager.readRSSI('device-not-connected') ).rejects.toThrow('Device not connected'); }); test('readRSSI works after connection', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-rssi'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device-rssi'; }); // Mock readRSSI with new signature (success, rssi, error) mockNative.readRSSI.mockImplementation((_deviceId: string, callback: (success: boolean, rssi: number, error: string) => void) => { callback(true, -65, ''); // Mock RSSI value of -65 dBm }); const rssi = await BleManager.readRSSI('device-rssi'); expect(mockNative.readRSSI).toHaveBeenCalledWith( 'device-rssi', expect.any(Function) ); expect(rssi).toBe(-65); }); test('readRSSI handles failure', async () => { // First connect mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => { callback(true, id, ''); }); await BleManager.connect('device-rssi-fail'); mockNative.isConnected.mockImplementation((id: string) => { return id === 'device-rssi-fail'; }); // Mock readRSSI failure mockNative.readRSSI.mockImplementation((_deviceId: string, callback: (success: boolean, rssi: number, error: string) => void) => { callback(false, 0, 'RSSI read failed'); }); await expect(BleManager.readRSSI('device-rssi-fail')).rejects.toThrow('RSSI read failed'); expect(mockNative.readRSSI).toHaveBeenCalledWith( 'device-rssi-fail', expect.any(Function) ); }); });