@react-native-harness/platform-android 1.1.0 → 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.
- package/dist/__tests__/adb.test.js +93 -1
- package/dist/__tests__/emulator-startup.test.js +1 -0
- package/dist/__tests__/instance.test.js +130 -0
- 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 +2 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +120 -1
- 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/package.json +4 -4
- package/src/__tests__/adb.test.ts +102 -0
- package/src/__tests__/emulator-startup.test.ts +3 -0
- package/src/__tests__/instance.test.ts +188 -0
- package/src/adb-errors.ts +47 -0
- package/src/adb-id.ts +4 -0
- package/src/adb.ts +160 -1
- package/src/emulator-startup.ts +0 -2
- package/src/instance.ts +30 -2
|
@@ -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) {
|
package/src/adb.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
|
|
2
|
-
import { spawn, SubprocessError } from '@react-native-harness/tools';
|
|
2
|
+
import { logger, spawn, SubprocessError } from '@react-native-harness/tools';
|
|
3
3
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
4
4
|
import type { ChildProcessByStdio } from 'node:child_process';
|
|
5
5
|
import { access, rm } from 'node:fs/promises';
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
getEmulatorStartupArgs,
|
|
24
24
|
type EmulatorBootMode,
|
|
25
25
|
} from './emulator-startup.js';
|
|
26
|
+
import {
|
|
27
|
+
AdbAppNotInstalledError,
|
|
28
|
+
AdbPermissionGrantError,
|
|
29
|
+
} from './adb-errors.js';
|
|
26
30
|
|
|
27
31
|
const wait = async (ms: number): Promise<void> => {
|
|
28
32
|
await new Promise((resolve) => {
|
|
@@ -59,6 +63,7 @@ const waitWithSignal = async (
|
|
|
59
63
|
|
|
60
64
|
const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
|
|
61
65
|
const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
|
|
66
|
+
const androidAdbLogger = logger.child('android-adb');
|
|
62
67
|
|
|
63
68
|
export const emulatorProcess = {
|
|
64
69
|
startDetachedProcess: (
|
|
@@ -374,6 +379,79 @@ export const getDeviceInfo = async (
|
|
|
374
379
|
return { manufacturer, model };
|
|
375
380
|
};
|
|
376
381
|
|
|
382
|
+
const getRequestedPermissions = async (
|
|
383
|
+
adbId: string,
|
|
384
|
+
bundleId: string,
|
|
385
|
+
): Promise<string[]> => {
|
|
386
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
387
|
+
'-s',
|
|
388
|
+
adbId,
|
|
389
|
+
'shell',
|
|
390
|
+
'dumpsys',
|
|
391
|
+
'package',
|
|
392
|
+
bundleId,
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
const requestedPermissions = new Set<string>();
|
|
396
|
+
const lines = stdout.split('\n');
|
|
397
|
+
let inRequestedPermissionsSection = false;
|
|
398
|
+
|
|
399
|
+
for (const line of lines) {
|
|
400
|
+
const trimmedLine = line.trim();
|
|
401
|
+
|
|
402
|
+
if (trimmedLine === 'requested permissions:') {
|
|
403
|
+
inRequestedPermissionsSection = true;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!inRequestedPermissionsSection) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (trimmedLine === '') {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (trimmedLine.endsWith(':')) {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (/^[a-zA-Z0-9_.]+$/.test(trimmedLine)) {
|
|
420
|
+
requestedPermissions.add(trimmedLine);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return [...requestedPermissions];
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const getDangerousPermissions = async (adbId: string): Promise<Set<string>> => {
|
|
431
|
+
const { stdout } = await spawn(getAdbBinaryPath(), [
|
|
432
|
+
'-s',
|
|
433
|
+
adbId,
|
|
434
|
+
'shell',
|
|
435
|
+
'pm',
|
|
436
|
+
'list',
|
|
437
|
+
'permissions',
|
|
438
|
+
'-g',
|
|
439
|
+
'-d',
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
const dangerousPermissions = new Set<string>();
|
|
443
|
+
|
|
444
|
+
for (const match of stdout.matchAll(/permission:([a-zA-Z0-9_.]+)/g)) {
|
|
445
|
+
const permission = match[1]?.trim();
|
|
446
|
+
|
|
447
|
+
if (permission) {
|
|
448
|
+
dangerousPermissions.add(permission);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return dangerousPermissions;
|
|
453
|
+
};
|
|
454
|
+
|
|
377
455
|
export const isBootCompleted = async (adbId: string): Promise<boolean> => {
|
|
378
456
|
try {
|
|
379
457
|
const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
|
|
@@ -398,6 +476,13 @@ export const installApp = async (
|
|
|
398
476
|
await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
|
|
399
477
|
};
|
|
400
478
|
|
|
479
|
+
export const uninstallApp = async (
|
|
480
|
+
adbId: string,
|
|
481
|
+
bundleId: string,
|
|
482
|
+
): Promise<void> => {
|
|
483
|
+
await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]);
|
|
484
|
+
};
|
|
485
|
+
|
|
401
486
|
export const hasAvd = async (name: string): Promise<boolean> => {
|
|
402
487
|
const avds = await getAvds();
|
|
403
488
|
return avds.includes(name);
|
|
@@ -730,3 +815,77 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
|
|
|
730
815
|
|
|
731
816
|
return devices;
|
|
732
817
|
};
|
|
818
|
+
|
|
819
|
+
export const grantPermissions = async (
|
|
820
|
+
adbId: string,
|
|
821
|
+
bundleId: string,
|
|
822
|
+
): Promise<void> => {
|
|
823
|
+
androidAdbLogger.debug('grantPermissions:start %o', {
|
|
824
|
+
adbId,
|
|
825
|
+
bundleId,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const isInstalled = await isAppInstalled(adbId, bundleId);
|
|
829
|
+
if (!isInstalled) {
|
|
830
|
+
throw new AdbAppNotInstalledError(bundleId, adbId);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const [requestedPermissions, dangerousPermissions] = await Promise.all([
|
|
834
|
+
getRequestedPermissions(adbId, bundleId),
|
|
835
|
+
getDangerousPermissions(adbId),
|
|
836
|
+
]);
|
|
837
|
+
const permissions = requestedPermissions.filter((permission) =>
|
|
838
|
+
dangerousPermissions.has(permission),
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
androidAdbLogger.debug('grantPermissions:resolved %o', {
|
|
842
|
+
adbId,
|
|
843
|
+
bundleId,
|
|
844
|
+
requestedPermissions,
|
|
845
|
+
permissions,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
if (permissions.length === 0) {
|
|
849
|
+
androidAdbLogger.debug('grantPermissions:skip %o', {
|
|
850
|
+
adbId,
|
|
851
|
+
bundleId,
|
|
852
|
+
});
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const grantCommands = permissions.map((permission) => [
|
|
857
|
+
'-s',
|
|
858
|
+
adbId,
|
|
859
|
+
'shell',
|
|
860
|
+
'pm',
|
|
861
|
+
'grant',
|
|
862
|
+
bundleId,
|
|
863
|
+
permission,
|
|
864
|
+
]);
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
androidAdbLogger.debug('grantPermissions:commands %o', {
|
|
868
|
+
adbId,
|
|
869
|
+
bundleId,
|
|
870
|
+
grantCommands,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
await Promise.all(
|
|
874
|
+
grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])),
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
androidAdbLogger.debug('grantPermissions:success %o', {
|
|
878
|
+
adbId,
|
|
879
|
+
bundleId,
|
|
880
|
+
permissions,
|
|
881
|
+
});
|
|
882
|
+
} catch (error) {
|
|
883
|
+
androidAdbLogger.debug('grantPermissions:error %o', {
|
|
884
|
+
adbId,
|
|
885
|
+
bundleId,
|
|
886
|
+
permissions,
|
|
887
|
+
error,
|
|
888
|
+
});
|
|
889
|
+
throw new AdbPermissionGrantError(bundleId, permissions, adbId);
|
|
890
|
+
}
|
|
891
|
+
};
|
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(
|