@react-native-harness/platform-android 1.1.0-rc.4 → 1.2.0-rc.1

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.
@@ -12,11 +12,13 @@ import {
12
12
  hasAvd,
13
13
  installApp,
14
14
  startEmulator,
15
+ uninstallApp,
15
16
  waitForBoot,
16
17
  waitForEmulatorDisconnect,
17
18
  } from '../adb.js';
18
19
  import * as tools from '@react-native-harness/tools';
19
20
  import * as environment from '../environment.js';
21
+ import * as avdConfig from '../avd-config.js';
20
22
 
21
23
  const createAbortError = () =>
22
24
  new DOMException('The operation was aborted', 'AbortError');
@@ -48,7 +50,7 @@ describe('getStartAppArgs', () => {
48
50
  user_id: 42,
49
51
  mode: 'debug',
50
52
  },
51
- })
53
+ }),
52
54
  ).toEqual([
53
55
  'shell',
54
56
  'am',
@@ -77,7 +79,7 @@ describe('getStartAppArgs', () => {
77
79
  extras: {
78
80
  count: Number.MAX_SAFE_INTEGER + 1,
79
81
  },
80
- })
82
+ }),
81
83
  ).toThrow('must be a safe integer');
82
84
  });
83
85
 
@@ -88,7 +90,7 @@ describe('getStartAppArgs', () => {
88
90
  } as Awaited<ReturnType<typeof tools.spawn>>);
89
91
 
90
92
  await expect(getAppUid('emulator-5554', 'com.example.app')).resolves.toBe(
91
- 10234
93
+ 10234,
92
94
  );
93
95
 
94
96
  expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
@@ -108,7 +110,7 @@ describe('getStartAppArgs', () => {
108
110
  } as Awaited<ReturnType<typeof tools.spawn>>);
109
111
 
110
112
  await expect(getLogcatTimestamp('emulator-5554')).resolves.toBe(
111
- '03-12 11:35:08.000'
113
+ '03-12 11:35:08.000',
112
114
  );
113
115
 
114
116
  expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
@@ -145,15 +147,35 @@ describe('getStartAppArgs', () => {
145
147
  ]);
146
148
  });
147
149
 
150
+ it('uninstalls the app via adb', async () => {
151
+ const spawnSpy = vi
152
+ .spyOn(tools, 'spawn')
153
+ .mockResolvedValueOnce({} as Awaited<ReturnType<typeof tools.spawn>>);
154
+
155
+ await uninstallApp('emulator-5554', 'com.example.app');
156
+
157
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
158
+ '-s',
159
+ 'emulator-5554',
160
+ 'uninstall',
161
+ 'com.example.app',
162
+ ]);
163
+ });
164
+
148
165
  it('creates an AVD and appends config overrides', async () => {
166
+ vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({});
149
167
  const spawnSpy = vi
150
168
  .spyOn(tools, 'spawn')
151
- .mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>);
169
+ .mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>)
170
+ .mockResolvedValueOnce({
171
+ stdout:
172
+ 'id: 0 or "pixel_8"\n Name: Pixel 8\nid: 1 or "pixel_6"\n Name: Pixel 6\n',
173
+ } as Awaited<ReturnType<typeof tools.spawn>>);
152
174
  const verifyAndroidEmulatorSdk = vi
153
175
  .spyOn(environment, 'ensureAndroidSdkPackages')
154
176
  .mockResolvedValue('/tmp/android-sdk');
155
177
  vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
156
- 'x86_64'
178
+ 'x86_64',
157
179
  );
158
180
 
