react-native-ble-nitro
Version:
High-performance React Native BLE library built on Nitro Modules - drop-in replacement for react-native-ble-plx
563 lines (472 loc) • 18.6 kB
text/typescript
/**
* deviceWrapper.test.ts
* React Native BLE Nitro - Device Wrapper Tests
* Copyright © 2025 Zyke (https://zyke.co)
*/
import { DeviceWrapper, ServiceWrapper, CharacteristicWrapper, DescriptorWrapper } from '../../compatibility/deviceWrapper';
import { ConnectionPriority, CharacteristicSubscriptionType } from '../../specs/types';
// Mock Nitro Device
const createMockNitroDevice = () => ({
id: 'test-device-id',
deviceName: 'Test Device',
rssi: -50,
mtu: 247,
manufacturerData: 'AQIDBA==', // [1, 2, 3, 4] in base64
serviceData: [
{ uuid: '180f', data: 'ZA==' }, // Battery service with value 100
{ uuid: '1234', data: 'dGVzdA==' } // Custom service with "test"
],
serviceUUIDs: ['180f', '1234-5678-9abc-def0-123456789abc'],
localName: 'Local Test Device',
txPowerLevel: 4,
solicitedServiceUUIDs: ['abcd'],
isConnectable: true,
overflowServiceUUIDs: ['efgh'],
rawScanRecord: 'AQIDBAU=',
// Mock methods
requestConnectionPriority: jest.fn().mockResolvedValue({}),
readRSSI: jest.fn().mockResolvedValue({}),
requestMTU: jest.fn().mockResolvedValue({}),
connect: jest.fn().mockResolvedValue({}),
cancelConnection: jest.fn().mockResolvedValue({}),
isConnected: jest.fn().mockResolvedValue(true),
onDisconnected: jest.fn().mockReturnValue({ remove: jest.fn() }),
discoverAllServicesAndCharacteristics: jest.fn().mockResolvedValue({}),
services: jest.fn().mockResolvedValue([]),
characteristicsForService: jest.fn().mockResolvedValue([]),
readCharacteristicForService: jest.fn().mockResolvedValue({}),
writeCharacteristicWithResponseForService: jest.fn().mockResolvedValue({}),
writeCharacteristicWithoutResponseForService: jest.fn().mockResolvedValue({}),
monitorCharacteristicForService: jest.fn().mockReturnValue({ remove: jest.fn() }),
descriptorsForService: jest.fn().mockResolvedValue([]),
readDescriptorForService: jest.fn().mockResolvedValue({}),
writeDescriptorForService: jest.fn().mockResolvedValue({})
});
describe('DeviceWrapper', () => {
let mockNitroDevice: ReturnType<typeof createMockNitroDevice>;
let deviceWrapper: DeviceWrapper;
beforeEach(() => {
mockNitroDevice = createMockNitroDevice();
deviceWrapper = new DeviceWrapper(mockNitroDevice);
jest.clearAllMocks();
});
describe('Property Mapping', () => {
it('should map device identification properties correctly', () => {
expect(deviceWrapper.id).toBe('test-device-id');
expect(deviceWrapper.name).toBe('Test Device'); // Maps from deviceName
expect(deviceWrapper.rssi).toBe(-50);
expect(deviceWrapper.mtu).toBe(247);
});
it('should handle null device name', () => {
mockNitroDevice.deviceName = null;
const wrapper = new DeviceWrapper(mockNitroDevice);
expect(wrapper.name).toBeNull();
});
it('should map advertisement data properties', () => {
expect(deviceWrapper.manufacturerData).toBe('AQIDBA==');
expect(deviceWrapper.rawScanRecord).toBe('AQIDBAU=');
expect(deviceWrapper.localName).toBe('Local Test Device');
expect(deviceWrapper.txPowerLevel).toBe(4);
expect(deviceWrapper.isConnectable).toBe(true);
});
it('should map service-related properties', () => {
expect(deviceWrapper.serviceUUIDs).toEqual(['180f', '1234-5678-9abc-def0-123456789abc']);
expect(deviceWrapper.solicitedServiceUUIDs).toEqual(['abcd']);
expect(deviceWrapper.overflowServiceUUIDs).toEqual(['efgh']);
});
it('should convert service data array to map format', () => {
const serviceData = deviceWrapper.serviceData;
expect(serviceData).toEqual({
'180f': 'ZA==',
'1234': 'dGVzdA=='
});
});
it('should handle null service data', () => {
mockNitroDevice.serviceData = null;
const wrapper = new DeviceWrapper(mockNitroDevice);
expect(wrapper.serviceData).toBeNull();
});
});
describe('Connection Management', () => {
it('should request connection priority', async () => {
mockNitroDevice.requestConnectionPriority.mockResolvedValue(mockNitroDevice);
const result = await deviceWrapper.requestConnectionPriority(
ConnectionPriority.High,
'transaction-123'
);
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.requestConnectionPriority).toHaveBeenCalledWith(
ConnectionPriority.High,
'transaction-123'
);
});
it('should read RSSI', async () => {
const updatedDevice = { ...mockNitroDevice, rssi: -45 };
mockNitroDevice.readRSSI.mockResolvedValue(updatedDevice);
const result = await deviceWrapper.readRSSI('transaction-456');
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.readRSSI).toHaveBeenCalledWith('transaction-456');
});
it('should request MTU change', async () => {
const updatedDevice = { ...mockNitroDevice, mtu: 512 };
mockNitroDevice.requestMTU.mockResolvedValue(updatedDevice);
const result = await deviceWrapper.requestMTU(512, 'mtu-transaction');
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.requestMTU).toHaveBeenCalledWith(512, 'mtu-transaction');
});
it('should connect with default options', async () => {
mockNitroDevice.connect.mockResolvedValue(mockNitroDevice);
const result = await deviceWrapper.connect();
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.connect).toHaveBeenCalledWith({
autoConnect: false,
requestMTU: 23,
timeout: 0
});
});
it('should connect with custom options', async () => {
mockNitroDevice.connect.mockResolvedValue(mockNitroDevice);
const result = await deviceWrapper.connect({
autoConnect: true,
requestMTU: 247,
timeout: 5000
});
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.connect).toHaveBeenCalledWith({
autoConnect: true,
requestMTU: 247,
timeout: 5000
});
});
it('should cancel connection', async () => {
mockNitroDevice.cancelConnection.mockResolvedValue(mockNitroDevice);
const result = await deviceWrapper.cancelConnection();
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.cancelConnection).toHaveBeenCalled();
});
it('should check connection status', async () => {
mockNitroDevice.isConnected.mockResolvedValue(true);
const result = await deviceWrapper.isConnected();
expect(result).toBe(true);
expect(mockNitroDevice.isConnected).toHaveBeenCalled();
});
it('should handle disconnection listener', () => {
const listener = jest.fn();
const mockSubscription = { remove: jest.fn() };
mockNitroDevice.onDisconnected.mockReturnValue(mockSubscription);
const subscription = deviceWrapper.onDisconnected(listener);
expect(subscription).toBe(mockSubscription);
expect(mockNitroDevice.onDisconnected).toHaveBeenCalled();
// Verify the listener wrapper is called correctly
const callArgs = mockNitroDevice.onDisconnected.mock.calls[0][0];
const mockError = new Error('Disconnected');
const mockDevice = { ...mockNitroDevice };
callArgs(mockError, mockDevice);
expect(listener).toHaveBeenCalledWith(mockError, expect.any(DeviceWrapper));
});
});
describe('Service Discovery', () => {
it('should discover all services and characteristics', async () => {
mockNitroDevice.discoverAllServicesAndCharacteristics.mockResolvedValue(mockNitroDevice);
const result = await deviceWrapper.discoverAllServicesAndCharacteristics('discovery-tx');
expect(result).toBeInstanceOf(DeviceWrapper);
expect(mockNitroDevice.discoverAllServicesAndCharacteristics).toHaveBeenCalledWith('discovery-tx');
});
it('should get services list', async () => {
const mockServices = [
{ id: 1, uuid: '180f', deviceID: 'test-device-id', isPrimary: true }
];
mockNitroDevice.services.mockResolvedValue(mockServices);
const result = await deviceWrapper.services();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(ServiceWrapper);
expect(mockNitroDevice.services).toHaveBeenCalled();
});
});
describe('Characteristic Operations', () => {
const serviceUUID = '180f';
const characteristicUUID = '2a19';
it('should get characteristics for service', async () => {
const mockCharacteristics = [
{
id: 1,
uuid: characteristicUUID,
serviceID: 1,
serviceUUID,
deviceID: 'test-device-id',
isReadable: true,
isWritableWithResponse: false,
isWritableWithoutResponse: false,
isNotifiable: true,
isNotifying: false,
isIndicatable: false,
value: null
}
];
mockNitroDevice.characteristicsForService.mockResolvedValue(mockCharacteristics);
const result = await deviceWrapper.characteristicsForService(serviceUUID);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(CharacteristicWrapper);
expect(mockNitroDevice.characteristicsForService).toHaveBeenCalledWith(serviceUUID);
});
it('should read characteristic', async () => {
const mockCharacteristic = {
id: 1,
uuid: characteristicUUID,
value: 'dGVzdA=='
};
mockNitroDevice.readCharacteristicForService.mockResolvedValue(mockCharacteristic);
const result = await deviceWrapper.readCharacteristicForService(
serviceUUID,
characteristicUUID,
'read-tx'
);
expect(result).toBeInstanceOf(CharacteristicWrapper);
expect(mockNitroDevice.readCharacteristicForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
'read-tx'
);
});
it('should write characteristic with response', async () => {
const value = 'dGVzdCB2YWx1ZQ==';
const mockCharacteristic = {
id: 1,
uuid: characteristicUUID,
value
};
mockNitroDevice.writeCharacteristicWithResponseForService.mockResolvedValue(mockCharacteristic);
const result = await deviceWrapper.writeCharacteristicWithResponseForService(
serviceUUID,
characteristicUUID,
value,
'write-tx'
);
expect(result).toBeInstanceOf(CharacteristicWrapper);
expect(mockNitroDevice.writeCharacteristicWithResponseForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
value,
'write-tx'
);
});
it('should write characteristic without response', async () => {
const value = 'dGVzdA==';
const mockCharacteristic = {
id: 1,
uuid: characteristicUUID,
value
};
mockNitroDevice.writeCharacteristicWithoutResponseForService.mockResolvedValue(mockCharacteristic);
const result = await deviceWrapper.writeCharacteristicWithoutResponseForService(
serviceUUID,
characteristicUUID,
value
);
expect(result).toBeInstanceOf(CharacteristicWrapper);
expect(mockNitroDevice.writeCharacteristicWithoutResponseForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
value,
undefined
);
});
it('should monitor characteristic for changes', () => {
const listener = jest.fn();
const mockSubscription = { remove: jest.fn() };
mockNitroDevice.monitorCharacteristicForService.mockReturnValue(mockSubscription);
const result = deviceWrapper.monitorCharacteristicForService(
serviceUUID,
characteristicUUID,
listener,
'monitor-tx',
'notification'
);
expect(result).toBe(mockSubscription);
expect(mockNitroDevice.monitorCharacteristicForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
expect.any(Function),
'monitor-tx',
CharacteristicSubscriptionType.Notification
);
// Test the listener wrapper
const callArgs = mockNitroDevice.monitorCharacteristicForService.mock.calls[0][2];
const mockError = null;
const mockCharacteristic = { id: 1, uuid: characteristicUUID };
callArgs(mockError, mockCharacteristic);
expect(listener).toHaveBeenCalledWith(mockError, expect.any(CharacteristicWrapper));
});
it('should monitor characteristic without subscription type', () => {
const listener = jest.fn();
const mockSubscription = { remove: jest.fn() };
mockNitroDevice.monitorCharacteristicForService.mockReturnValue(mockSubscription);
deviceWrapper.monitorCharacteristicForService(
serviceUUID,
characteristicUUID,
listener
);
expect(mockNitroDevice.monitorCharacteristicForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
expect.any(Function),
undefined,
undefined
);
});
});
describe('Descriptor Operations', () => {
const serviceUUID = '180f';
const characteristicUUID = '2a19';
const descriptorUUID = '2902';
it('should get descriptors for characteristic', async () => {
const mockDescriptors = [
{
id: 1,
uuid: descriptorUUID,
characteristicID: 1,
characteristicUUID,
serviceID: 1,
serviceUUID,
deviceID: 'test-device-id',
value: null
}
];
mockNitroDevice.descriptorsForService.mockResolvedValue(mockDescriptors);
const result = await deviceWrapper.descriptorsForService(serviceUUID, characteristicUUID);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(DescriptorWrapper);
expect(mockNitroDevice.descriptorsForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID
);
});
it('should read descriptor', async () => {
const mockDescriptor = {
id: 1,
uuid: descriptorUUID,
value: 'AQA=' // [1, 0] in base64
};
mockNitroDevice.readDescriptorForService.mockResolvedValue(mockDescriptor);
const result = await deviceWrapper.readDescriptorForService(
serviceUUID,
characteristicUUID,
descriptorUUID,
'descriptor-read-tx'
);
expect(result).toBeInstanceOf(DescriptorWrapper);
expect(mockNitroDevice.readDescriptorForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
descriptorUUID,
'descriptor-read-tx'
);
});
it('should write descriptor', async () => {
const value = 'AQA=';
const mockDescriptor = {
id: 1,
uuid: descriptorUUID,
value
};
mockNitroDevice.writeDescriptorForService.mockResolvedValue(mockDescriptor);
const result = await deviceWrapper.writeDescriptorForService(
serviceUUID,
characteristicUUID,
descriptorUUID,
value,
'descriptor-write-tx'
);
expect(result).toBeInstanceOf(DescriptorWrapper);
expect(mockNitroDevice.writeDescriptorForService).toHaveBeenCalledWith(
serviceUUID,
characteristicUUID,
descriptorUUID,
value,
'descriptor-write-tx'
);
});
});
});
describe('ServiceWrapper', () => {
let mockService: any;
let mockNitroDevice: any;
let serviceWrapper: ServiceWrapper;
beforeEach(() => {
mockService = {
id: 1,
uuid: '180f',
deviceID: 'test-device',
isPrimary: true
};
mockNitroDevice = createMockNitroDevice();
serviceWrapper = new ServiceWrapper(mockService, mockNitroDevice);
});
it('should expose service properties', () => {
expect(serviceWrapper.id).toBe(1);
expect(serviceWrapper.uuid).toBe('180f');
expect(serviceWrapper.deviceID).toBe('test-device');
expect(serviceWrapper.isPrimary).toBe(true);
});
it('should delegate characteristics method to device', async () => {
mockNitroDevice.characteristicsForService.mockResolvedValue([]);
await serviceWrapper.characteristics();
expect(mockNitroDevice.characteristicsForService).toHaveBeenCalledWith('180f');
});
it('should delegate read characteristic method to device', async () => {
const characteristicUUID = '2a19';
mockNitroDevice.readCharacteristicForService.mockResolvedValue({});
await serviceWrapper.readCharacteristic(characteristicUUID, 'tx-123');
expect(mockNitroDevice.readCharacteristicForService).toHaveBeenCalledWith(
'180f',
characteristicUUID,
'tx-123'
);
});
});
describe('CharacteristicWrapper', () => {
let mockCharacteristic: any;
let mockNitroDevice: any;
let characteristicWrapper: CharacteristicWrapper;
beforeEach(() => {
mockCharacteristic = {
id: 1,
uuid: '2a19',
serviceID: 1,
serviceUUID: '180f',
deviceID: 'test-device',
isReadable: true,
isWritableWithResponse: true,
isWritableWithoutResponse: false,
isNotifiable: true,
isNotifying: false,
isIndicatable: false,
value: 'dGVzdA=='
};
mockNitroDevice = createMockNitroDevice();
characteristicWrapper = new CharacteristicWrapper(mockCharacteristic, mockNitroDevice);
});
it('should expose characteristic properties', () => {
expect(characteristicWrapper.id).toBe(1);
expect(characteristicWrapper.uuid).toBe('2a19');
expect(characteristicWrapper.serviceID).toBe(1);
expect(characteristicWrapper.serviceUUID).toBe('180f');
expect(characteristicWrapper.deviceID).toBe('test-device');
expect(characteristicWrapper.isReadable).toBe(true);
expect(characteristicWrapper.isWritableWithResponse).toBe(true);
expect(characteristicWrapper.isWritableWithoutResponse).toBe(false);
expect(characteristicWrapper.isNotifiable).toBe(true);
expect(characteristicWrapper.isNotifying).toBe(false);
expect(characteristicWrapper.isIndicatable).toBe(false);
expect(characteristicWrapper.value).toBe('dGVzdA==');
});
it('should delegate read method to device', async () => {
mockNitroDevice.readCharacteristicForService.mockResolvedValue(mockCharacteristic);
await characteristicWrapper.read('read-tx');
expect(mockNitroDevice.readCharacteristicForService).toHaveBeenCalledWith(
'180f',
'2a19',
'read-tx'
);
});
});