@onekeyfe/hd-transport-react-native 1.2.0-alpha.54 → 1.2.0-alpha.56

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.
@@ -0,0 +1,132 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ import { getConnectedDeviceIds } from '../BleManager';
4
+ import ReactNativeBleTransport from '../index';
5
+
6
+ jest.mock(
7
+ 'react-native',
8
+ () => ({
9
+ PermissionsAndroid: {},
10
+ Platform: { OS: 'ios' },
11
+ }),
12
+ { virtual: true }
13
+ );
14
+
15
+ jest.mock('react-native-ble-plx', () => ({
16
+ BleError: class BleError extends Error {},
17
+ BleErrorCode: {},
18
+ BleManager: jest.fn(),
19
+ ScanMode: { LowLatency: 2 },
20
+ }));
21
+
22
+ jest.mock('../BleManager', () => ({
23
+ getConnectedDeviceIds: jest.fn(),
24
+ onDeviceBondState: jest.fn(),
25
+ pairDevice: jest.fn(),
26
+ }));
27
+
28
+ jest.mock('../subscribeBleOn', () => ({
29
+ subscribeBleOn: jest.fn(() => Promise.resolve()),
30
+ }));
31
+
32
+ const ONEKEY_SERVICE_UUID = '00000001-0000-1000-8000-00805f9b34fb';
33
+
34
+ describe('ReactNativeBleTransport iOS discovery', () => {
35
+ test('filters a bonded Pro2 Find My peripheral while keeping the wallet peripheral', async () => {
36
+ jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([
37
+ {
38
+ id: 'find-my-peripheral',
39
+ name: 'Pro2 6E9E - Find My',
40
+ localName: null,
41
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
42
+ },
43
+ {
44
+ id: 'wallet-peripheral',
45
+ name: 'Pro2 6E9E',
46
+ localName: 'Pro2 6E9E',
47
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
48
+ },
49
+ ] as never);
50
+ const blePlxManager = {
51
+ startDeviceScan: jest.fn(),
52
+ stopDeviceScan: jest.fn(),
53
+ };
54
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
55
+ transport.blePlxManager = blePlxManager as never;
56
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
57
+
58
+ const devices = await transport.enumerate();
59
+
60
+ expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
61
+ });
62
+
63
+ test('filters a scanned Pro2 Find My peripheral by name when localName is null', async () => {
64
+ jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
65
+ const blePlxManager = {
66
+ startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
67
+ queueMicrotask(() => {
68
+ listener(null, {
69
+ id: 'find-my-peripheral',
70
+ name: 'Pro2 6E9E - Find My',
71
+ localName: null,
72
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
73
+ });
74
+ listener(null, {
75
+ id: 'wallet-peripheral',
76
+ name: 'Pro2 6E9E',
77
+ localName: 'Pro2 6E9E',
78
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
79
+ });
80
+ });
81
+ }),
82
+ stopDeviceScan: jest.fn(),
83
+ };
84
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
85
+ transport.blePlxManager = blePlxManager as never;
86
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
87
+
88
+ const devices = await transport.enumerate();
89
+
90
+ expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
91
+ });
92
+
93
+ test('ignores an unnamed scanned advertisement while keeping the named wallet peripheral', async () => {
94
+ jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
95
+ const blePlxManager = {
96
+ startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
97
+ queueMicrotask(() => {
98
+ listener(null, {
99
+ id: 'unnamed-peripheral',
100
+ name: null,
101
+ localName: null,
102
+ serviceUUIDs: [
103
+ '0000180a-0000-1000-8000-00805f9b34fb',
104
+ '0000180f-0000-1000-8000-00805f9b34fb',
105
+ '0000fffd-0000-1000-8000-00805f9b34fb',
106
+ ONEKEY_SERVICE_UUID,
107
+ ],
108
+ });
109
+ listener(null, {
110
+ id: 'wallet-peripheral',
111
+ name: 'Pro2 769D',
112
+ localName: 'Pro2 769D',
113
+ serviceUUIDs: [
114
+ '0000180a-0000-1000-8000-00805f9b34fb',
115
+ '0000180f-0000-1000-8000-00805f9b34fb',
116
+ '0000fffd-0000-1000-8000-00805f9b34fb',
117
+ ONEKEY_SERVICE_UUID,
118
+ ],
119
+ });
120
+ });
121
+ }),
122
+ stopDeviceScan: jest.fn(),
123
+ };
124
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
125
+ transport.blePlxManager = blePlxManager as never;
126
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
127
+
128
+ const devices = await transport.enumerate();
129
+
130
+ expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
131
+ });
132
+ });
@@ -3,7 +3,6 @@ import transportPackage, {
3
3
  PROTOCOL_V2_CHANNEL_BLE_UART,
4
4
  ProtocolV2,
5
5
  TRANSPORT_EVENT,
6
- bytesToHex,
7
6
  } from '@onekeyfe/hd-transport';
