@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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adb-id.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb-id.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adb-id.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getAdbId, isAdbIdEmulator } from '../adb-id.js';
3
+ import * as adb from '../adb.js';
4
+ describe('adb id resolution', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('identifies emulator adb ids', () => {
9
+ expect(isAdbIdEmulator('emulator-5554')).toBe(true);
10
+ expect(isAdbIdEmulator('device-serial-001')).toBe(false);
11
+ });
12
+ it('skips non-emulator ids when resolving an emulator device', async () => {
13
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([
14
+ 'device-serial-001',
15
+ 'emulator-5554',
16
+ ]);
17
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Test_AVD_API_35');
18
+ await expect(getAdbId({
19
+ type: 'emulator',
20
+ name: 'Test_AVD_API_35',
21
+ })).resolves.toBe('emulator-5554');
22
+ });
23
+ it('resolves matching physical device by manufacturer and model', async () => {
24
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([
25
+ 'device-serial-001',
26
+ 'emulator-5554',
27
+ ]);
28
+ vi.spyOn(adb, 'getDeviceInfo').mockImplementation(async (adbId) => {
29
+ const results = {
30
+ 'device-serial-001': { manufacturer: 'Acme', model: 'Model A1' },
31
+ 'emulator-5554': { manufacturer: 'Emulator', model: 'Emulator Model' },
32
+ };
33
+ return results[adbId] || null;
34
+ });
35
+ await expect(getAdbId({
36
+ type: 'physical',
37
+ manufacturer: 'acme',
38
+ model: 'model a1',
39
+ })).resolves.toBe('device-serial-001');
40
+ });
41
+ });
@@ -1,12 +1,17 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
2
5
  import { PassThrough } from 'node:stream';
3
6
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
7
  import { SubprocessError } from '@react-native-harness/tools';
5
- import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startEmulator, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
8
+ vi.unmock('../adb.js');
9
+ import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startLogcat, startEmulator, uninstallApp, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
6
10
  import * as tools from '@react-native-harness/tools';
7
11
  import * as environment from '../environment.js';
8
12
  import * as avdConfig from '../avd-config.js';
9
13
  const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
14
+ let sdkRoot;
10
15
  const createMockChildProcess = () => {
11
16
  const process = new EventEmitter();
12
17
  process.stdout = new PassThrough();
@@ -16,6 +21,14 @@ const createMockChildProcess = () => {
16
21
  };
17
22
  beforeEach(() => {
18
23
  vi.restoreAllMocks();
24
+ vi.unstubAllEnvs();
25
+ sdkRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'android-sdk-'));
26
+ fs.mkdirSync(path.join(sdkRoot, 'emulator'), { recursive: true });
27
+ fs.writeFileSync(path.join(sdkRoot, 'emulator', 'emulator'), '');
28
+ fs.mkdirSync(path.join(sdkRoot, 'platform-tools'), { recursive: true });
29
+ fs.writeFileSync(path.join(sdkRoot, 'platform-tools', 'adb'), '');
30
+ vi.stubEnv('ANDROID_HOME', sdkRoot);
31
+ vi.spyOn(environment, 'ensureAndroidEmulatorAvailable').mockResolvedValue(sdkRoot);
19
32
  });
20
33
  describe('getStartAppArgs', () => {
21
34
  it('maps supported extras to adb am start flags', () => {
@@ -81,8 +94,31 @@ describe('getStartAppArgs', () => {
81
94
  "+'%m-%d %H:%M:%S.000'",
82
95
  ]);
83
96
  });
97
+ it('starts logcat using the resolved adb binary path', async () => {
98
+ const subprocess = {};
99
+ const spawnSpy = vi.spyOn(tools, 'spawn').mockReturnValue(subprocess);
100
+ expect(startLogcat('emulator-5554', [
101
+ 'logcat',
102
+ '-v',
103
+ 'threadtime',
104
+ '-b',
105
+ 'crash',
106
+ ])).toBe(subprocess);
107
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
108
+ '-s',
109
+ 'emulator-5554',
110
+ 'logcat',
111
+ '-v',
112
+ 'threadtime',
113
+ '-b',
114
+ 'crash',
115
+ ], {
116
+ stdout: 'pipe',
117
+ stderr: 'pipe',
118
+ });
119
+ });
84
120
  it('checks whether an AVD exists', async () => {
85
- vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
121
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
86
122
  stdout: 'Pixel_6_API_33\nPixel_8_API_35\n',
87
123
  });
