@react-native-harness/platform-android 1.1.0 → 1.2.0

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.
@@ -3,9 +3,6 @@ import {
3
3
  DEFAULT_METRO_PORT,
4
4
  type Config as HarnessConfig,
5
5
  } from '@react-native-harness/config';
6
- import fs from 'node:fs';
7
- import os from 'node:os';
8
- import path from 'node:path';
9
6
  import {
10
7
  getAndroidEmulatorPlatformInstance,
11
8
  getAndroidPhysicalDevicePlatformInstance,
@@ -27,9 +24,13 @@ const init = {
27
24
  };
28
25
 
29
26
  describe('Android platform instance', () => {
30
- beforeEach(() => {
27
+ beforeEach(async () => {
31
28
  vi.restoreAllMocks();
32
29
  vi.unstubAllEnvs();
30
+ vi.spyOn(
31
+ await import('../environment.js'),
32
+ 'ensureAndroidEmulatorAvailable',
33
+ ).mockResolvedValue('/tmp/android-sdk');
33
34
  });
34
35
 
35
36
  it('reuses a running emulator and does not shut it down on dispose', async () => {
@@ -191,6 +192,10 @@ describe('Android platform instance', () => {
191
192
 
192
193
  it('reuses a compatible cached AVD snapshot when caching is enabled', async () => {
193
194
  vi.stubEnv('HARNESS_AVD_CACHING', 'true');
195
+ vi.spyOn(
196
+ await import('../environment.js'),
197
+ 'getHostAndroidSystemImageArch',
198
+ ).mockReturnValue('arm64-v8a');
194
199
  vi.spyOn(
195
200
  await import('../environment.js'),
196
201
  'ensureAndroidEmulatorEnvironment',
@@ -382,53 +387,6 @@ describe('Android platform instance', () => {
382
387
  );
383
388
  });
384
389
 
385
- it('installs the app from HARNESS_APP_PATH when missing', async () => {
386
- const appPath = path.join(os.tmpdir(), 'HarnessPlayground.apk');
387
- fs.writeFileSync(appPath, 'apk');
388
- vi.stubEnv('HARNESS_APP_PATH', appPath);
389
- vi.spyOn(
390
- await import('../environment.js'),
391
- 'ensureAndroidEmulatorEnvironment',
392
- ).mockResolvedValue('/tmp/android-sdk');
393
- vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
394
- vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
395
- vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
396
- vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(false);
397
- const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
398
- vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
399
- vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
400
- vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
401
- vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
402
- undefined,
403
- );
404
-
405
- await expect(
406
- getAndroidEmulatorPlatformInstance(
407
- {
408
- name: 'android',
409
- device: {
410
- type: 'emulator',
411
- name: 'Pixel_8_API_35',
412
- avd: {
413
- apiLevel: 35,
414
- profile: 'pixel_8',
415
- diskSize: '1G',
416
- heapSize: '1G',
417
- },
418
- },
419
- bundleId: 'com.harnessplayground',
420
- activityName: '.MainActivity',
421
- },
422
- harnessConfig,
423
- init,
424
- ),
425
- ).resolves.toBeDefined();
426
-
427
- expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
428
-
429
- fs.rmSync(appPath, { force: true });
430
- });
431
-
432
390
  it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
433
391
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
434
392
  vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
@@ -607,4 +565,138 @@ describe('Android platform instance', () => {
607
565
  expect(appMonitor.addListener(listener)).toBeUndefined();
608
566
  expect(appMonitor.removeListener(listener)).toBeUndefined();
609
567
  });
568
+
569
+ it('grants permissions when permissions are enabled for emulator', async () => {
570
+ vi.spyOn(
571
+ await import('../environment.js'),
572
+ 'ensureAndroidEmulatorEnvironment',
573
+ ).mockResolvedValue('/tmp/android-sdk');
574
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
575
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
576
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
577
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
578
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
579
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
580
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
581
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
582
+ undefined,
583
+ );
584
+ const grantPermissions = vi
585
+ .spyOn(adb, 'grantPermissions')
586
+ .mockResolvedValue(undefined);
587
+
588
+ const harnessConfigWithPermissions = {
589
+ ...harnessConfig,
590
+ permissions: true,
591
+ } as HarnessConfig;
592
+
593
+ await getAndroidEmulatorPlatformInstance(
594
+ {
595
+ name: 'android',
596
+ device: {
597
+ type: 'emulator',
598
+ name: 'Pixel_8_API_35',
599
+ avd: {
600
+ apiLevel: 35,
601
+ profile: 'pixel_8',
602
+ diskSize: '1G',
603
+ heapSize: '1G',
604
+ },
605
+ },
606
+ bundleId: 'com.harnessplayground',
607
+ activityName: '.MainActivity',
608
+ },
609
+ harnessConfigWithPermissions,
610
+ init,
611
+ );
612
+
613
+ expect(grantPermissions).toHaveBeenCalledWith(
614
+ 'emulator-5554',
615
+ 'com.harnessplayground',
616
+ );
617
+ });
618
+
619
+ it('does not grant permissions when permissions are disabled for emulator', async () => {
620
+ vi.spyOn(
621
+ await import('../environment.js'),
622
+ 'ensureAndroidEmulatorEnvironment',
623
+ ).mockResolvedValue('/tmp/android-sdk');
624
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
625
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
626
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
627
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
628
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
629
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
630
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
631
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
632
+ undefined,
633
+ );
634
+ const grantPermissions = vi
635
+ .spyOn(adb, 'grantPermissions')
636
+ .mockResolvedValue(undefined);
637
+
638
+ await getAndroidEmulatorPlatformInstance(
639
+ {
640
+ name: 'android',
641
+ device: {
642
+ type: 'emulator',
643
+ name: 'Pixel_8_API_35',
644
+ avd: {
645
+ apiLevel: 35,
646
+ profile: 'pixel_8',
647
+ diskSize: '1G',
648
+ heapSize: '1G',
649
+ },
650
+ },
651
+ bundleId: 'com.harnessplayground',
652
+ activityName: '.MainActivity',
653
+ },
654
+ harnessConfig,
655
+ init,
656
+ );
657
+
658
+ expect(grantPermissions).not.toHaveBeenCalled();
659
+ });
660
+
661
+ it('grants permissions when permissions are enabled for physical device', async () => {
662
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
663
+ vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
664
+ manufacturer: 'motorola',
665
+ model: 'moto g72',
666
+ });
667
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
668
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
669
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
670
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
671
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
672
+ undefined,
673
+ );
674
+ const grantPermissions = vi
675
+ .spyOn(adb, 'grantPermissions')
676
+ .mockResolvedValue(undefined);
677
+
678
+ const harnessConfigWithPermissions = {
679
+ ...harnessConfig,
680
+ permissions: true,
681
+ } as HarnessConfig;
682
+
683
+ await getAndroidPhysicalDevicePlatformInstance(
684
+ {
685
+ name: 'android-device',
686
+ device: {
687
+ type: 'physical',
688
+ manufacturer: 'motorola',
689
+ model: 'moto g72',
690
+ },
691
+ bundleId: 'com.harnessplayground',
692
+ activityName: '.MainActivity',
693
+ },
694
+ harnessConfigWithPermissions,
695
+ );
696
+
697
+ expect(grantPermissions).toHaveBeenCalledWith(
698
+ '012345',
699
+ 'com.harnessplayground',
700
+ );
701
+ });
610
702
  });
@@ -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) {
package/src/adb.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
2
- import { spawn, SubprocessError } from '@react-native-harness/tools';
2
+ import {
3
+ logger,
4
+ spawn,
5
+ SubprocessError,
6
+ type Subprocess,
7
+ } from '@react-native-harness/tools';
3
8
  import { spawn as nodeSpawn } from 'node:child_process';
4
9
  import type { ChildProcessByStdio } from 'node:child_process';
5
10
  import { access, rm } from 'node:fs/promises';
@@ -23,6 +28,10 @@ import {
23
28
  getEmulatorStartupArgs,
24
29
  type EmulatorBootMode,
25
30
  } from './emulator-startup.js';
31
+ import {
32
+ AdbAppNotInstalledError,
33
+ AdbPermissionGrantError,
34
+ } from './adb-errors.js';
26
35
 
27
36
  const wait = async (ms: number): Promise<void> => {
28
37
  await new Promise((resolve) => {
@@ -59,6 +68,7 @@ const waitWithSignal = async (
59
68
 
60
69
  const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
61
70
  const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
71
+ const androidAdbLogger = logger.child('android-adb');
62
72
 
63
73
  export const emulatorProcess = {
64
74
  startDetachedProcess: (
@@ -374,6 +384,79 @@ export const getDeviceInfo = async (
374
384
  return { manufacturer, model };
375
385
  };
376
386
 
387
+ const getRequestedPermissions = async (
388
+ adbId: string,
389
+ bundleId: string,
390
+ ): Promise<string[]> => {
391
+ const { stdout } = await spawn(getAdbBinaryPath(), [
392
+ '-s',
393
+ adbId,
394
+ 'shell',
395
+ 'dumpsys',
396
+ 'package',
397
+ bundleId,
398
+ ]);
399
+
400
+ const requestedPermissions = new Set<string>();
401
+ const lines = stdout.split('\n');
402
+ let inRequestedPermissionsSection = false;
403
+
404
+ for (const line of lines) {
405
+ const trimmedLine = line.trim();
406
+
407
+ if (trimmedLine === 'requested permissions:') {
408
+ inRequestedPermissionsSection = true;
409
+ continue;
410
+ }
411
+
412
+ if (!inRequestedPermissionsSection) {
413
+ continue;
414
+ }
415
+
416
+ if (trimmedLine === '') {
417
+ continue;
418
+ }
419
+
420
+ if (trimmedLine.endsWith(':')) {
421
+ break;
422
+ }
423
+
424
+ if (/^[a-zA-Z0-9_.]+$/.test(trimmedLine)) {
425
+ requestedPermissions.add(trimmedLine);
426
+ continue;
427
+ }
428
+
429
+ break;
430
+ }
431
+
432
+ return [...requestedPermissions];
433
+ };
434
+
435
+ const getDangerousPermissions = async (adbId: string): Promise<Set<string>> => {
436
+ const { stdout } = await spawn(getAdbBinaryPath(), [
437
+ '-s',
438
+ adbId,
439
+ 'shell',
440
+ 'pm',
441
+ 'list',
442
+ 'permissions',
443
+ '-g',
444
+ '-d',
445
+ ]);
446
+
447
+ const dangerousPermissions = new Set<string>();
448
+
449
+ for (const match of stdout.matchAll(/permission:([a-zA-Z0-9_.]+)/g)) {
450
+ const permission = match[1]?.trim();
451
+
452
+ if (permission) {
453
+ dangerousPermissions.add(permission);
454
+ }
455
+ }
456
+
457
+ return dangerousPermissions;
458
+ };
459
+
377
460
  export const isBootCompleted = async (adbId: string): Promise<boolean> => {
378
461
  try {
379
462
  const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
@@ -398,6 +481,13 @@ export const installApp = async (
398
481
  await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
399
482
  };
400
483
 
484
+ export const uninstallApp = async (
485
+ adbId: string,
486
+ bundleId: string,
487
+ ): Promise<void> => {
488
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]);
489
+ };
490
+
401
491
  export const hasAvd = async (name: string): Promise<boolean> => {
402
492
  const avds = await getAvds();
403
493
  return avds.includes(name);
@@ -680,6 +770,15 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
680
770
  return stdout.trim().replace(/^'+|'+$/g, '');
681
771
  };
682
772
 
773
+ export const startLogcat = (
774
+ adbId: string,
775
+ args: readonly string[],
776
+ ): Subprocess =>
777
+ spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
778
+ stdout: 'pipe',
779
+ stderr: 'pipe',
780
+ });
781
+
683
782
  export const getAvds = async (): Promise<string[]> => {
684
783
  try {
685
784
  const emulatorBinaryPath = await ensureEmulatorInstalled();
@@ -730,3 +829,77 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
730
829
 
731
830
  return devices;
732
831
  };
832
+
833
+ export const grantPermissions = async (
834
+ adbId: string,
835
+ bundleId: string,
836
+ ): Promise<void> => {
837
+ androidAdbLogger.debug('grantPermissions:start %o', {
838
+ adbId,
839
+ bundleId,
840
+ });
841
+
842
+ const isInstalled = await isAppInstalled(adbId, bundleId);
843
+ if (!isInstalled) {
844
+ throw new AdbAppNotInstalledError(bundleId, adbId);
845
+ }
846
+
847
+ const [requestedPermissions, dangerousPermissions] = await Promise.all([
848
+ getRequestedPermissions(adbId, bundleId),
849
+ getDangerousPermissions(adbId),
850
+ ]);
851
+ const permissions = requestedPermissions.filter((permission) =>
852
+ dangerousPermissions.has(permission),
853
+ );
854
+
855
+ androidAdbLogger.debug('grantPermissions:resolved %o', {
856
+ adbId,
857
+ bundleId,
858
+ requestedPermissions,
859
+ permissions,
860
+ });
861
+
862
+ if (permissions.length === 0) {
863
+ androidAdbLogger.debug('grantPermissions:skip %o', {
864
+ adbId,
865
+ bundleId,
866
+ });
867
+ return;
868
+ }
869
+
870
+ const grantCommands = permissions.map((permission) => [
871
+ '-s',
872
+ adbId,
873
+ 'shell',
874
+ 'pm',
875
+ 'grant',
876
+ bundleId,
877
+ permission,
878
+ ]);
879
+
880
+ try {
881
+ androidAdbLogger.debug('grantPermissions:commands %o', {
882
+ adbId,
883
+ bundleId,
884
+ grantCommands,
885
+ });
886
+
887
+ await Promise.all(
888
+ grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])),
889
+ );
890
+
891
+ androidAdbLogger.debug('grantPermissions:success %o', {
892
+ adbId,
893
+ bundleId,
894
+ permissions,
895
+ });
896
+ } catch (error) {
897
+ androidAdbLogger.debug('grantPermissions:error %o', {
898
+ adbId,
899
+ bundleId,
900
+ permissions,
901
+ error,
902
+ });
903
+ throw new AdbPermissionGrantError(bundleId, permissions, adbId);
904
+ }
905
+ };
@@ -10,7 +10,6 @@ import {
10
10
  escapeRegExp,
11
11
  getEmitter,
12
12
  logger,
13
- spawn,
14
13
  SubprocessError,
15
14
  type Subprocess,
16
15
  } from '@react-native-harness/tools';
@@ -445,13 +444,9 @@ export const createAndroidAppMonitor = ({
445
444
  const startLogcat = async () => {
446
445
  const logcatTimestamp = await adb.getLogcatTimestamp(adbId);
447
446
 
448
- logcatProcess = spawn(
449
- 'adb',
450
- ['-s', adbId, ...getLogcatArgs(appUid, logcatTimestamp)],
451
- {
452
- stdout: 'pipe',
453
- stderr: 'pipe',
454
- }
447
+ logcatProcess = adb.startLogcat(
448
+ adbId,
449
+ getLogcatArgs(appUid, logcatTimestamp)
455
450
  );
456
451
 
457
452
  const currentProcess = logcatProcess;
@@ -9,8 +9,6 @@ const COMMON_EMULATOR_ARGS = [
9
9
  'swiftshader_indirect',
10
10
  '-noaudio',
11
11
  '-no-boot-anim',
12
- '-camera-back',
13
- 'none',
14
12
  ] as const;
15
13
 
16
14
  export const getEmulatorStartupArgs = (
package/src/instance.ts CHANGED
@@ -59,6 +59,20 @@ const getHarnessAppPath = (): string => {
59
59
  return appPath;
60
60
  };
61
61
 
62
+ const getOptionalHarnessAppPath = (): string | undefined => {
63
+ const appPath = process.env.HARNESS_APP_PATH;
64
+
65
+ if (!appPath) {
66
+ return undefined;
67
+ }
68
+
69
+ if (!fs.existsSync(appPath)) {
70
+ throw new HarnessAppPathError('invalid', appPath);
71
+ }
72
+
73
+ return appPath;
74
+ };
75
+
62
76
  const configureAndroidRuntime = async (
63
77
  adbId: string,
64
78
  config: AndroidPlatformConfig,
@@ -180,6 +194,7 @@ export const getAndroidEmulatorPlatformInstance = async (
180
194
  ): Promise<HarnessPlatformRunner> => {
181
195
  assertAndroidDeviceEmulator(config.device);
182
196
  const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
197
+ const permissionsEnabled = harnessConfig.permissions ?? false;
183
198
  const emulatorConfig = config.device;
184
199
  const emulatorName = emulatorConfig.name;
185
200
  const avdConfig = emulatorConfig.avd;
@@ -250,14 +265,22 @@ export const getAndroidEmulatorPlatformInstance = async (
250
265
  );
251
266
 
252
267
  const isInstalled = await adb.isAppInstalled(adbId, config.bundleId);
268
+ const appPath = getOptionalHarnessAppPath();
253
269
 
254
- if (!isInstalled) {
255
- const appPath = getHarnessAppPath();
270
+ if (isInstalled && appPath) {
271
+ await adb.uninstallApp(adbId, config.bundleId);
256
272
  await adb.installApp(adbId, appPath);
273
+ } else if (!isInstalled) {
274
+ const installPath = appPath ?? getHarnessAppPath();
275
+ await adb.installApp(adbId, installPath);
257
276
  }
258
277
 
259
278
  const appUid = await configureAndroidRuntime(adbId, config, harnessConfig);
260
279
 
280
+ if (permissionsEnabled) {
281
+ await adb.grantPermissions(adbId, config.bundleId);
282
+ }
283
+
261
284
  return {
262
285
  startApp: async (options) => {
263
286
  await adb.startApp(
@@ -315,6 +338,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
315
338
  ): Promise<HarnessPlatformRunner> => {
316
339
  assertAndroidDevicePhysical(config.device);
317
340
  const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
341
+ const permissionsEnabled = harnessConfig.permissions ?? false;
318
342
 
319
343
  const adbId = await getAdbId(config.device);
320
344
 
@@ -333,6 +357,10 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
333
357
 
334
358
  const appUid = await configureAndroidRuntime(adbId, config, harnessConfig);
335
359
 
360
+ if (permissionsEnabled) {
361
+ await adb.grantPermissions(adbId, config.bundleId);
362
+ }
363
+
336
364
  return {
337
365
  startApp: async (options) => {
338
366
  await adb.startApp(
package/test/setup.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { PassThrough } from 'node:stream';
2
+ import { vi } from 'vitest';
3
+ import type { ChildProcessByStdio } from 'node:child_process';
4
+
5
+ const createMockChildProcess = (): ChildProcessByStdio<
6
+ null,
7
+ PassThrough,
8
+ PassThrough
9
+ > => {
10
+ const stdout = new PassThrough();
11
+ const stderr = new PassThrough();
12
+
13
+ return {
14
+ stdout,
15
+ stderr,
16
+ unref: vi.fn(),
17
+ once: vi.fn(),
18
+ removeAllListeners: vi.fn(),
19
+ } as unknown as ChildProcessByStdio<null, PassThrough, PassThrough>;
20
+ };
21
+
22
+ const createMockSubprocess = () => ({
23
+ nodeChildProcess: Promise.resolve({
24
+ kill: vi.fn(),
25
+ }),
26
+ [Symbol.asyncIterator]: async function* () {
27
+ yield* [];
28
+ },
29
+ });
30
+
31
+ vi.mock('../src/adb.js', async (importOriginal) => {
32
+ const actual = await importOriginal<typeof import('../src/adb.js')>();
33
+
34
+ return {
35
+ ...actual,
36
+ emulatorProcess: {
37
+ startDetachedProcess: vi.fn(createMockChildProcess),
38
+ },
39
+ verifyAndroidEmulatorSdk: vi.fn().mockResolvedValue(undefined),
40
+ isAppInstalled: vi.fn().mockResolvedValue(true),
41
+ reversePort: vi.fn().mockResolvedValue(undefined),
42
+ stopApp: vi.fn().mockResolvedValue(undefined),
43
+ startApp: vi.fn().mockResolvedValue(undefined),
44
+ getDeviceIds: vi.fn().mockResolvedValue([]),
45
+ getEmulatorName: vi.fn().mockResolvedValue(''),
46
+ getShellProperty: vi.fn().mockResolvedValue(''),
47
+ getDeviceInfo: vi.fn().mockResolvedValue(null),
48
+ isBootCompleted: vi.fn().mockResolvedValue(true),
49
+ stopEmulator: vi.fn().mockResolvedValue(undefined),
50
+ installApp: vi.fn().mockResolvedValue(undefined),
51
+ uninstallApp: vi.fn().mockResolvedValue(undefined),
52
+ hasAvd: vi.fn().mockResolvedValue(false),
53
+ createAvd: vi.fn().mockResolvedValue(undefined),
54
+ deleteAvd: vi.fn().mockResolvedValue(undefined),
55
+ startEmulator: vi.fn().mockResolvedValue(undefined),
56
+ waitForEmulator: vi.fn().mockResolvedValue('emulator-5554'),
57
+ waitForEmulatorDisconnect: vi.fn().mockResolvedValue(undefined),
58
+ waitForBoot: vi.fn().mockResolvedValue('emulator-5554'),
59
+ isAppRunning: vi.fn().mockResolvedValue(false),
60
+ getAppUid: vi.fn().mockResolvedValue(0),
61
+ setHideErrorDialogs: vi.fn().mockResolvedValue(undefined),
62
+ getLogcatTimestamp: vi.fn().mockResolvedValue('01-01 00:00:00.000'),
63
+ startLogcat: vi.fn(createMockSubprocess),
64
+ getAvds: vi.fn().mockResolvedValue([]),
65
+ getConnectedDevices: vi.fn().mockResolvedValue([]),
66
+ grantPermissions: vi.fn().mockResolvedValue(undefined),
67
+ };
68
+ });
package/vite.config.ts ADDED
@@ -0,0 +1,19 @@
1
+ /// <reference types='vitest' />
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig(() => ({
5
+ root: __dirname,
6
+ cacheDir: '../../node_modules/.vite/packages/platform-android',
7
+ test: {
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ setupFiles: ['./test/setup.ts'],
12
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
13
+ reporters: ['default'],
14
+ coverage: {
15
+ reportsDirectory: './test-output/vitest/coverage',
16
+ provider: 'v8' as const,
17
+ },
18
+ },
19
+ }));