8
7
  import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
9
8
 
@@ -46,6 +45,11 @@ jest.mock('../subscribeBleOn', () => ({
46
45
  subscribeBleOn: jest.fn(() => Promise.resolve()),
47
46
  }));
48
47
 
48
+ const setPlatformOS = (os: 'ios' | 'android') => {
49
+ const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
50
+ reactNative.Platform.OS = os;
51
+ };
52
+
49
53
  const { parseConfigure } = transportPackage;
50
54
 
51
55
  const protocolV1Schema = {
@@ -69,11 +73,13 @@ const protocolV1Schema = {
69
73
 
70
74
  const protocolV2Schema = {
71
75
  nested: {
76
+ ProtocolInfoRequest: { fields: {} },
72
77
  Ping: {
73
78
  fields: {
74
79
  message: { type: 'string', id: 1 },
75
80
  },
76
81
  },
82
+ DeviceInfoGet: { fields: {} },
77
83
  FileWrite: { fields: {} },
78
84
  Success: {
79
85
  fields: {
@@ -82,8 +88,10 @@ const protocolV2Schema = {
82
88
  },
83
89
  MessageType: {
84
90
  values: {
91
+ MessageType_ProtocolInfoRequest: 60200,
85
92
  MessageType_Ping: 60206,
86
93
  MessageType_Success: 60207,
94
+ MessageType_DeviceInfoGet: 60600,
87
95
  MessageType_FileWrite: 60805,
88
96
  },
89
97
  },
@@ -95,7 +103,13 @@ const schemas = {
95
103
  protocolV2: parseConfigure(protocolV2Schema),
96
104
  };
97
105
 
98
- const createHarness = () => {
106
+ const createHarness = ({
107
+ deviceName = 'OneKey Pro 2',
108
+ isWritableWithResponse = true,
109
+ }: {
110
+ deviceName?: string;
111
+ isWritableWithResponse?: boolean;
112
+ } = {}) => {
99
113
  const uuid = 'rn-pro2-id';
100
114
  const sentSeqs: number[] = [];
101
115
  let responseSeq = 0;
@@ -134,15 +148,15 @@ const createHarness = () => {
134
148
  const writeCharacteristic = {
135
149
  uuid: '0002',
136
150
  deviceID: uuid,
137
- isWritableWithResponse: true,
151
+ isWritableWithResponse,
138
152
  isWritableWithoutResponse: true,
139
153
  writeWithResponse: jest.fn(handleWrite),
140
154
  writeWithoutResponse: jest.fn(handleWrite),
141
155
  };
142
156
  const device = {
143
157
  id: uuid,
144
- name: 'OneKey Pro 2',
145
- localName: 'OneKey Pro 2',
158
+ name: deviceName,
159
+ localName: deviceName,
146
160
  serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
147
161
  isConnected: jest.fn(() => Promise.resolve(true)),
148
162
  cancelConnection: jest.fn(() => Promise.resolve()),
@@ -184,7 +198,13 @@ const createHarness = () => {
184
198
  };
185
199
  };
186
200
 
187
- const createV1Harness = () => {
201
+ const createV1Harness = ({
202
+ respondOnWriteCount = 1,
203
+ isWritableWithResponse = true,
204
+ }: {
205
+ respondOnWriteCount?: number | number[];
206
+ isWritableWithResponse?: boolean;
207
+ } = {}) => {
188
208
  const uuid = 'rn-classic-id';
189
209
  const notifySubscriptionRemovers: jest.Mock[] = [];
190
210
  const disconnectSubscriptionRemovers: jest.Mock[] = [];
@@ -203,20 +223,25 @@ const createV1Harness = () => {
203
223
  }),
204
224
  };
205
225
  let writeCount = 0;
226
+ const responseWriteCounts = new Set(
227
+ Array.isArray(respondOnWriteCount) ? respondOnWriteCount : [respondOnWriteCount]
228
+ );
229
+ const handleWrite = () => {
230
+ writeCount += 1;
231
+ if (responseWriteCounts.has(writeCount)) {
232
+ notifyCallback?.(null, {
233
+ value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
234
+ });
235
+ }
236
+ return Promise.resolve();
237
+ };
206
238
  const writeCharacteristic = {
207
239
  uuid: '0002',
208
240
  deviceID: uuid,
209
- isWritableWithResponse: true,
241
+ isWritableWithResponse,
210
242
  isWritableWithoutResponse: true,
211
- writeWithoutResponse: jest.fn(() => {
212
- writeCount += 1;
213
- if (writeCount === 1) {
214
- notifyCallback?.(null, {
215
- value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
216
- });
217
- }
218
- return Promise.resolve();
219
- }),
243
+ writeWithResponse: jest.fn(handleWrite),
244
+ writeWithoutResponse: jest.fn(handleWrite),
220
245
  };
221
246
  const device = {
222
247
  id: uuid,
@@ -249,6 +274,7 @@ const createV1Harness = () => {
249
274
  uuid,
250
275
  device,
251
276
  bleManager,
277
+ writeCharacteristic,
252
278
  notifySubscriptionRemovers,
253
279
  disconnectSubscriptionRemovers,
254
280
  };
@@ -302,12 +328,81 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
302
328
  expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
303
329
  });
304
330
 
331
+ test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
332
+ const { transport, uuid, writeCharacteristic } = createV1Harness({
333
+ respondOnWriteCount: [1, 2],
334
+ });
335
+
336
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
337
+ uuid,
338
+ protocolType: 'V1',
339
+ });
340
+ const releaseNative = jest.spyOn(transport as any, 'releaseNative');
341
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
342
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
343
+
344
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
345
+ await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
346
+
347
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
348
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
349
+ expect(releaseNative).not.toHaveBeenCalled();
350
+ await transport.release(uuid, true);
351
+ });
352
+
353
+ test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
354
+ const { transport, uuid, writeCharacteristic } = createV1Harness({
355
+ isWritableWithResponse: false,
356
+ });
357
+
358
+ await transport.acquire({ uuid });
359
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
360
+
361
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
362
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
363
+ await transport.release(uuid, true);
364
+ });
365
+
366
+ test('does not resend a failed iOS Protocol V1 control write without response', async () => {
367
+ const { transport, uuid, writeCharacteristic } = createV1Harness();
368
+ const writeError = new Error('write with response failed');
369
+
370
+ await transport.acquire({ uuid });
371
+ writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
372
+
373
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
374
+ errorCode: HardwareErrorCode.BleWriteCharacteristicError,
375
+ });
376
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
377
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
378
+ await transport.release(uuid, true);
379
+ });
380
+
381
+ test('keeps the first Core command as the first iOS BLE request for a Protocol V2 device', async () => {
382
+ const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
383
+ deviceName: 'Pro2 6E9E',
384
+ });
385
+
386
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
387
+ uuid,
388
+ protocolType: 'V2',
389
+ });
390
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
391
+
392
+ await expect(
393
+ transport.call(uuid, 'Ping', { message: 'first-core-command' })
394
+ ).resolves.toBeDefined();
395
+ expect(sentSeqs).toEqual([1]);
396
+ await transport.release(uuid, true);
397
+ });
398
+
305
399
  test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