88
124
  await expect(hasAvd('Pixel_8_API_35')).resolves.toBe(true);
@@ -101,6 +137,18 @@ describe('getStartAppArgs', () => {
101
137
  '/tmp/app.apk',
102
138
  ]);
103
139
  });
140
+ it('uninstalls the app via adb', async () => {
141
+ const spawnSpy = vi
142
+ .spyOn(tools, 'spawn')
143
+ .mockResolvedValueOnce({});
144
+ await uninstallApp('emulator-5554', 'com.example.app');
145
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
146
+ '-s',
147
+ 'emulator-5554',
148
+ 'uninstall',
149
+ 'com.example.app',
150
+ ]);
151
+ });
104
152
  it('creates an AVD and appends config overrides', async () => {
105
153
  vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({});
106
154
  const spawnSpy = vi
@@ -353,17 +401,24 @@ describe('getStartAppArgs', () => {
353
401
  });
354
402
  it('aborts while waiting for boot completion', async () => {
355
403
  vi.useFakeTimers();
356
- const spawnSpy = vi.spyOn(tools, 'spawn');
357
- spawnSpy
358
- .mockResolvedValueOnce({
359
- stdout: 'List of devices attached\nemulator-5554\tdevice\n',
360
- })
361
- .mockResolvedValueOnce({
362
- stdout: 'Pixel_8_API_35\n',
363
- })
364
- .mockResolvedValueOnce({
365
- stdout: '0\n',
366
- });
404
+ vi.spyOn(tools, 'spawn').mockImplementation((async (_file, args) => {
405
+ if (args?.[0] === 'devices') {
406
+ return {
407
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
408
+ };
409
+ }
410
+ if (args?.includes('emu') && args?.includes('avd') && args?.includes('name')) {
411
+ return {
412
+ stdout: 'Pixel_8_API_35\n',
413
+ };
414
+ }
415
+ if (args?.includes('getprop')) {
416
+ return {
417
+ stdout: '0\n',
418
+ };
419
+ }
420
+ throw new Error(`Unexpected adb call: ${args?.join(' ')}`);
421
+ }));
367
422
  const controller = new AbortController();
368
423
  const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
369
424
  await vi.advanceTimersByTimeAsync(1000);
@@ -414,4 +469,84 @@ describe('getStartAppArgs', () => {
414
469
  await expect(waitPromise).resolves.toBeUndefined();
415
470
  expect(spawnSpy).toHaveBeenCalledTimes(2);
416
471
  });
472
+ it('grants requested dangerous permissions to an app', async () => {
473
+ const { grantPermissions } = await import('../adb.js');
474
+ const spawnSpy = vi
475
+ .spyOn(tools, 'spawn')
476
+ .mockResolvedValueOnce({
477
+ stdout: 'package:com.example.app\n',
478
+ })
479
+ .mockResolvedValueOnce({
480
+ stdout: `requested permissions:\n android.permission.CAMERA\n android.permission.INTERNET\n android.permission.ACCESS_FINE_LOCATION\ninstall permissions:\n`,
481
+ })
482
+ .mockResolvedValueOnce({
483
+ stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n permission:android.permission.ACCESS_FINE_LOCATION\n`,
484
+ })
485
+ .mockResolvedValue({
486
+ stdout: '',
487
+ });
488
+ await grantPermissions('emulator-5554', 'com.example.app');
489
+ expect(spawnSpy).toHaveBeenCalledTimes(5);
490
+ expect(spawnSpy).toHaveBeenNthCalledWith(1, expect.any(String), [
491
+ '-s',
492
+ 'emulator-5554',
493
+ 'shell',
494
+ 'pm',
495
+ 'list',
496
+ 'packages',
497
+ 'com.example.app',
498
+ ]);
499
+ expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [
500
+ '-s',
501
+ 'emulator-5554',
502
+ 'shell',
503
+ 'dumpsys',
504
+ 'package',
505
+ 'com.example.app',
506
+ ]);
507
+ expect(spawnSpy).toHaveBeenNthCalledWith(3, expect.any(String), [
508
+ '-s',
509
+ 'emulator-5554',
510
+ 'shell',
511
+ 'pm',
512
+ 'list',
513
+ 'permissions',
514
+ '-g',
515
+ '-d',
516
+ ]);
517
+ expect(spawnSpy).toHaveBeenNthCalledWith(4, expect.any(String), [
518
+ '-s',
519
+ 'emulator-5554',
520
+ 'shell',
521
+ 'pm',
522
+ 'grant',
523
+ 'com.example.app',
524
+ 'android.permission.CAMERA',
525
+ ]);
526
+ expect(spawnSpy).toHaveBeenNthCalledWith(5, expect.any(String), [
527
+ '-s',
528
+ 'emulator-5554',
529
+ 'shell',
530
+ 'pm',
531
+ 'grant',
532
+ 'com.example.app',
533
+ 'android.permission.ACCESS_FINE_LOCATION',
534
+ ]);
535
+ });
536
+ it('does nothing when the app has no grantable dangerous permissions', async () => {
537
+ const { grantPermissions } = await import('../adb.js');
538
+ const spawnSpy = vi
539
+ .spyOn(tools, 'spawn')
540
+ .mockResolvedValueOnce({
541
+ stdout: 'package:com.example.app\n',
542
+ })
543
+ .mockResolvedValueOnce({
544
+ stdout: `requested permissions:\n android.permission.INTERNET\ninstall permissions:\n`,
545
+ })
546
+ .mockResolvedValueOnce({
547
+ stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n`,
548
+ });
549
+ await grantPermissions('emulator-5554', 'com.example.app');
550
+ expect(spawnSpy).toHaveBeenCalledTimes(3);
551
+ });
417
552
  });
@@ -3,8 +3,8 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { createAndroidAppMonitor, createAndroidLogEvent } from '../app-monitor.js';
6
- import * as tools from '@react-native-harness/tools';
7
6
  import { createCrashArtifactWriter } from '@react-native-harness/tools';
7
+ import * as adb from '../adb.js';
8
8
  const createMockSubprocess = () => ({
9
9
  nodeChildProcess: Promise.resolve({
10
10
  kill: vi.fn(),
@@ -55,15 +55,12 @@ describe('createAndroidLogEvent', () => {
55
55
  });
56
56
  });
57
57
  it('starts logcat from the current device timestamp', async () => {
58
- const spawnSpy = vi.spyOn(tools, 'spawn');
59
- spawnSpy.mockImplementation(((file, args) => {
60
- if (file === 'adb' && args?.includes('date')) {
61
- return {
62
- stdout: '03-12 11:35:08.000\n',
63
- };
64
- }
65
- return createMockSubprocess();
66
- }));
58
+ const getLogcatTimestampSpy = vi
59
+ .spyOn(adb, 'getLogcatTimestamp')
60
+ .mockResolvedValue('03-12 11:35:08.000');
61
+ const startLogcatSpy = vi
62
+ .spyOn(adb, 'startLogcat')
63
+ .mockReturnValue(createMockSubprocess());
67
64
  const monitor = createAndroidAppMonitor({
68
65
  adbId: 'emulator-5554',
69
66
  bundleId: 'com.harnessplayground',
@@ -71,9 +68,8 @@ describe('createAndroidLogEvent', () => {
71
68
  });
72
69
  await monitor.start();
73
70
  await monitor.stop();
74
- expect(spawnSpy).toHaveBeenNthCalledWith(2, 'adb', [
75
- '-s',
76
- 'emulator-5554',
71
+ expect(getLogcatTimestampSpy).toHaveBeenCalledWith('emulator-5554');
72
+ expect(startLogcatSpy).toHaveBeenCalledWith('emulator-5554', [
77
73
  'logcat',
78
74
  '-v',
79
75
  'threadtime',
@@ -82,34 +78,24 @@ describe('createAndroidLogEvent', () => {
82
78
  '--uid=10234',
83
79
  '-T',
84
80
  '03-12 11:35:08.000',
85
- ], {
86
- stdout: 'pipe',
87
- stderr: 'pipe',
88
- });
81
+ ]);
89
82
  });
90
83
  it('hydrates crash details with stack lines that arrive after the first crash event', async () => {
91
- const spawnSpy = vi.spyOn(tools, 'spawn');
92
- spawnSpy.mockImplementation(((file, args) => {
93
- if (file === 'adb' && args?.includes('date')) {
94
- return {
95
- stdout: '03-12 10:44:40.000\n',
96
- };
97
- }
98
- return createStreamingSubprocess([
99
- { line: '--------- beginning of crash' },
100
- {
101
- line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
102
- },
103
- {
104
- line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
105
- delayMs: 25,
106
- },
107
- {
108
- line: '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
109
- delayMs: 25,
110
- },
111
- ]);
112
- }));
84
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('03-12 10:44:40.000');
85
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
86
+ { line: '--------- beginning of crash' },
87
+ {
88
+ line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
89
+ },
90
+ {
91
+ line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
92
+ delayMs: 25,
93
+ },
94
+ {
95
+ line: '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
96
+ delayMs: 25,
97
+ },
98
+ ]));
113
99
  const monitor = createAndroidAppMonitor({
114
100
  adbId: 'emulator-5554',
115
101
  bundleId: 'com.harnessplayground',
@@ -130,24 +116,17 @@ describe('createAndroidLogEvent', () => {
130
116
  ]);
131
117
  });
132
118
  it('persists resolved Android crash blocks into .harness', async () => {
133
- const spawnSpy = vi.spyOn(tools, 'spawn');
134
- spawnSpy.mockImplementation(((file, args) => {
135
- if (file === 'adb' && args?.includes('date')) {
136
- return {
137
- stdout: '03-12 10:44:40.000\n',
138
- };
139
- }
140
- return createStreamingSubprocess([
141
- { line: '--------- beginning of crash' },
142
- {
143
- line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
144
- },
145
- {
146
- line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
147
- delayMs: 20,
148
- },
149
- ]);
150
- }));
119
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('03-12 10:44:40.000');
120
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
121
+ { line: '--------- beginning of crash' },
122
+ {
123
+ line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
124
+ },
125
+ {
126
+ line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
127
+ delayMs: 20,
128
+ },
129
+ ]));
151
130
  const monitor = createAndroidAppMonitor({
152
131
  adbId: 'emulator-5554',
153
132
  bundleId: 'com.harnessplayground',
@@ -171,21 +150,13 @@ describe('createAndroidLogEvent', () => {
171
150
  expect(fs.readFileSync(details.artifactPath, 'utf8')).toContain('RuntimeException: boom');
172
151
  });
173
152
  it('can be started again after timestamp lookup fails', async () => {
174
- const spawnSpy = vi.spyOn(tools, 'spawn');
175
153
  const timestampError = new Error('date failed');
176
- spawnSpy.mockImplementation(((file, args) => {
177
- if (file === 'adb' && args?.includes('date')) {
178
- if (spawnSpy.mock.calls.filter(([calledFile, calledArgs]) => calledFile === 'adb' &&
179
- Array.isArray(calledArgs) &&
180
- calledArgs.includes('date')).length === 1) {
181
- throw timestampError;
182
- }
183
- return {
184
- stdout: '03-12 11:35:08.000\n',
185
- };
186
- }
187
- return createMockSubprocess();
188
- }));
154
+ vi.spyOn(adb, 'getLogcatTimestamp')
155
+ .mockRejectedValueOnce(timestampError)
156
+ .mockResolvedValueOnce('03-12 11:35:08.000');
157
+ const startLogcatSpy = vi
158
+ .spyOn(adb, 'startLogcat')
159
+ .mockReturnValue(createMockSubprocess());
189
160
  const monitor = createAndroidAppMonitor({
190
161
  adbId: 'emulator-5554',
191
162
  bundleId: 'com.harnessplayground',
@@ -194,9 +165,15 @@ describe('createAndroidLogEvent', () => {
194
165
  await expect(monitor.start()).rejects.toThrow(timestampError);
195
166
  await expect(monitor.start()).resolves.toBeUndefined();
196
167
  await monitor.stop();
197
- expect(spawnSpy.mock.calls.some(([file, args]) => file === 'adb' &&
198
- Array.isArray(args) &&
199
- args.includes('logcat') &&
200
- args.includes('--uid=10234'))).toBe(true);
168
+ expect(startLogcatSpy).toHaveBeenCalledWith('emulator-5554', [
169
+ 'logcat',
170
+ '-v',
171
+ 'threadtime',
172
+ '-b',
173
+ 'crash',
174
+ '--uid=10234',
175
+ '-T',
176
+ '03-12 11:35:08.000',
177
+ ]);
201
178
  });
202
179
  });
@@ -7,6 +7,7 @@ describe('emulator startup modes', () => {
7
7
  '-no-snapshot-load',
8
8
  '-no-snapshot-save',
9
9
  ]));
10
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual(expect.arrayContaining(['-camera-back', 'none']));
10
11
  });
11
12
  it('builds clean snapshot generation args', () => {
12
13
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-load']));
@@ -1,8 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { DEFAULT_METRO_PORT, } from '@react-native-harness/config';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
3
  import { getAndroidEmulatorPlatformInstance, getAndroidPhysicalDevicePlatformInstance, } from '../instance.js';
7
4
  import * as adb from '../adb.js';
8
5
  import * as avdConfig from '../avd-config.js';
@@ -19,9 +16,10 @@ const init = {
19
16
  signal: new AbortController().signal,
20
17
  };
21
18
  describe('Android platform instance', () => {
22
- beforeEach(() => {
19
+ beforeEach(async () => {
23
20
  vi.restoreAllMocks();
24
21
  vi.unstubAllEnvs();
22
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorAvailable').mockResolvedValue('/tmp/android-sdk');
25
23
  });
26
24
  it('reuses a running emulator and does not shut it down on dispose', async () => {
27
25
  const ensureAndroidEmulatorEnvironment = vi
@@ -137,6 +135,7 @@ describe('Android platform instance', () => {
137
135
  });
138
136
  it('reuses a compatible cached AVD snapshot when caching is enabled', async () => {
139
137
  vi.stubEnv('HARNESS_AVD_CACHING', 'true');
138
+ vi.spyOn(await import('../environment.js'), 'getHostAndroidSystemImageArch').mockReturnValue('arm64-v8a');
140
139
  vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
141
140
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
142
141
  vi.spyOn(adb, 'hasAvd').mockResolvedValue(true);
@@ -261,38 +260,6 @@ describe('Android platform instance', () => {
261
260
  expect(waitForEmulatorDisconnect).toHaveBeenCalledWith('emulator-5554', init.signal);
262
261
  expect(startEmulator).toHaveBeenNthCalledWith(2, 'Pixel_8_API_35', 'snapshot-reuse');
263
262
  });
264
- it('installs the app from HARNESS_APP_PATH when missing', async () => {
265
- const appPath = path.join(os.tmpdir(), 'HarnessPlayground.apk');
266
- fs.writeFileSync(appPath, 'apk');
267
- vi.stubEnv('HARNESS_APP_PATH', appPath);
268
- vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
269
- vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
270
- vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
271
- vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
272
- vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(false);
273
- const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
274
- vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
275
- vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
276
- vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
277
- vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
278
- await expect(getAndroidEmulatorPlatformInstance({
279
- name: 'android',
280
- device: {
281
- type: 'emulator',
282
- name: 'Pixel_8_API_35',
283
- avd: {
284
- apiLevel: 35,
285
- profile: 'pixel_8',
286
- diskSize: '1G',
287
- heapSize: '1G',
288
- },
289
- },
290
- bundleId: 'com.harnessplayground',
291
- activityName: '.MainActivity',
292
- }, harnessConfig, init)).resolves.toBeDefined();
293
- expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
294
- fs.rmSync(appPath, { force: true });
295
- });
296
263
  it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
297
264
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
298
265
  vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
@@ -420,4 +387,98 @@ describe('Android platform instance', () => {
420
387
  expect(appMonitor.addListener(listener)).toBeUndefined();
421
388
  expect(appMonitor.removeListener(listener)).toBeUndefined();
422
389
  });
390
+ it('grants permissions when permissions are enabled for emulator', async () => {
391
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
392
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
393
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
394
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
395
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
396
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
397
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
398
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
399
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
400
+ const grantPermissions = vi
401
+ .spyOn(adb, 'grantPermissions')
402
+ .mockResolvedValue(undefined);
403
+ const harnessConfigWithPermissions = {
404
+ ...harnessConfig,
405
+ permissions: true,
406
+ };
407
+ await getAndroidEmulatorPlatformInstance({
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
+ }, harnessConfigWithPermissions, init);
422
+ expect(grantPermissions).toHaveBeenCalledWith('emulator-5554', 'com.harnessplayground');
423
+ });
424
+ it('does not grant permissions when permissions are disabled for emulator', async () => {
425
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
426
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
427
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
428
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
429
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
430
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
431
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
432
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
433
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
434
+ const grantPermissions = vi
435
+ .spyOn(adb, 'grantPermissions')
436
+ .mockResolvedValue(undefined);
437
+ await getAndroidEmulatorPlatformInstance({
438
+ name: 'android',
439
+ device: {
440
+ type: 'emulator',
441
+ name: 'Pixel_8_API_35',
442
+ avd: {
443
+ apiLevel: 35,
444
+ profile: 'pixel_8',
445
+ diskSize: '1G',
446
+ heapSize: '1G',
447
+ },
448
+ },
449
+ bundleId: 'com.harnessplayground',
450
+ activityName: '.MainActivity',
451
+ }, harnessConfig, init);
452
+ expect(grantPermissions).not.toHaveBeenCalled();
453
+ });
454
+ it('grants permissions when permissions are enabled for physical device', async () => {
455
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
456
+ vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
457
+ manufacturer: 'motorola',
458
+ model: 'moto g72',
459
+ });
460
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
461
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
462
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
463
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
464
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
465
+ const grantPermissions = vi
466
+ .spyOn(adb, 'grantPermissions')
467
+ .mockResolvedValue(undefined);
468
+ const harnessConfigWithPermissions = {
469
+ ...harnessConfig,
470
+ permissions: true,
471
+ };
472
+ await getAndroidPhysicalDevicePlatformInstance({
473
+ name: 'android-device',
474
+ device: {
475
+ type: 'physical',
476
+ manufacturer: 'motorola',
477
+ model: 'moto g72',
478
+ },
479
+ bundleId: 'com.harnessplayground',
480
+ activityName: '.MainActivity',
481
+ }, harnessConfigWithPermissions);
482
+ expect(grantPermissions).toHaveBeenCalledWith('012345', 'com.harnessplayground');
483
+ });
423
484
  });
@@ -0,0 +1,16 @@
1
+ export declare class AdbError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class AdbDeviceNotFoundError extends AdbError {
5
+ constructor(adbId: string);
6
+ }
7
+ export declare class AdbAppNotInstalledError extends AdbError {
8
+ constructor(bundleId: string, adbId: string);
9
+ }
10
+ export declare class AdbPermissionGrantError extends AdbError {
11
+ constructor(bundleId: string, permissions: string[], adbId: string);
12
+ }
13
+ export declare class AdbBinaryNotFoundError extends AdbError {
14
+ constructor();
15
+ }
16
+ //# sourceMappingURL=adb-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb-errors.d.ts","sourceRoot":"","sources":["../src/adb-errors.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAS,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,sBAAuB,SAAQ,QAAQ;gBACtC,KAAK,EAAE,MAAM;CAO1B;AAED,qBAAa,uBAAwB,SAAQ,QAAQ;gBACvC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAO5C;AAED,qBAAa,uBAAwB,SAAQ,QAAQ;gBACvC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM;CAQnE;AAED,qBAAa,sBAAuB,SAAQ,QAAQ;;CAQnD"}