@onekeyfe/hd-transport-electron 1.2.2-alpha.12 → 1.2.2-alpha.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-eea100c1.js → index-474dbbae.js} +33 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/{noble-ble-handler-e4eaea92.js → noble-ble-handler-23c86603.js} +373 -99
- package/dist/noble-ble-handler.d.ts +11 -2
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/noble-ble-timeouts.d.ts +1 -1
- package/dist/noble-ble-timeouts.d.ts.map +1 -1
- package/dist/types/desktop-api.d.ts +13 -0
- package/dist/types/desktop-api.d.ts.map +1 -1
- package/dist/types/noble-extended.d.ts +2 -0
- package/dist/types/noble-extended.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +775 -22
- package/src/index.ts +14 -1
- package/src/noble-ble-handler.ts +440 -119
- package/src/noble-ble-timeouts.ts +1 -1
- package/src/types/desktop-api.ts +44 -0
- package/src/types/noble-extended.ts +2 -0
|
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|
|
2
2
|
import { EOneKeyBleMessageKeys, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS,
|
|
6
6
|
NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
|
|
7
7
|
} from '../noble-ble-timeouts';
|
|
8
8
|
|
|
@@ -26,29 +26,128 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
26
26
|
jest.clearAllMocks();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
test('
|
|
29
|
+
test('bounds targeted scans and subscription callbacks independently', () => {
|
|
30
30
|
expect(NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS).toBe(5_000);
|
|
31
|
-
expect(
|
|
31
|
+
expect(NOBLE_BLE_SUBSCRIBE_TIMEOUT_MS).toBe(10_000);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test('maps macOS stale pairing failures without
|
|
34
|
+
test('maps structured macOS stale pairing failures without parsing localized text', async () => {
|
|
35
35
|
const { createNobleBleConnectionError } = await import('../noble-ble-handler');
|
|
36
36
|
|
|
37
|
+
const staleBondError = createNobleBleConnectionError(
|
|
38
|
+
Object.assign(new Error('Peer removed pairing information on the device side'), {
|
|
39
|
+
nativeErrorCode: 14,
|
|
40
|
+
nativeErrorDomain: 'CBErrorDomain',
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
expect(staleBondError).toMatchObject({
|
|
44
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
45
|
+
params: {
|
|
46
|
+
nativeErrorMessage: 'Peer removed pairing information on the device side',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
expect(staleBondError.message).toContain('Peer removed pairing information on the device side');
|
|
50
|
+
expect(createNobleBleConnectionError(new Error('Encryption is insufficient'))).toMatchObject({
|
|
51
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
52
|
+
});
|
|
37
53
|
expect(
|
|
38
54
|
createNobleBleConnectionError(
|
|
39
|
-
new Error('
|
|
55
|
+
Object.assign(new Error('localized native message'), {
|
|
56
|
+
nativeErrorCode: 14,
|
|
57
|
+
nativeErrorDomain: 'CBATTErrorDomain',
|
|
58
|
+
})
|
|
40
59
|
)
|
|
41
60
|
).toMatchObject({
|
|
42
|
-
errorCode: HardwareErrorCode.
|
|
61
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
43
62
|
});
|
|
44
|
-
expect(
|
|
45
|
-
|
|
63
|
+
expect(
|
|
64
|
+
createNobleBleConnectionError(
|
|
65
|
+
Object.assign(new Error('Encryption is insufficient'), {
|
|
66
|
+
nativeErrorCode: 15,
|
|
67
|
+
nativeErrorDomain: 'CBATTErrorDomain',
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
).toMatchObject({
|
|
71
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
46
72
|
});
|
|
47
73
|
expect(createNobleBleConnectionError(new Error('connection failed'))).toMatchObject({
|
|
48
74
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
49
75
|
});
|
|
50
76
|
});
|
|
51
77
|
|
|
78
|
+
test('serializes HardwareError fields for the Noble IPC boundary', async () => {
|
|
79
|
+
const { createNobleBleIpcErrorResponse } = await import('../noble-ble-handler');
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
createNobleBleIpcErrorResponse({
|
|
83
|
+
name: 'HardwareError',
|
|
84
|
+
message: 'Bluetooth pairing information is no longer valid',
|
|
85
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
86
|
+
params: { nativeErrorMessage: 'native message' },
|
|
87
|
+
})
|
|
88
|
+
).toEqual({
|
|
89
|
+
type: 'NobleBleIpcError',
|
|
90
|
+
success: false,
|
|
91
|
+
error: {
|
|
92
|
+
name: 'HardwareError',
|
|
93
|
+
message: 'Bluetooth pairing information is no longer valid',
|
|
94
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
95
|
+
params: { nativeErrorMessage: 'native message' },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(createNobleBleIpcErrorResponse(new Error('untyped failure'))).toEqual({
|
|
100
|
+
type: 'NobleBleIpcError',
|
|
101
|
+
success: false,
|
|
102
|
+
error: {
|
|
103
|
+
name: 'Error',
|
|
104
|
+
message: 'untyped failure',
|
|
105
|
+
errorCode: HardwareErrorCode.UnknownError,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const circularParams: { self?: unknown } = {};
|
|
110
|
+
circularParams.self = circularParams;
|
|
111
|
+
expect(
|
|
112
|
+
createNobleBleIpcErrorResponse({
|
|
113
|
+
message: 'failure with unsafe params',
|
|
114
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
115
|
+
params: circularParams,
|
|
116
|
+
})
|
|
117
|
+
).toEqual({
|
|
118
|
+
type: 'NobleBleIpcError',
|
|
119
|
+
success: false,
|
|
120
|
+
error: {
|
|
121
|
+
name: 'Error',
|
|
122
|
+
message: 'failure with unsafe params',
|
|
123
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('rejects a structured Noble IPC failure at the preload boundary', async () => {
|
|
129
|
+
const { invokeNobleBleIpc } = await import('../types/desktop-api');
|
|
130
|
+
|
|
131
|
+
await expect(
|
|
132
|
+
invokeNobleBleIpc(
|
|
133
|
+
Promise.resolve({
|
|
134
|
+
type: 'NobleBleIpcError' as const,
|
|
135
|
+
success: false as const,
|
|
136
|
+
error: {
|
|
137
|
+
name: 'HardwareError',
|
|
138
|
+
message: 'Bluetooth pairing information is no longer valid',
|
|
139
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
).rejects.toMatchObject({
|
|
144
|
+
name: 'HardwareError',
|
|
145
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await expect(invokeNobleBleIpc(Promise.resolve('connected'))).resolves.toBe('connected');
|
|
149
|
+
});
|
|
150
|
+
|
|
52
151
|
test('keeps safe pacing by default and allows an explicit high-throughput bypass', async () => {
|
|
53
152
|
const { resolveNobleBleWritePacingDelay } = await import('../noble-ble-handler');
|
|
54
153
|
|
|
@@ -126,6 +225,133 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
126
225
|
]);
|
|
127
226
|
});
|
|
128
227
|
|
|
228
|
+
test.each(['Pro A1B2', 'Pro2 A1B2'])(
|
|
229
|
+
'retains %s across scan rounds until Bluetooth is powered off',
|
|
230
|
+
async name => {
|
|
231
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
232
|
+
|
|
233
|
+
const handlers = new Map<string, IpcHandler>();
|
|
234
|
+
const ipcMain = {
|
|
235
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
236
|
+
handlers.set(channel, handler);
|
|
237
|
+
}),
|
|
238
|
+
removeHandler: jest.fn((channel: string) => {
|
|
239
|
+
handlers.delete(channel);
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
const noble = Object.assign(new EventEmitter(), {
|
|
243
|
+
state: 'poweredOn',
|
|
244
|
+
startScanning: jest.fn(),
|
|
245
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
249
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
250
|
+
jest.doMock('electron-log', () => ({
|
|
251
|
+
info: jest.fn(),
|
|
252
|
+
debug: jest.fn(),
|
|
253
|
+
error: jest.fn(),
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
257
|
+
setupNobleBleHandlers({
|
|
258
|
+
on: jest.fn(),
|
|
259
|
+
send: jest.fn(),
|
|
260
|
+
} as unknown as WebContents);
|
|
261
|
+
|
|
262
|
+
const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
|
|
263
|
+
if (!enumerate) {
|
|
264
|
+
throw new Error('Electron Noble BLE enumerate handler was not registered');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const scan = async (peripherals: ReturnType<typeof createPeripheral>[]) => {
|
|
268
|
+
let resolveScanStarted = () => undefined;
|
|
269
|
+
const scanStarted = new Promise<void>(resolve => {
|
|
270
|
+
resolveScanStarted = resolve;
|
|
271
|
+
});
|
|
272
|
+
noble.startScanning.mockImplementationOnce((_services, _duplicates, callback) => {
|
|
273
|
+
callback?.();
|
|
274
|
+
peripherals.forEach(peripheral => noble.emit('discover', peripheral));
|
|
275
|
+
resolveScanStarted();
|
|
276
|
+
});
|
|
277
|
+
const devices = Promise.resolve(enumerate());
|
|
278
|
+
await scanStarted;
|
|
279
|
+
jest.advanceTimersByTime(5_000);
|
|
280
|
+
return devices;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const firstDevice = createPeripheral('first-device', name);
|
|
284
|
+
const secondDevice = createPeripheral('second-device', 'Pro2 C3D4');
|
|
285
|
+
await expect(scan([firstDevice])).resolves.toEqual([
|
|
286
|
+
expect.objectContaining({ id: 'first-device', name }),
|
|
287
|
+
]);
|
|
288
|
+
await expect(scan([secondDevice, secondDevice])).resolves.toEqual([
|
|
289
|
+
expect.objectContaining({ id: 'first-device', name }),
|
|
290
|
+
expect.objectContaining({ id: 'second-device', name: 'Pro2 C3D4' }),
|
|
291
|
+
]);
|
|
292
|
+
await expect(scan([])).resolves.toHaveLength(2);
|
|
293
|
+
|
|
294
|
+
noble.state = 'poweredOff';
|
|
295
|
+
noble.emit('stateChange', 'poweredOff');
|
|
296
|
+
noble.state = 'poweredOn';
|
|
297
|
+
noble.emit('stateChange', 'poweredOn');
|
|
298
|
+
await expect(scan([])).resolves.toEqual([]);
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
test.each(['Pro A1B2', 'Pro2 A1B2'])(
|
|
303
|
+
'refreshes a retained disconnected %s peripheral before cold connecting',
|
|
304
|
+
async name => {
|
|
305
|
+
const handlers = new Map<string, IpcHandler>();
|
|
306
|
+
const stalePeripheral = Object.assign(new EventEmitter(), createPeripheral('device', name), {
|
|
307
|
+
connect: jest.fn(),
|
|
308
|
+
});
|
|
309
|
+
const freshPeripheral = Object.assign(new EventEmitter(), createPeripheral('device', name), {
|
|
310
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
311
|
+
callback(new Error('expected fresh connection failure'));
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
const noble = Object.assign(new EventEmitter(), {
|
|
315
|
+
state: 'poweredOn',
|
|
316
|
+
startScanning: jest.fn((_services, _duplicates, callback) => {
|
|
317
|
+
callback?.();
|
|
318
|
+
noble.emit('discover', freshPeripheral);
|
|
319
|
+
}),
|
|
320
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
321
|
+
connectAsync: jest.fn(() => Promise.resolve(undefined)),
|
|
322
|
+
});
|
|
323
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
324
|
+
jest.doMock('electron', () => ({
|
|
325
|
+
ipcMain: {
|
|
326
|
+
handle: (channel: string, handler: IpcHandler) => handlers.set(channel, handler),
|
|
327
|
+
removeHandler: (channel: string) => handlers.delete(channel),
|
|
328
|
+
},
|
|
329
|
+
}));
|
|
330
|
+
jest.doMock('electron-log', () => ({
|
|
331
|
+
info: jest.fn(),
|
|
332
|
+
debug: jest.fn(),
|
|
333
|
+
error: jest.fn(),
|
|
334
|
+
}));
|
|
335
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
336
|
+
setupNobleBleHandlers({ on: jest.fn(), send: jest.fn() } as unknown as WebContents);
|
|
337
|
+
const availability = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK);
|
|
338
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
339
|
+
if (!availability || !connect) throw new Error('Noble handlers were not registered');
|
|
340
|
+
await availability();
|
|
341
|
+
noble.emit('discover', stalePeripheral);
|
|
342
|
+
|
|
343
|
+
await expect(Promise.resolve(connect(undefined, stalePeripheral.id))).resolves.toMatchObject({
|
|
344
|
+
success: false,
|
|
345
|
+
error: { message: 'expected fresh connection failure' },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(stalePeripheral.connect).not.toHaveBeenCalled();
|
|
349
|
+
expect(freshPeripheral.connect).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(noble.startScanning).toHaveBeenCalledTimes(1);
|
|
351
|
+
expect(noble.connectAsync).toHaveBeenCalledTimes(name.startsWith('Pro2') ? 1 : 0);
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
129
355
|
test('waits for a targeted scan to stop before connecting', async () => {
|
|
130
356
|
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
131
357
|
|
|
@@ -194,13 +420,19 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
194
420
|
expect(peripheral.connect).not.toHaveBeenCalled();
|
|
195
421
|
|
|
196
422
|
stopScanningCallback?.();
|
|
197
|
-
await expect(connectPromise).
|
|
423
|
+
await expect(connectPromise).resolves.toEqual({
|
|
424
|
+
type: 'NobleBleIpcError',
|
|
425
|
+
success: false,
|
|
426
|
+
error: {
|
|
427
|
+
name: 'HardwareError',
|
|
428
|
+
message: 'expected test connection failure',
|
|
429
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
198
432
|
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
199
433
|
});
|
|
200
434
|
|
|
201
|
-
test('disconnects a
|
|
202
|
-
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
203
|
-
|
|
435
|
+
test('disconnects and settles a pending Noble connect before a late callback arrives', async () => {
|
|
204
436
|
const handlers = new Map<string, IpcHandler>();
|
|
205
437
|
const ipcMain = {
|
|
206
438
|
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
@@ -215,6 +447,7 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
215
447
|
startScanning: jest.Mock;
|
|
216
448
|
stopScanning: jest.Mock;
|
|
217
449
|
};
|
|
450
|
+
let stopScanningCallback: (() => void) | undefined;
|
|
218
451
|
let connectCallback: ((error?: Error) => void) | undefined;
|
|
219
452
|
let resolveConnectStarted = () => undefined;
|
|
220
453
|
const connectStarted = new Promise<void>(resolve => {
|
|
@@ -222,13 +455,17 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
222
455
|
});
|
|
223
456
|
const peripheral = Object.assign(
|
|
224
457
|
new EventEmitter(),
|
|
225
|
-
createPeripheral('
|
|
458
|
+
createPeripheral('pending-device', 'Pro2 C3D4'),
|
|
226
459
|
{
|
|
227
460
|
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
228
461
|
connectCallback = callback;
|
|
229
462
|
resolveConnectStarted();
|
|
230
463
|
}),
|
|
231
|
-
disconnect: jest.fn((callback: () => void) =>
|
|
464
|
+
disconnect: jest.fn((callback: () => void) => {
|
|
465
|
+
peripheral.state = 'disconnected';
|
|
466
|
+
callback();
|
|
467
|
+
}),
|
|
468
|
+
discoverServices: jest.fn(),
|
|
232
469
|
}
|
|
233
470
|
);
|
|
234
471
|
noble.state = 'poweredOn';
|
|
@@ -236,7 +473,9 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
236
473
|
callback?.();
|
|
237
474
|
noble.emit('discover', peripheral);
|
|
238
475
|
});
|
|
239
|
-
noble.stopScanning = jest.fn(callback =>
|
|
476
|
+
noble.stopScanning = jest.fn(callback => {
|
|
477
|
+
stopScanningCallback = callback;
|
|
478
|
+
});
|
|
240
479
|
|
|
241
480
|
jest.doMock('@stoprocent/noble', () => noble);
|
|
242
481
|
jest.doMock('electron', () => ({ ipcMain }));
|
|
@@ -253,20 +492,257 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
253
492
|
} as unknown as WebContents);
|
|
254
493
|
|
|
255
494
|
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
256
|
-
|
|
257
|
-
|
|
495
|
+
const disconnect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT);
|
|
496
|
+
if (!connect || !disconnect) {
|
|
497
|
+
throw new Error('Electron Noble BLE handlers were not registered');
|
|
258
498
|
}
|
|
259
499
|
|
|
260
|
-
const connectPromise = Promise.resolve(connect(undefined,
|
|
500
|
+
const connectPromise = Promise.resolve(connect(undefined, peripheral.id));
|
|
501
|
+
await Promise.resolve();
|
|
502
|
+
stopScanningCallback?.();
|
|
261
503
|
await connectStarted;
|
|
262
|
-
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
263
504
|
|
|
264
|
-
|
|
265
|
-
await expect(connectPromise).
|
|
505
|
+
await expect(Promise.resolve(disconnect(undefined, peripheral.id))).resolves.toBeUndefined();
|
|
506
|
+
await expect(connectPromise).resolves.toMatchObject({
|
|
507
|
+
type: 'NobleBleIpcError',
|
|
508
|
+
success: false,
|
|
509
|
+
error: {
|
|
510
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
266
514
|
|
|
515
|
+
peripheral.state = 'connected';
|
|
267
516
|
connectCallback?.();
|
|
268
517
|
await Promise.resolve();
|
|
269
|
-
|
|
518
|
+
|
|
519
|
+
expect(peripheral.discoverServices).not.toHaveBeenCalled();
|
|
520
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(2);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('disconnects and settles a pending forced reconnect before a late callback arrives', async () => {
|
|
524
|
+
const handlers = new Map<string, IpcHandler>();
|
|
525
|
+
const ipcMain = {
|
|
526
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
527
|
+
handlers.set(channel, handler);
|
|
528
|
+
}),
|
|
529
|
+
removeHandler: jest.fn((channel: string) => {
|
|
530
|
+
handlers.delete(channel);
|
|
531
|
+
}),
|
|
532
|
+
};
|
|
533
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
534
|
+
state: string;
|
|
535
|
+
startScanning: jest.Mock;
|
|
536
|
+
stopScanning: jest.Mock;
|
|
537
|
+
};
|
|
538
|
+
let stopScanningCallback: (() => void) | undefined;
|
|
539
|
+
const connectCallbacks: Array<(error?: Error) => void> = [];
|
|
540
|
+
let resolveInitialConnectStarted = () => undefined;
|
|
541
|
+
const initialConnectStarted = new Promise<void>(resolve => {
|
|
542
|
+
resolveInitialConnectStarted = resolve;
|
|
543
|
+
});
|
|
544
|
+
let resolveForcedReconnectStarted = () => undefined;
|
|
545
|
+
const forcedReconnectStarted = new Promise<void>(resolve => {
|
|
546
|
+
resolveForcedReconnectStarted = resolve;
|
|
547
|
+
});
|
|
548
|
+
const peripheral = Object.assign(
|
|
549
|
+
new EventEmitter(),
|
|
550
|
+
createPeripheral('forced-reconnect-device', 'Pro2 C3D4'),
|
|
551
|
+
{
|
|
552
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
553
|
+
connectCallbacks.push(callback);
|
|
554
|
+
if (connectCallbacks.length === 1) {
|
|
555
|
+
resolveInitialConnectStarted();
|
|
556
|
+
} else if (connectCallbacks.length === 2) {
|
|
557
|
+
resolveForcedReconnectStarted();
|
|
558
|
+
}
|
|
559
|
+
}),
|
|
560
|
+
disconnect: jest.fn((callback: () => void) => {
|
|
561
|
+
peripheral.state = 'disconnected';
|
|
562
|
+
callback();
|
|
563
|
+
}),
|
|
564
|
+
discoverServices: jest.fn(),
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
noble.state = 'poweredOn';
|
|
568
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
569
|
+
callback?.();
|
|
570
|
+
noble.emit('discover', peripheral);
|
|
571
|
+
});
|
|
572
|
+
noble.stopScanning = jest.fn(callback => {
|
|
573
|
+
stopScanningCallback = callback;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
577
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
578
|
+
jest.doMock('electron-log', () => ({
|
|
579
|
+
info: jest.fn(),
|
|
580
|
+
debug: jest.fn(),
|
|
581
|
+
error: jest.fn(),
|
|
582
|
+
}));
|
|
583
|
+
|
|
584
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
585
|
+
setupNobleBleHandlers({
|
|
586
|
+
on: jest.fn(),
|
|
587
|
+
send: jest.fn(),
|
|
588
|
+
} as unknown as WebContents);
|
|
589
|
+
|
|
590
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
591
|
+
const disconnect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT);
|
|
592
|
+
if (!connect || !disconnect) {
|
|
593
|
+
throw new Error('Electron Noble BLE handlers were not registered');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const connectPromise = Promise.resolve(connect(undefined, peripheral.id));
|
|
597
|
+
await Promise.resolve();
|
|
598
|
+
stopScanningCallback?.();
|
|
599
|
+
await initialConnectStarted;
|
|
600
|
+
|
|
601
|
+
peripheral.state = 'connected';
|
|
602
|
+
connectCallbacks[0]?.();
|
|
603
|
+
await forcedReconnectStarted;
|
|
604
|
+
|
|
605
|
+
await expect(Promise.resolve(disconnect(undefined, peripheral.id))).resolves.toBeUndefined();
|
|
606
|
+
await expect(connectPromise).resolves.toMatchObject({
|
|
607
|
+
type: 'NobleBleIpcError',
|
|
608
|
+
success: false,
|
|
609
|
+
error: {
|
|
610
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(2);
|
|
614
|
+
|
|
615
|
+
peripheral.state = 'connected';
|
|
616
|
+
connectCallbacks[1]?.();
|
|
617
|
+
await Promise.resolve();
|
|
618
|
+
|
|
619
|
+
expect(peripheral.discoverServices).not.toHaveBeenCalled();
|
|
620
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(3);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('maps a structured native subscription failure before returning it over IPC', async () => {
|
|
624
|
+
const handlers = new Map<string, IpcHandler>();
|
|
625
|
+
const ipcMain = {
|
|
626
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
627
|
+
handlers.set(channel, handler);
|
|
628
|
+
}),
|
|
629
|
+
removeHandler: jest.fn((channel: string) => {
|
|
630
|
+
handlers.delete(channel);
|
|
631
|
+
}),
|
|
632
|
+
};
|
|
633
|
+
const nativeError = Object.assign(new Error('Encryption is insufficient'), {
|
|
634
|
+
nativeErrorCode: 15,
|
|
635
|
+
nativeErrorDomain: 'CBATTErrorDomain',
|
|
636
|
+
});
|
|
637
|
+
const notifyCharacteristic = Object.assign(new EventEmitter(), {
|
|
638
|
+
uuid: '0003',
|
|
639
|
+
unsubscribe: jest.fn((callback: (error?: Error) => void) => callback()),
|
|
640
|
+
subscribe: jest.fn((callback: (error?: Error) => void) => callback(nativeError)),
|
|
641
|
+
});
|
|
642
|
+
const writeCharacteristic = Object.assign(new EventEmitter(), {
|
|
643
|
+
uuid: '0002',
|
|
644
|
+
});
|
|
645
|
+
const service = {
|
|
646
|
+
uuid: '0001',
|
|
647
|
+
discoverCharacteristics: jest.fn(
|
|
648
|
+
(
|
|
649
|
+
_characteristicUuids: string[],
|
|
650
|
+
callback: (error: Error | null, value: unknown[]) => void
|
|
651
|
+
) => callback(null, [writeCharacteristic, notifyCharacteristic])
|
|
652
|
+
),
|
|
653
|
+
};
|
|
654
|
+
const peripheral = Object.assign(
|
|
655
|
+
new EventEmitter(),
|
|
656
|
+
createPeripheral('subscription-device', 'Pro2 E5F6'),
|
|
657
|
+
{
|
|
658
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
659
|
+
peripheral.state = 'connected';
|
|
660
|
+
callback();
|
|
661
|
+
}),
|
|
662
|
+
disconnect: jest.fn((callback: () => void) => {
|
|
663
|
+
peripheral.state = 'disconnected';
|
|
664
|
+
callback();
|
|
665
|
+
}),
|
|
666
|
+
discoverServices: jest.fn(
|
|
667
|
+
(_serviceUuids: string[], callback: (error: Error | null, value: unknown[]) => void) =>
|
|
668
|
+
callback(null, [service])
|
|
669
|
+
),
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
const noble = Object.assign(new EventEmitter(), {
|
|
673
|
+
state: 'poweredOn',
|
|
674
|
+
startScanning: jest.fn((_services, _duplicates, callback) => {
|
|
675
|
+
callback?.();
|
|
676
|
+
noble.emit('discover', peripheral);
|
|
677
|
+
}),
|
|
678
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
682
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
683
|
+
jest.doMock('electron-log', () => ({
|
|
684
|
+
info: jest.fn(),
|
|
685
|
+
debug: jest.fn(),
|
|
686
|
+
error: jest.fn(),
|
|
687
|
+
}));
|
|
688
|
+
|
|
689
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
690
|
+
setupNobleBleHandlers({
|
|
691
|
+
on: jest.fn(),
|
|
692
|
+
send: jest.fn(),
|
|
693
|
+
} as unknown as WebContents);
|
|
694
|
+
|
|
695
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
696
|
+
const subscribe = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE);
|
|
697
|
+
const disconnect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT);
|
|
698
|
+
if (!connect || !subscribe || !disconnect) {
|
|
699
|
+
throw new Error('Electron Noble BLE handlers were not registered');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
await expect(Promise.resolve(connect(undefined, peripheral.id))).resolves.toBeUndefined();
|
|
703
|
+
try {
|
|
704
|
+
await expect(Promise.resolve(subscribe(undefined, peripheral.id))).resolves.toMatchObject({
|
|
705
|
+
type: 'NobleBleIpcError',
|
|
706
|
+
success: false,
|
|
707
|
+
error: {
|
|
708
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
709
|
+
params: {
|
|
710
|
+
nativeErrorMessage: 'Notification subscription failed: Encryption is insufficient',
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
} finally {
|
|
715
|
+
await disconnect(undefined, peripheral.id);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test('settles a pending Noble connect with the native disconnect error', () => {
|
|
720
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
721
|
+
const Noble = require('@stoprocent/noble/lib/noble');
|
|
722
|
+
const bindings = Object.assign(new EventEmitter(), {
|
|
723
|
+
connect: jest.fn(),
|
|
724
|
+
});
|
|
725
|
+
const noble = new Noble(bindings);
|
|
726
|
+
noble._registerListeners();
|
|
727
|
+
const peripheral = noble._createPeripheral(
|
|
728
|
+
'aabbccdd',
|
|
729
|
+
'',
|
|
730
|
+
'unknown',
|
|
731
|
+
true,
|
|
732
|
+
{ localName: 'Pro2 A1B2', serviceUuids: ['0001'] },
|
|
733
|
+
-50,
|
|
734
|
+
false
|
|
735
|
+
);
|
|
736
|
+
const connectCallback = jest.fn();
|
|
737
|
+
|
|
738
|
+
peripheral.connect(connectCallback);
|
|
739
|
+
const nativeError = new Error(
|
|
740
|
+
'CBErrorDomain:14 Peer removed pairing information on the device side'
|
|
741
|
+
);
|
|
742
|
+
bindings.emit('disconnect', 'aabbccdd', nativeError);
|
|
743
|
+
|
|
744
|
+
expect(connectCallback).toHaveBeenCalledWith(nativeError);
|
|
745
|
+
expect(peripheral.state).toBe('disconnected');
|
|
270
746
|
});
|
|
271
747
|
|
|
272
748
|
test('enumerates a Pro2 communication advertisement after Find My changes its name', async () => {
|
|
@@ -399,3 +875,280 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
399
875
|
]);
|
|
400
876
|
});
|
|
401
877
|
});
|
|
878
|
+
|
|
879
|
+
describe('Noble BLE process shutdown', () => {
|
|
880
|
+
const flushCallbacks = () =>
|
|
881
|
+
new Promise<void>(resolve => {
|
|
882
|
+
setImmediate(resolve);
|
|
883
|
+
});
|
|
884
|
+
afterEach(() => {
|
|
885
|
+
jest.useRealTimers();
|
|
886
|
+
jest.resetModules();
|
|
887
|
+
jest.clearAllMocks();
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const setup = async (state = 'poweredOn') => {
|
|
891
|
+
const handlers = new Map<string, IpcHandler>();
|
|
892
|
+
const native = Object.assign(new EventEmitter(), {
|
|
893
|
+
state,
|
|
894
|
+
startScanning: jest.fn((_uuids, _duplicates, callback) => callback?.()),
|
|
895
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
896
|
+
stop: jest.fn(),
|
|
897
|
+
});
|
|
898
|
+
jest.doMock('@stoprocent/noble', () => native);
|
|
899
|
+
jest.doMock('electron', () => ({
|
|
900
|
+
ipcMain: {
|
|
901
|
+
handle: (channel: string, listener: IpcHandler) => handlers.set(channel, listener),
|
|
902
|
+
removeHandler: (channel: string) => handlers.delete(channel),
|
|
903
|
+
},
|
|
904
|
+
}));
|
|
905
|
+
jest.doMock(
|
|
906
|
+
'electron-log',
|
|
907
|
+
() => ({
|
|
908
|
+
info: jest.fn(),
|
|
909
|
+
debug: jest.fn(),
|
|
910
|
+
warn: jest.fn(),
|
|
911
|
+
error: jest.fn(),
|
|
912
|
+
}),
|
|
913
|
+
{ virtual: true }
|
|
914
|
+
);
|
|
915
|
+
const sdk = await import('../noble-ble-handler');
|
|
916
|
+
const window = new EventEmitter();
|
|
917
|
+
sdk.setupNobleBleHandlers(window as unknown as WebContents);
|
|
918
|
+
return { sdk, native, window, handlers };
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
test('does not initialize native BLE when quitting before first use', async () => {
|
|
922
|
+
const { sdk, native, handlers } = await setup();
|
|
923
|
+
await sdk.disposeNobleBleSupport();
|
|
924
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
925
|
+
expect(handlers.size).toBe(0);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test('cancels an active scan, ignores its late callback and releases native once', async () => {
|
|
929
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
930
|
+
const { sdk, native, handlers } = await setup();
|
|
931
|
+
let completeScan: (() => void) | undefined;
|
|
932
|
+
native.startScanning.mockImplementation((_uuids, _duplicates, callback) => {
|
|
933
|
+
completeScan = callback;
|
|
934
|
+
});
|
|
935
|
+
const scan = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({});
|
|
936
|
+
await Promise.resolve();
|
|
937
|
+
await Promise.resolve();
|
|
938
|
+
await sdk.disposeNobleBleSupport();
|
|
939
|
+
await sdk.disposeNobleBleSupport();
|
|
940
|
+
await scan;
|
|
941
|
+
completeScan?.();
|
|
942
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
943
|
+
expect(native.listenerCount('discover')).toBe(0);
|
|
944
|
+
expect(native.listenerCount('stateChange')).toBe(0);
|
|
945
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test('cancels power-on waits before releasing native', async () => {
|
|
949
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
950
|
+
const { sdk, native, handlers } = await setup('unknown');
|
|
951
|
+
const availability = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
952
|
+
await sdk.disposeNobleBleSupport();
|
|
953
|
+
await availability;
|
|
954
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
955
|
+
expect(native.listenerCount('stateChange')).toBe(0);
|
|
956
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test('cancels native connects and waits for disconnect before release', async () => {
|
|
960
|
+
const { sdk, native, handlers } = await setup();
|
|
961
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
962
|
+
let connectCallback: ((error?: Error) => void) | undefined;
|
|
963
|
+
let disconnectCallback: (() => void) | undefined;
|
|
964
|
+
const peripheral = Object.assign(new EventEmitter(), {
|
|
965
|
+
...createPeripheral('pending-device', 'OneKey Pro'),
|
|
966
|
+
connect: jest.fn(callback => {
|
|
967
|
+
connectCallback = callback;
|
|
968
|
+
}),
|
|
969
|
+
cancelConnect: jest.fn(() => connectCallback?.(new Error('connection canceled'))),
|
|
970
|
+
disconnect: jest.fn(callback => {
|
|
971
|
+
disconnectCallback = callback;
|
|
972
|
+
}),
|
|
973
|
+
discoverServices: jest.fn(),
|
|
974
|
+
});
|
|
975
|
+
native.emit('discover', peripheral);
|
|
976
|
+
native.startScanning.mockImplementationOnce((_uuids, _duplicates, callback) => {
|
|
977
|
+
callback?.();
|
|
978
|
+
native.emit('discover', peripheral);
|
|
979
|
+
});
|
|
980
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
981
|
+
await flushCallbacks();
|
|
982
|
+
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
983
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
984
|
+
await flushCallbacks();
|
|
985
|
+
expect(peripheral.cancelConnect).toHaveBeenCalledTimes(1);
|
|
986
|
+
expect(peripheral.disconnect).toHaveBeenCalled();
|
|
987
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
988
|
+
disconnectCallback?.();
|
|
989
|
+
await disposing;
|
|
990
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
991
|
+
expect(await connecting).toMatchObject({ success: false });
|
|
992
|
+
expect(peripheral.discoverServices).not.toHaveBeenCalled();
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
test.each(['peripheral', 'direct'])(
|
|
996
|
+
'keeps late %s connects inert after the disposal timeout',
|
|
997
|
+
async route => {
|
|
998
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
999
|
+
const { sdk, native, handlers } = await setup();
|
|
1000
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
1001
|
+
let finishConnect: () => void = () => undefined;
|
|
1002
|
+
const peripheral = Object.assign(new EventEmitter(), {
|
|
1003
|
+
...createPeripheral('late-device', 'OneKey Pro'),
|
|
1004
|
+
connect: jest.fn(callback => {
|
|
1005
|
+
finishConnect = () => callback();
|
|
1006
|
+
}),
|
|
1007
|
+
cancelConnect: jest.fn(),
|
|
1008
|
+
disconnect: jest.fn(callback => callback?.()),
|
|
1009
|
+
discoverServices: jest.fn(),
|
|
1010
|
+
});
|
|
1011
|
+
const connectAsync = jest.fn(
|
|
1012
|
+
() =>
|
|
1013
|
+
new Promise(resolve => {
|
|
1014
|
+
finishConnect = () => resolve(peripheral);
|
|
1015
|
+
})
|
|
1016
|
+
);
|
|
1017
|
+
const cancelConnect = jest.fn();
|
|
1018
|
+
if (route === 'direct') Object.assign(native, { connectAsync, cancelConnect });
|
|
1019
|
+
else {
|
|
1020
|
+
native.emit('discover', peripheral);
|
|
1021
|
+
native.startScanning.mockImplementationOnce((_uuids, _duplicates, callback) => {
|
|
1022
|
+
callback?.();
|
|
1023
|
+
native.emit('discover', peripheral);
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
1027
|
+
await flushCallbacks();
|
|
1028
|
+
if (route === 'direct') {
|
|
1029
|
+
jest.advanceTimersByTime(1500);
|
|
1030
|
+
await flushCallbacks();
|
|
1031
|
+
}
|
|
1032
|
+
expect(route === 'direct' ? connectAsync : peripheral.connect).toHaveBeenCalledTimes(1);
|
|
1033
|
+
const disposing = sdk.disposeNobleBleSupport();
|
|
1034
|
+
await flushCallbacks();
|
|
1035
|
+
expect(route === 'direct' ? cancelConnect : peripheral.cancelConnect).toHaveBeenCalledTimes(
|
|
1036
|
+
1
|
|
1037
|
+
);
|
|
1038
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
1039
|
+
jest.advanceTimersByTime(3500);
|
|
1040
|
+
await disposing;
|
|
1041
|
+
const disconnects = peripheral.disconnect.mock.calls.length;
|
|
1042
|
+
peripheral.state = 'connected';
|
|
1043
|
+
finishConnect();
|
|
1044
|
+
await flushCallbacks();
|
|
1045
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(disconnects);
|
|
1046
|
+
expect(peripheral.discoverServices).not.toHaveBeenCalled();
|
|
1047
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1048
|
+
expect(await connecting).toMatchObject({ success: false });
|
|
1049
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
test('allows a host to defer shared native release until both transports are idle', async () => {
|
|
1054
|
+
const { sdk, native, handlers } = await setup();
|
|
1055
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
1056
|
+
const releaseNoble = jest.fn();
|
|
1057
|
+
await sdk.disposeNobleBleSupport(releaseNoble);
|
|
1058
|
+
expect(releaseNoble).toHaveBeenCalledWith(native);
|
|
1059
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test('replacement window scanning waits for old cleanup and preserves native', async () => {
|
|
1063
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1064
|
+
const { sdk, native, handlers, window } = await setup();
|
|
1065
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
1066
|
+
let finishOldCleanup: (() => void) | undefined;
|
|
1067
|
+
native.stopScanning.mockImplementationOnce(callback => {
|
|
1068
|
+
finishOldCleanup = callback;
|
|
1069
|
+
});
|
|
1070
|
+
window.emit('destroyed');
|
|
1071
|
+
sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
|
|
1072
|
+
const scan = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({});
|
|
1073
|
+
await flushCallbacks();
|
|
1074
|
+
expect(native.startScanning).not.toHaveBeenCalled();
|
|
1075
|
+
finishOldCleanup?.();
|
|
1076
|
+
await flushCallbacks();
|
|
1077
|
+
expect(native.startScanning).toHaveBeenCalledTimes(1);
|
|
1078
|
+
expect(native.listenerCount('discover')).toBe(1);
|
|
1079
|
+
native.emit('discover', createPeripheral('replacement-device', 'OneKey Pro'));
|
|
1080
|
+
jest.advanceTimersByTime(5000);
|
|
1081
|
+
expect(await scan).toEqual([expect.objectContaining({ id: 'replacement-device' })]);
|
|
1082
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
1083
|
+
await sdk.disposeNobleBleSupport();
|
|
1084
|
+
expect(native.stop).toHaveBeenCalledTimes(1);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test.each(['started', 'late-start-callback'])(
|
|
1088
|
+
'retires an old enumeration before a shared native scan resumes: %s',
|
|
1089
|
+
async scanState => {
|
|
1090
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
1091
|
+
const { sdk, native, handlers, window } = await setup();
|
|
1092
|
+
let lateStart: (() => void) | undefined;
|
|
1093
|
+
if (scanState === 'late-start-callback') {
|
|
1094
|
+
native.startScanning.mockImplementationOnce((_uuids, _duplicates, callback) => {
|
|
1095
|
+
lateStart = callback;
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
let oldScanSettled = false;
|
|
1099
|
+
const oldScan = Promise.resolve(
|
|
1100
|
+
handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({})
|
|
1101
|
+
).then(result => {
|
|
1102
|
+
oldScanSettled = true;
|
|
1103
|
+
return result;
|
|
1104
|
+
});
|
|
1105
|
+
await flushCallbacks();
|
|
1106
|
+
jest.advanceTimersByTime(1000);
|
|
1107
|
+
|
|
1108
|
+
let finishOldScan: (() => void) | undefined;
|
|
1109
|
+
native.stopScanning.mockImplementationOnce(callback => {
|
|
1110
|
+
finishOldScan = callback;
|
|
1111
|
+
});
|
|
1112
|
+
window.emit('destroyed');
|
|
1113
|
+
sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
|
|
1114
|
+
const replacementReady = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
1115
|
+
let replacementSettled = false;
|
|
1116
|
+
Promise.resolve(replacementReady).then(() => {
|
|
1117
|
+
replacementSettled = true;
|
|
1118
|
+
});
|
|
1119
|
+
await flushCallbacks();
|
|
1120
|
+
expect(replacementSettled).toBe(false);
|
|
1121
|
+
finishOldScan?.();
|
|
1122
|
+
await replacementReady;
|
|
1123
|
+
await flushCallbacks();
|
|
1124
|
+
expect(oldScanSettled).toBe(true);
|
|
1125
|
+
expect(await oldScan).toEqual([]);
|
|
1126
|
+
|
|
1127
|
+
// Another transport can now scan on the same process-wide Noble instance.
|
|
1128
|
+
native.startScanning([], true, () => undefined);
|
|
1129
|
+
const stopCount = native.stopScanning.mock.calls.length;
|
|
1130
|
+
lateStart?.();
|
|
1131
|
+
jest.advanceTimersByTime(5000);
|
|
1132
|
+
await flushCallbacks();
|
|
1133
|
+
expect(native.stopScanning).toHaveBeenCalledTimes(stopCount);
|
|
1134
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1135
|
+
expect(native.stop).not.toHaveBeenCalled();
|
|
1136
|
+
await sdk.disposeNobleBleSupport();
|
|
1137
|
+
}
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
test('does not start an old window enumeration after Bluetooth becomes ready', async () => {
|
|
1141
|
+
const { sdk, native, handlers, window } = await setup('unknown');
|
|
1142
|
+
const oldScan = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE)?.({});
|
|
1143
|
+
await flushCallbacks();
|
|
1144
|
+
window.emit('destroyed');
|
|
1145
|
+
sdk.setupNobleBleHandlers(new EventEmitter() as unknown as WebContents);
|
|
1146
|
+
await handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK)?.({});
|
|
1147
|
+
|
|
1148
|
+
native.state = 'poweredOn';
|
|
1149
|
+
native.emit('stateChange', 'poweredOn');
|
|
1150
|
+
expect(await oldScan).toEqual([]);
|
|
1151
|
+
expect(native.startScanning).not.toHaveBeenCalled();
|
|
1152
|
+
await sdk.disposeNobleBleSupport();
|
|
1153
|
+
});
|
|
1154
|
+
});
|