400
+ setPlatformOS('android');
306
401
  const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
307
402
  createV1Harness();
308
403
  const probeProtocolV2 = jest
309
404
  .spyOn(transport as any, 'probeProtocolV2')
310
- .mockImplementationOnce(async () => {
405
+ .mockImplementation(async () => {
311
406
  await (transport as any).releaseNative(uuid, true);
312
407
  return false;
313
408
  });
@@ -333,8 +428,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
333
428
  });
334
429
 
335
430
  test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
431
+ setPlatformOS('android');
336
432
  const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
337
- jest.spyOn(transport as any, 'probeProtocolV2').mockImplementationOnce(async () => {
433
+ jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
338
434
  await (transport as any).releaseNative(uuid, true);
339
435
  return false;
340
436
  });
@@ -353,7 +449,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
353
449
  });
354
450
 
355
451
  test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
356
- const { transport, uuid, device } = createV1Harness();
452
+ const { transport, uuid, device } = createV1Harness({
453
+ respondOnWriteCount: Number.POSITIVE_INFINITY,
454
+ });
357
455
 
358
456
  await transport.acquire({ uuid, expectedProtocol: 'V1' });
359
457
  await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
@@ -365,17 +463,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
365
463
  });
366
464
 
367
465
  afterEach(() => {
466
+ setPlatformOS('ios');
368
467
  resetProtocolV2BleTuning();
369
468
  });
