UNPKG

@ledgerhq/live-common

Version:
325 lines (269 loc) • 11.3 kB
/** * @jest-environment jsdom */ import { renderHook, act } from "@testing-library/react"; import type { Observer as TransportObserver, Subscription as TransportSubscription, DescriptorEvent, } from "@ledgerhq/hw-transport"; import { DeviceModelId, getDeviceModel } from "@ledgerhq/devices"; import { HwTransportError } from "@ledgerhq/errors"; import { useBleDevicesScanning } from "./useBleDevicesScanning"; import type { TransportBleDevice } from "../types"; jest.useFakeTimers(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const nanoXServiceUuid = getDeviceModel(DeviceModelId.nanoX).bluetoothSpec![0].serviceUuid; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const staxServiceUuid = getDeviceModel(DeviceModelId.stax).bluetoothSpec![0].serviceUuid; // Fake devices info we would get from the bluetooth transport listen const aTransportBleDevice = (overrideProps?: Partial<TransportBleDevice>) => { return { id: "aBleNanoId", name: "aBleNano", localName: null, rssi: 50, mtu: 50, serviceUUIDs: [nanoXServiceUuid], ...overrideProps, }; }; type bleTransportListenObserver = TransportObserver< DescriptorEvent<TransportBleDevice | null>, HwTransportError >; const mockBleTransportListenUnsubscribe = jest.fn(); // HOF creating a mocked Transport listen method to be given to useBleDevicesScanning const setupMockBleTransportListen = (mockEmitValuesByObserver: (observer: bleTransportListenObserver) => void) => (observer: bleTransportListenObserver): TransportSubscription => { mockEmitValuesByObserver(observer); return { unsubscribe: mockBleTransportListenUnsubscribe, }; }; describe("useBleDevicesScanning", () => { afterEach(() => { jest.clearAllTimers(); mockBleTransportListenUnsubscribe.mockClear(); }); describe("When several unique devices are found by the scanner", () => { const deviceIdA = "ID_A"; const deviceIdB = "ID_B"; const deviceIdC = "ID_C"; const mockEmitValuesByObserver = (observer: bleTransportListenObserver) => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdA }), }); setTimeout(() => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdB, serviceUUIDs: [staxServiceUuid], }), }); }, 1000); setTimeout(() => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdC }), }); }, 2000); }; describe("and when no filters are applied to the scanning", () => { it("should update the list of scanned devices for each new scanned device", async () => { const { result } = renderHook(() => useBleDevicesScanning({ bleTransportListen: setupMockBleTransportListen(mockEmitValuesByObserver), }), ); expect(result.current.scannedDevices).toHaveLength(1); expect(result.current.scannedDevices[0].deviceId).toBe(deviceIdA); // The model was correctly deduced from the ble spec expect(result.current.scannedDevices[0].deviceModel.id).toBe(DeviceModelId.nanoX); await act(async () => { jest.advanceTimersByTime(1000); }); expect(result.current.scannedDevices).toHaveLength(2); expect(result.current.scannedDevices[1].deviceId).toBe(deviceIdB); expect(result.current.scannedDevices[1].deviceModel.id).toBe(DeviceModelId.stax); }); }); describe("and when filterByDeviceModelIds is not null", () => { it("should filter the scanning result by the given model ids", async () => { const { result } = renderHook(() => useBleDevicesScanning({ bleTransportListen: setupMockBleTransportListen(mockEmitValuesByObserver), filterByDeviceModelIds: [DeviceModelId.nanoX], }), ); // The first scanned device was a nanoX expect(result.current.scannedDevices).toHaveLength(1); expect(result.current.scannedDevices[0].deviceId).toBe(deviceIdA); // The model was correctly deduced from the ble spec expect(result.current.scannedDevices[0].deviceModel.id).toBe(DeviceModelId.nanoX); await act(async () => { jest.advanceTimersByTime(1000); }); // The second scanned device was a stax, and was filtered out expect(result.current.scannedDevices).toHaveLength(1); await act(async () => { jest.advanceTimersByTime(1000); }); // The third scanned device was a nanoX expect(result.current.scannedDevices).toHaveLength(2); expect(result.current.scannedDevices[1].deviceId).toBe(deviceIdC); expect(result.current.scannedDevices[1].deviceModel.id).toBe(DeviceModelId.nanoX); }); }); describe("and when filterOutDevicesByDeviceIds is not null nor empty", () => { it("should not add the scanned device if its ids is in the array", async () => { const { result } = renderHook(() => useBleDevicesScanning({ bleTransportListen: setupMockBleTransportListen(mockEmitValuesByObserver), filterOutDevicesByDeviceIds: [deviceIdA, deviceIdC], }), ); // The first scanned device (deviceIdA) should be filtered out expect(result.current.scannedDevices).toHaveLength(0); await act(async () => { jest.advanceTimersByTime(1000); }); // The second scanned device is deviceIdB and should be kept expect(result.current.scannedDevices).toHaveLength(1); expect(result.current.scannedDevices[0].deviceId).toBe(deviceIdB); await act(async () => { jest.advanceTimersByTime(1000); }); // The third scanned device (deviceIdC) should be filtered out expect(result.current.scannedDevices).toHaveLength(1); }); }); describe("and when the hook consumer stops the scanning", () => { it("should stop the scanning", async () => { let stopBleScanning = false; const { result, rerender } = renderHook(() => useBleDevicesScanning({ bleTransportListen: setupMockBleTransportListen(mockEmitValuesByObserver), stopBleScanning, }), ); // At first the scanning finds device(s) expect(result.current.scannedDevices).toHaveLength(1); expect(result.current.scannedDevices[0].deviceId).toBe(deviceIdA); expect(result.current.scannedDevices[0].deviceModel.id).toBe(DeviceModelId.nanoX); // Then the consumer stops the scanning stopBleScanning = true; rerender({ bleTransportListen: setupMockBleTransportListen(mockEmitValuesByObserver), stopBleScanning, }); await act(async () => { jest.advanceTimersByTime(2000); }); // It should not find any new devices expect(result.current.scannedDevices).toHaveLength(1); }); }); }); describe("When the same device is being scanned several times", () => { const deviceIdA = "ID_A"; const deviceIdB = "ID_B"; const mockEmitValuesByObserver = (observer: bleTransportListenObserver) => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdA }), }); setTimeout(() => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdA }), }); }, 1000); setTimeout(() => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdB }), }); }, 2000); }; it("should update the list of scanned devices without any duplicate", async () => { const { result } = renderHook(() => useBleDevicesScanning({ bleTransportListen: setupMockBleTransportListen(mockEmitValuesByObserver), }), ); // The first time it gets the device from the scanning expect(result.current.scannedDevices).toHaveLength(1); expect(result.current.scannedDevices[0].deviceId).toBe(deviceIdA); expect(result.current.scannedDevices[0].deviceModel.id).toBe(DeviceModelId.nanoX); await act(async () => { jest.advanceTimersByTime(1000); }); // The second time it gets the same device from the scanning // It should not have been added to the list expect(result.current.scannedDevices).toHaveLength(1); await act(async () => { jest.advanceTimersByTime(1000); }); // The third time it gets a new device expect(result.current.scannedDevices).toHaveLength(2); expect(result.current.scannedDevices[1].deviceId).toBe(deviceIdB); expect(result.current.scannedDevices[1].deviceModel.id).toBe(DeviceModelId.nanoX); }); }); describe("When a device is only seen after a cleaned scanning", () => { const deviceIdA = "ID_A"; const deviceIdB = "ID_B"; const emitTimeOfDeviceB = 3000; const mockEmitValuesByObserver = (observer: bleTransportListenObserver) => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdA }), }); setTimeout(() => { observer.next({ type: "add", descriptor: aTransportBleDevice({ id: deviceIdB }), }); }, emitTimeOfDeviceB); }; it("should restart the scanning after a defined time and update the list of scanned devices", async () => { const restartScanningTimeoutMs = emitTimeOfDeviceB; // To avoid re-rendering the hook when mockEmitValuesByObserver // emits a new value with the setTimeout const bleTransportListen = setupMockBleTransportListen(mockEmitValuesByObserver); const { result } = renderHook(() => useBleDevicesScanning({ bleTransportListen, restartScanningTimeoutMs, }), ); // The first time it gets the device from the scanning expect(result.current.scannedDevices).toHaveLength(1); expect(result.current.scannedDevices[0].deviceId).toBe(deviceIdA); expect(result.current.scannedDevices[0].deviceModel.id).toBe(DeviceModelId.nanoX); const nbUnsubscribesHappeningBecauseOfRenderHook = mockBleTransportListenUnsubscribe.mock.calls.length; // Advances by less than the first restart timeout await act(async () => { jest.advanceTimersByTime(restartScanningTimeoutMs - 1000); }); expect(mockBleTransportListenUnsubscribe).toBeCalledTimes( nbUnsubscribesHappeningBecauseOfRenderHook, ); // Advances by the total time of the restart timeout await act(async () => { jest.advanceTimersByTime(1000); }); expect(mockBleTransportListenUnsubscribe).toBeCalledTimes( nbUnsubscribesHappeningBecauseOfRenderHook + 1, ); expect(result.current.scannedDevices).toHaveLength(2); expect(result.current.scannedDevices[1].deviceId).toBe(deviceIdB); expect(result.current.scannedDevices[1].deviceModel.id).toBe(DeviceModelId.nanoX); }); }); });