UNPKG

react-native-ble-nitro

Version:

High-performance React Native BLE library built on Nitro Modules

338 lines (279 loc) 13.2 kB
// 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(), readRSSI: jest.fn(), discoverServices: jest.fn(), getServices: jest.fn(), getCharacteristics: jest.fn(), readCharacteristic: jest.fn(), writeCharacteristic: jest.fn(), subscribeToCharacteristic: jest.fn(), unsubscribeFromCharacteristic: 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 }, })); jest.mock('../specs/NativeBleNitroFactory', () => ({ __esModule: true, default: { create: jest.fn(() => mockNativeInstance), }, })); import { 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('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 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'); // 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'); // 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('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'); // 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'); // 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(undefined); }); 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('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'); // 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'); // 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) ); }); });