370
469
 
371
- test('keeps the Protocol V2 sequence across probe and the next call', async () => {
470
+ test('starts the Protocol V2 sequence with the first Core call on iOS', async () => {
372
471
  const { transport, uuid, sentSeqs } = createHarness();
373
472
 
374
473
  await transport.acquire({ uuid });
375
- await transport.call(uuid, 'Ping', { message: 'after-probe' });
474
+ await transport.call(uuid, 'Ping', { message: 'first-core-command' });
376
475
 
377
- expect(sentSeqs).toEqual([1, 2]);
378
- expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
476
+ expect(sentSeqs).toEqual([1]);
379
477
  await transport.release(uuid, true);
380
478
  });
381
479
 
@@ -386,8 +484,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
386
484
  harness.setShouldRespond(false);
387
485
 
388
486
  const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
389
- while (sentSeqs.length < 2) {
390
- await Promise.resolve();
487
+ while (sentSeqs.length < 1) {
488
+ await new Promise(resolve => {
489
+ setTimeout(resolve, 0);
490
+ });
391
491
  }
392
492
  await new Promise(resolve => {
393
493
  setTimeout(resolve, 0);
@@ -403,30 +503,135 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
403
503
  const { transport, uuid, sentSeqs } = createHarness();
404
504
 
405
505
  await transport.acquire({ uuid });
506
+ await transport.call(uuid, 'Ping', { message: 'first-generation' });
406
507
  await transport.release(uuid, true);
407
508
  await transport.acquire({ uuid });
509
+ await transport.call(uuid, 'Ping', { message: 'second-generation' });
408
510
 
409
511
  expect(sentSeqs).toEqual([1, 2]);
410
512
  await transport.release(uuid, true);
411
513
  });
412
514
 
413
- test('uses withoutResponse for normal and high-volume calls', async () => {
515
+ test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
414
516
  const { transport, uuid, writeCharacteristic } = createHarness();
415
517
 
416
518
  await transport.acquire({ uuid });
417
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
519
+ const releaseNative = jest.spyOn(transport as any, 'releaseNative');
520
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
418
521
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
419
522
 
420
- await transport.call(uuid, 'Ping', { message: 'normal' });
421
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
422
- expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
523
+ await transport.call(uuid, 'DeviceInfoGet', {});
524
+ await transport.call(uuid, 'ProtocolInfoRequest', {});
525
+
526
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
527
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
528
+ expect(releaseNative).not.toHaveBeenCalled();
529
+
530
+ await transport.release(uuid, true);
531
+ });
532
+
533
+ test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
534
+ const { transport, uuid, writeCharacteristic } = createHarness();
535
+
536
+ await transport.acquire({ uuid });
423
537
 
424
538
  await transport.call(uuid, 'FileWrite', {});
425
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
539
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
540
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
541
+ await transport.release(uuid, true);
542
+ });
543
+
544
+ test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
545
+ const { transport, uuid, writeCharacteristic } = createHarness();
546
+
547
+ await transport.acquire({ uuid });
548
+
549
+ await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
550
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
551
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
552
+ await transport.release(uuid, true);
553
+ });
554
+
555
+ test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
556
+ const { transport, uuid, writeCharacteristic } = createHarness({
557
+ isWritableWithResponse: false,
558
+ });
559
+
560
+ await transport.acquire({ uuid });
561
+ await transport.call(uuid, 'ProtocolInfoRequest', {});
562
+
426
563
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
564
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
427
565
  await transport.release(uuid, true);