159
181
  await createAvd({
@@ -170,29 +192,128 @@ describe('getStartAppArgs', () => {
170
192
  'platforms;android-35',
171
193
  'system-images;android-35;default;x86_64',
172
194
  ]);
173
- expect(spawnSpy).toHaveBeenNthCalledWith(1, 'bash', [
195
+ expect(spawnSpy).toHaveBeenNthCalledWith(
196
+ 1,
197
+ expect.stringMatching(/avdmanager$/),
198
+ ['list', 'device'],
199
+ );
200
+ expect(spawnSpy).toHaveBeenNthCalledWith(2, 'bash', [
174
201
  '-lc',
175
202
  expect.stringContaining(
176
- 'create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8"'
203
+ 'create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8" -p ',
177
204
  ),
178
205
  ]);
179
- expect(spawnSpy).toHaveBeenNthCalledWith(2, 'bash', [
206
+ expect(spawnSpy).toHaveBeenNthCalledWith(3, 'bash', [
207
+ '-lc',
208
+ expect.stringContaining(`'avd.ini.encoding=UTF-8' 'path=`),
209
+ ]);
210
+ expect(spawnSpy).toHaveBeenNthCalledWith(4, 'bash', [
180
211
  '-lc',
181
212
  expect.stringContaining(
182
- `'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `
213
+ `'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `,
183
214
  ),
184
215
  ]);
185
216
  });
186
217
 
187
- it('creates an AVD with arm64 system image packages on arm64 hosts', async () => {
188
- vi.spyOn(tools, 'spawn').mockResolvedValue(
189
- {} as Awaited<ReturnType<typeof tools.spawn>>
218
+ it('recreates the companion ini file when avdmanager leaves it missing', async () => {
219
+ vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({});
220
+ const spawnSpy = vi
221
+ .spyOn(tools, 'spawn')
222
+ .mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>)
223
+ .mockResolvedValueOnce({
224
+ stdout: 'id: 0 or "pixel_6"\n Name: Pixel 6\n',
225
+ } as Awaited<ReturnType<typeof tools.spawn>>);
226
+ vi.spyOn(environment, 'ensureAndroidSdkPackages').mockResolvedValue(
227
+ '/tmp/android-sdk',
228
+ );
229
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
230
+ 'x86_64',
231
+ );
232
+
233
+ await createAvd({
234
+ name: 'Pixel_6_API_35',
235
+ apiLevel: 35,
236
+ profile: 'pixel_6',
237
+ diskSize: '1G',
238
+ heapSize: '1G',
239
+ });
240
+
241
+ expect(spawnSpy).toHaveBeenNthCalledWith(3, 'bash', [
242
+ '-lc',
243
+ expect.stringContaining(`'avd.ini.encoding=UTF-8' 'path=`),
244
+ ]);
245
+ expect(spawnSpy).toHaveBeenNthCalledWith(4, 'bash', [
246
+ '-lc',
247
+ expect.stringContaining(
248
+ `'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `,
249
+ ),
250
+ ]);
251
+ });
252
+
253
+ it('throws an actionable error when the requested AVD profile is unavailable', async () => {
254
+ vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
255
+ stdout:
256
+ 'id: 0 or "pixel_8"\n Name: Pixel 8\nid: 1 or "pixel_6"\n Name: Pixel 6\n',
257
+ } as Awaited<ReturnType<typeof tools.spawn>>);
258
+ vi.spyOn(environment, 'ensureAndroidSdkPackages').mockResolvedValue(
259
+ '/tmp/android-sdk',
260
+ );
261
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
262
+ 'x86_64',
263
+ );
264
+
265
+ await expect(
266
+ createAvd({
267
+ name: 'Pixel_8_Pro_API_35',
268
+ apiLevel: 35,
269
+ profile: 'pixel_8_pro',
270
+ diskSize: '1G',
271
+ heapSize: '1G',
272
+ }),
273
+ ).rejects.toThrow(
274
+ 'Android AVD profile "pixel_8_pro" is not available on this machine. Available profiles: pixel_8, pixel_6',
275
+ );
276
+ });
277
+
278
+ it('throws a clear error when avdmanager does not create config.ini', async () => {
279
+ vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue(null);
280
+ vi.spyOn(tools, 'spawn')
281
+ .mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>)
282
+ .mockResolvedValueOnce({
283
+ stdout: 'id: 0 or "pixel_6"\n Name: Pixel 6\n',
284
+ } as Awaited<ReturnType<typeof tools.spawn>>);
285
+ vi.spyOn(environment, 'ensureAndroidSdkPackages').mockResolvedValue(
286
+ '/tmp/android-sdk',
287
+ );
288
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
289
+ 'x86_64',
290
+ );
291
+
292
+ await expect(
293
+ createAvd({
294
+ name: 'Pixel_6_API_35',
295
+ apiLevel: 35,
296
+ profile: 'pixel_6',
297
+ diskSize: '1G',
298
+ heapSize: '1G',
299
+ }),
300
+ ).rejects.toThrow(
301
+ 'Android AVD "Pixel_6_API_35" was created, but config.ini was not found at ',
190
302
  );
303
+ });
304
+
305
+ it('creates an AVD with arm64 system image packages on arm64 hosts', async () => {
306
+ vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({});
307
+ vi.spyOn(tools, 'spawn')
308
+ .mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>)
309
+ .mockResolvedValueOnce({
310
+ stdout: 'id: 0 or "pixel_8"\n Name: Pixel 8\n',
311
+ } as Awaited<ReturnType<typeof tools.spawn>>);
191
312
  const ensureAndroidSdkPackages = vi
192
313
  .spyOn(environment, 'ensureAndroidSdkPackages')
193
314
  .mockResolvedValue('/tmp/android-sdk');
194
315
  vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
195
- 'arm64-v8a'
316
+ 'arm64-v8a',
196
317
  );
