@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.
- package/dist/__tests__/adb-id.test.d.ts +2 -0
- package/dist/__tests__/adb-id.test.d.ts.map +1 -0
- package/dist/__tests__/adb-id.test.js +41 -0
- package/dist/__tests__/adb.test.js +148 -13
- package/dist/__tests__/app-monitor.test.js +52 -75
- package/dist/__tests__/emulator-startup.test.js +1 -0
- package/dist/__tests__/instance.test.js +97 -36
- package/dist/adb-errors.d.ts +16 -0
- package/dist/adb-errors.d.ts.map +1 -0
- package/dist/adb-errors.js +35 -0
- package/dist/adb-id.d.ts.map +1 -1
- package/dist/adb-id.js +3 -0
- package/dist/adb.d.ts +4 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +124 -1
- package/dist/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +2 -5
- package/dist/emulator-startup.d.ts.map +1 -1
- package/dist/emulator-startup.js +0 -2
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +25 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/eslint.config.mjs +1 -0
- package/package.json +5 -4
- package/src/__tests__/adb-id.test.ts +51 -0
- package/src/__tests__/emulator-startup.test.ts +3 -0
- package/src/__tests__/instance.test.ts +143 -51
- package/src/adb-errors.ts +47 -0
- package/src/adb-id.ts +4 -0
- package/src/adb.ts +174 -1
- package/src/app-monitor.ts +3 -8
- package/src/emulator-startup.ts +0 -2
- package/src/instance.ts +30 -2
- package/test/setup.ts +68 -0
- package/vite.config.ts +19 -0
- package/src/__tests__/adb.test.ts +0 -598
- package/src/__tests__/app-monitor.test.ts +0 -273
- package/src/__tests__/environment.test.ts +0 -212
|
@@ -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 {
|
|
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
|
+
};
|
package/src/app-monitor.ts
CHANGED
|
@@ -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 =
|
|
449
|
-
|
|
450
|
-
|
|
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;
|
package/src/emulator-startup.ts
CHANGED
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 (
|
|
255
|
-
|
|
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
|
+
}));
|