428
566
  });
429
567
 
568
+ test('does not resend a failed iOS Protocol V2 control write without response', async () => {
569
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
570
+ const writeError = new Error('write with response failed');
571
+ const writeWithResponse = jest.fn().mockRejectedValue(writeError);
572
+ const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
573
+ const context = {
574
+ messageName: 'ProtocolInfoRequest',
575
+ timeoutMs: 1000,
576
+ highVolume: false,
577
+ generation: 1,
578
+ signal: new AbortController().signal,
579
+ };
580
+
581
+ await expect(
582
+ transport.writeProtocolV2Packet(
583
+ {
584
+ writeCharacteristic: {
585
+ isWritableWithResponse: true,
586
+ writeWithResponse,
587
+ writeWithoutResponse,
588
+ },
589
+ },
590
+ Buffer.from('control').toString('base64'),
591
+ context,
592
+ jest.fn()
593
+ )
594
+ ).rejects.toBe(writeError);
595
+ expect(writeWithResponse).toHaveBeenCalledTimes(1);
596
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
597
+ });
598
+
599
+ test('paces a one-packet Protocol V2 control write on iOS', async () => {
600
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
601
+ const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
602
+ const bleTransport = {
603
+ mtuSize: 23,
604
+ writeCharacteristic: { writeWithoutResponse },
605
+ };
606
+ const context = {
607
+ messageName: 'ProtocolInfoRequest',
608
+ timeoutMs: 1000,
609
+ highVolume: false,
610
+ generation: 1,
611
+ signal: new AbortController().signal,
612
+ };
613
+ configureProtocolV2BleTuning({ iosPacketLength: 20 });
614
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
615
+
616
+ try {
617
+ const call = transport.writeProtocolV2Frame(
618
+ bleTransport,
619
+ new Uint8Array(10),
620
+ context,
621
+ jest.fn()
622
+ );
623
+
624
+ await Promise.resolve();
625
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
626
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
627
+
628
+ await call;
629
+ expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
630
+ } finally {
631
+ setTimeoutSpy.mockRestore();
632
+ }
633
+ });
634
+
430
635
  test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
431
636
  const harness = createHarness();
432
637
  const { transport, uuid, sentSeqs } = harness;
@@ -434,8 +639,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
434
639
  harness.setShouldRespond(false);
435
640
 
436
641
  const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
437
- while (sentSeqs.length < 2) {
438
- await Promise.resolve();
642
+ while (sentSeqs.length < 1) {
643
+ await new Promise(resolve => {
644
+ setTimeout(resolve, 0);
645
+ });
439
646
  }
440
647
 
441
648
  const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
@@ -482,7 +689,6 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
482
689
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
483
690
 
484
691
  await transport.writeProtocolV2Frame(
485
- 'device-uuid',
486
692
  bleTransport,
487
693
  new Uint8Array(30),
488
694
  context,
@@ -514,13 +720,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
514
720
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
515
721
 
516
722
  await expect(
517
- transport.writeProtocolV2Frame(
518
- 'device-uuid',
519
- bleTransport,
520
- new Uint8Array(30),
521
- context,
522
- jest.fn()
523
- )
723
+ transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
524
724
  ).rejects.toMatchObject({ errorCode: 205 });
525
725
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
526
726
  });