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