197
318
 
198
319
  await createAvd({
@@ -241,7 +362,7 @@ describe('getStartAppArgs', () => {
241
362
  child.emit('close', 1, null);
242
363
 
243
364
  await expect(startPromise).rejects.toThrow(
244
- 'Unknown AVD name [Pixel_8_API_35]'
365
+ 'Unknown AVD name [Pixel_8_API_35]',
245
366
  );
246
367
  });
247
368
 
@@ -271,7 +392,7 @@ describe('getStartAppArgs', () => {
271
392
  child.emit('close', 1, null);
272
393
 
273
394
  await expect(startPromise).rejects.toThrow(
274
- 'emulator: panic: broken config'
395
+ 'emulator: panic: broken config',
275
396
  );
276
397
  });
277
398
 
@@ -291,7 +412,7 @@ describe('getStartAppArgs', () => {
291
412
  vi.spyOn(emulatorProcess, 'startDetachedProcess').mockReturnValue(
292
413
  child as unknown as ReturnType<
293
414
  typeof emulatorProcess.startDetachedProcess
294
- >
415
+ >,
295
416
  );
296
417
 
297
418
  const startPromise = startEmulator('Pixel_8_API_35');
@@ -317,7 +438,7 @@ describe('getStartAppArgs', () => {
317
438
  .mockReturnValue(
318
439
  child as unknown as ReturnType<
319
440
  typeof emulatorProcess.startDetachedProcess
320
- >
441
+ >,
321
442
  );
322
443
 
323
444
  const startPromise = startEmulator('Pixel_8_API_35');
@@ -326,7 +447,7 @@ describe('getStartAppArgs', () => {
326
447
 
327
448
  expect(startDetachedProcess).toHaveBeenCalledWith(
328
449
  expect.stringMatching(/emulator$/),
329
- expect.arrayContaining(['-no-snapshot-load', '-no-snapshot-save'])
450
+ expect.arrayContaining(['-no-snapshot-load', '-no-snapshot-save']),
330
451
  );
331
452
  });
332
453
 
@@ -345,22 +466,22 @@ describe('getStartAppArgs', () => {
345
466
  .mockReturnValue(
346
467
  child as unknown as ReturnType<
347
468
  typeof emulatorProcess.startDetachedProcess
348
- >
469
+ >,
349
470
  );
350
471
 
351
472
  const startPromise = startEmulator(
352
473
  'Pixel_8_API_35',
353
- 'clean-snapshot-generation'
474
+ 'clean-snapshot-generation',
354
475
  );
355
476
  await vi.runAllTimersAsync();
356
477
  await startPromise;
357
478
 
358
479
  expect(startDetachedProcess).toHaveBeenCalledWith(
359
480
  expect.stringMatching(/emulator$/),
360
- expect.arrayContaining(['-no-snapshot-load'])
481
+ expect.arrayContaining(['-no-snapshot-load']),
361
482
  );
362
483
  expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain(
363
- '-no-snapshot-save'
484
+ '-no-snapshot-save',
364
485
  );
365
486
  });
366
487
 
@@ -379,7 +500,7 @@ describe('getStartAppArgs', () => {
379
500
  .mockReturnValue(
380
501
  child as unknown as ReturnType<
381
502
  typeof emulatorProcess.startDetachedProcess
382
- >
503
+ >,
383
504
  );
384
505
 
385
506
  const startPromise = startEmulator('Pixel_8_API_35', 'snapshot-reuse');
@@ -388,10 +509,10 @@ describe('getStartAppArgs', () => {
388
509
 
389
510
  expect(startDetachedProcess).toHaveBeenCalledWith(
390
511
  expect.stringMatching(/emulator$/),
391
- expect.arrayContaining(['-no-snapshot-save'])
512
+ expect.arrayContaining(['-no-snapshot-save']),
392
513
  );
393
514
  expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain(
394
- '-no-snapshot-load'
515
+ '-no-snapshot-load',
395
516
  );
396
517
  });
397
518
 
@@ -459,7 +580,7 @@ describe('getStartAppArgs', () => {
459
580
 
460
581
  const waitPromise = waitForBoot(
461
582
  'Pixel_8_API_35',
462
- new AbortController().signal
583
+ new AbortController().signal,
463
584
  );
464
585
 
465
586
  await vi.advanceTimersByTimeAsync(1000);
@@ -482,7 +603,7 @@ describe('getStartAppArgs', () => {
482
603
 
483
604
  const waitPromise = waitForEmulatorDisconnect(
484
605
  'emulator-5554',
485
- new AbortController().signal
606
+ new AbortController().signal,
486
607
  );
487
608
 
488
609
  await vi.advanceTimersByTimeAsync(1000);
@@ -490,4 +611,90 @@ describe('getStartAppArgs', () => {
490
611
  await expect(waitPromise).resolves.toBeUndefined();
491
612
  expect(spawnSpy).toHaveBeenCalledTimes(2);
492
613
  });
614
+
615
+ it('grants requested dangerous permissions to an app', async () => {
616
+ const { grantPermissions } = await import('../adb.js');
617
+ const spawnSpy = vi
618
+ .spyOn(tools, 'spawn')
619
+ .mockResolvedValueOnce({
620
+ stdout: 'package:com.example.app\n',
621
+ } as Awaited<ReturnType<typeof tools.spawn>>)
622
+ .mockResolvedValueOnce({
623
+ stdout: `requested permissions:\n android.permission.CAMERA\n android.permission.INTERNET\n android.permission.ACCESS_FINE_LOCATION\ninstall permissions:\n`,
624
+ } as Awaited<ReturnType<typeof tools.spawn>>)
625
+ .mockResolvedValueOnce({
626
+ stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n permission:android.permission.ACCESS_FINE_LOCATION\n`,
627
+ } as Awaited<ReturnType<typeof tools.spawn>>)
628
+ .mockResolvedValue({
629
+ stdout: '',
630
+ } as Awaited<ReturnType<typeof tools.spawn>>);
631
+
632
+ await grantPermissions('emulator-5554', 'com.example.app');
633
+
634
+ expect(spawnSpy).toHaveBeenCalledTimes(5);
635
+ expect(spawnSpy).toHaveBeenNthCalledWith(1, expect.any(String), [
636
+ '-s',
637
+ 'emulator-5554',
638
+ 'shell',
639
+ 'pm',
640
+ 'list',
641
+ 'packages',
642
+ 'com.example.app',
643
+ ]);
644
+ expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [
645
+ '-s',
646
+ 'emulator-5554',
647
+ 'shell',
648
+ 'dumpsys',
649
+ 'package',
650
+ 'com.example.app',
651
+ ]);
652
+ expect(spawnSpy).toHaveBeenNthCalledWith(3, expect.any(String), [
653
+ '-s',
654
+ 'emulator-5554',
655
+ 'shell',
656
+ 'pm',
657
+ 'list',
658
+ 'permissions',
659
+ '-g',
660
+ '-d',
661
+ ]);
662
+ expect(spawnSpy).toHaveBeenNthCalledWith(4, expect.any(String), [
663
+ '-s',
664
+ 'emulator-5554',
665
+ 'shell',
666
+ 'pm',
667
+ 'grant',
668
+ 'com.example.app',
669
+ 'android.permission.CAMERA',
670
+ ]);
671
+ expect(spawnSpy).toHaveBeenNthCalledWith(5, expect.any(String), [
672
+ '-s',
673
+ 'emulator-5554',
674
+ 'shell',
675
+ 'pm',
676
+ 'grant',
677
+ 'com.example.app',
678
+ 'android.permission.ACCESS_FINE_LOCATION',
679
+ ]);
680
+ });
681
+
682
+ it('does nothing when the app has no grantable dangerous permissions', async () => {
683
+ const { grantPermissions } = await import('../adb.js');
684
+ const spawnSpy = vi
685
+ .spyOn(tools, 'spawn')
686
+ .mockResolvedValueOnce({
687
+ stdout: 'package:com.example.app\n',
688
+ } as Awaited<ReturnType<typeof tools.spawn>>)
689
+ .mockResolvedValueOnce({
690
+ stdout: `requested permissions:\n android.permission.INTERNET\ninstall permissions:\n`,
691
+ } as Awaited<ReturnType<typeof tools.spawn>>)
692
+ .mockResolvedValueOnce({
693
+ stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n`,
694
+ } as Awaited<ReturnType<typeof tools.spawn>>);
695
+
696
+ await grantPermissions('emulator-5554', 'com.example.app');
697
+
698
+ expect(spawnSpy).toHaveBeenCalledTimes(3);
699
+ });
493
700
  });
@@ -10,6 +10,9 @@ describe('emulator startup modes', () => {
10
10
  '-no-snapshot-save',
11
11
  ])
12
12
  );
13
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual(
14
+ expect.arrayContaining(['-camera-back', 'none'])
15
+ );
13
16
  });
14
17
 
15
18
  it('builds clean snapshot generation args', () => {
@@ -429,6 +429,60 @@ describe('Android platform instance', () => {
429
429
  fs.rmSync(appPath, { force: true });
430
430
  });
431
431
 
432
+ it('reinstalls the app from HARNESS_APP_PATH when already installed', async () => {
433
+ const appPath = path.join(os.tmpdir(), 'HarnessPlayground-installed.apk');
434
+ fs.writeFileSync(appPath, 'apk');
435
+ vi.stubEnv('HARNESS_APP_PATH', appPath);
436
+ vi.spyOn(
437
+ await import('../environment.js'),
438
+ 'ensureAndroidEmulatorEnvironment',
439
+ ).mockResolvedValue('/tmp/android-sdk');
440
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
441
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
442
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
443
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
444
+ const uninstallApp = vi
445
+ .spyOn(adb, 'uninstallApp')
446
+ .mockResolvedValue(undefined);
447
+ const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
448
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
449
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
450
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
451
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
452
+ undefined,
453
+ );
454
+
455
+ await expect(
456
+ getAndroidEmulatorPlatformInstance(
457
+ {
458
+ name: 'android',
459
+ device: {
460
+ type: 'emulator',
461
+ name: 'Pixel_8_API_35',
462
+ avd: {
463
+ apiLevel: 35,
464
+ profile: 'pixel_8',
465
+ diskSize: '1G',
466
+ heapSize: '1G',
467
+ },
468
+ },
469
+ bundleId: 'com.harnessplayground',
470
+ activityName: '.MainActivity',
471
+ },
472
+ harnessConfig,
473
+ init,
474
+ ),
475
+ ).resolves.toBeDefined();
476
+
477
+ expect(uninstallApp).toHaveBeenCalledWith(
478
+ 'emulator-5554',
479
+ 'com.harnessplayground',
480
+ );
481
+ expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
482
+
483
+ fs.rmSync(appPath, { force: true });
484
+ });
485
+
432
486
  it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
433
487
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
434
488
  vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
@@ -607,4 +661,138 @@ describe('Android platform instance', () => {
607
661
  expect(appMonitor.addListener(listener)).toBeUndefined();
608
662
  expect(appMonitor.removeListener(listener)).toBeUndefined();
609
663
  });
664
+
665
+ it('grants permissions when permissions are enabled for emulator', async () => {
666
+ vi.spyOn(
667
+ await import('../environment.js'),
668
+ 'ensureAndroidEmulatorEnvironment',
669
+ ).mockResolvedValue('/tmp/android-sdk');
670
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
671
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
672
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
673
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
674
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
675
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
676
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
677
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
678
+ undefined,
679
+ );
680
+ const grantPermissions = vi
681
+ .spyOn(adb, 'grantPermissions')
682
+ .mockResolvedValue(undefined);
683
+
684
+ const harnessConfigWithPermissions = {
685
+ ...harnessConfig,
686
+ permissions: true,
687
+ } as HarnessConfig;
688
+
689
+ await getAndroidEmulatorPlatformInstance(
690
+ {
691
+ name: 'android',
692
+ device: {
693
+ type: 'emulator',
694
+ name: 'Pixel_8_API_35',
695
+ avd: {
696
+ apiLevel: 35,
697
+ profile: 'pixel_8',
698
+ diskSize: '1G',
699
+ heapSize: '1G',
700
+ },
701
+ },
702
+ bundleId: 'com.harnessplayground',
703
+ activityName: '.MainActivity',
704
+ },
705
+ harnessConfigWithPermissions,
706
+ init,
707
+ );
708
+
709
+ expect(grantPermissions).toHaveBeenCalledWith(
710
+ 'emulator-5554',
711
+ 'com.harnessplayground',
712
+ );
713
+ });
714
+
715
+ it('does not grant permissions when permissions are disabled for emulator', async () => {
716
+ vi.spyOn(
717
+ await import('../environment.js'),
718
+ 'ensureAndroidEmulatorEnvironment',
719
+ ).mockResolvedValue('/tmp/android-sdk');
720
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
721
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
722
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
723
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
724
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
725
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
726
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
727
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
728
+ undefined,
729
+ );
730
+ const grantPermissions = vi
731
+ .spyOn(adb, 'grantPermissions')
732
+ .mockResolvedValue(undefined);
733
+
734
+ await getAndroidEmulatorPlatformInstance(
735
+ {
736
+ name: 'android',
737
+ device: {
738
+ type: 'emulator',
739
+ name: 'Pixel_8_API_35',
740
+ avd: {
741
+ apiLevel: 35,
742
+ profile: 'pixel_8',
743
+ diskSize: '1G',
744
+ heapSize: '1G',
745
+ },
746
+ },
747
+ bundleId: 'com.harnessplayground',
748
+ activityName: '.MainActivity',
749
+ },
750
+ harnessConfig,
751
+ init,
752
+ );
753
+
754
+ expect(grantPermissions).not.toHaveBeenCalled();
755
+ });
756
+
757
+ it('grants permissions when permissions are enabled for physical device', async () => {
758
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
759
+ vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
760
+ manufacturer: 'motorola',
761
+ model: 'moto g72',
762
+ });
763
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
764
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
765
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
766
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
767
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
768
+ undefined,
769
+ );
770
+ const grantPermissions = vi
771
+ .spyOn(adb, 'grantPermissions')
772
+ .mockResolvedValue(undefined);
773
+
774
+ const harnessConfigWithPermissions = {
775
+ ...harnessConfig,
776
+ permissions: true,
777
+ } as HarnessConfig;
778
+
779
+ await getAndroidPhysicalDevicePlatformInstance(
780
+ {
781
+ name: 'android-device',
782
+ device: {
783
+ type: 'physical',
784
+ manufacturer: 'motorola',
785
+ model: 'moto g72',
786
+ },
787
+ bundleId: 'com.harnessplayground',
788
+ activityName: '.MainActivity',
789
+ },
790
+ harnessConfigWithPermissions,
791
+ );
792
+
793
+ expect(grantPermissions).toHaveBeenCalledWith(
794
+ '012345',
795
+ 'com.harnessplayground',
796
+ );
797
+ });
610
798
  });
@@ -0,0 +1,47 @@
1
+ export class AdbError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'AdbError';
5
+ }
6
+ }
7
+
8
+ export class AdbDeviceNotFoundError extends AdbError {
9
+ constructor(adbId: string) {
10
+ super(
11
+ `Android device "${adbId}" not found or not connected. ` +
12
+ `Run "adb devices" to see available devices.`
13
+ );
14
+ this.name = 'AdbDeviceNotFoundError';
15
+ }
16
+ }
17
+
18
+ export class AdbAppNotInstalledError extends AdbError {
19
+ constructor(bundleId: string, adbId: string) {
20
+ super(
21
+ `App "${bundleId}" is not installed on device "${adbId}". ` +
22
+ `Install the app before running tests.`
23
+ );
24
+ this.name = 'AdbAppNotInstalledError';
25
+ }
26
+ }
27
+
28
+ export class AdbPermissionGrantError extends AdbError {
29
+ constructor(bundleId: string, permissions: string[], adbId: string) {
30
+ const permissionList = permissions.join(', ');
31
+ super(
32
+ `Failed to grant permissions [${permissionList}] to "${bundleId}" on device "${adbId}". ` +
33
+ `Verify the app is installed and the device supports these permissions.`
34
+ );
35
+ this.name = 'AdbPermissionGrantError';
36
+ }
37
+ }
38
+
39
+ export class AdbBinaryNotFoundError extends AdbError {
40
+ constructor() {
41
+ super(
42
+ `adb binary not found or not accessible. ` +
43
+ `Ensure Android SDK is properly installed and ANDROID_HOME is set.`
44
+ );
45
+ this.name = 'AdbBinaryNotFoundError';
46
+ }
47
+ }
package/src/adb-id.ts CHANGED
@@ -16,6 +16,10 @@ export const getAdbId = async (
16
16
 
17
17
  for (const adbId of adbIds) {
18
18
  if (isAndroidDeviceEmulator(device)) {
19
+ if (!isAdbIdEmulator(adbId)) {
20
+ continue;
21
+ }
22
+
19
23
  const emulatorName = await adb.getEmulatorName(adbId);
20
24
 
21
25
  if (emulatorName === device.name) {