@react-native-harness/platform-android 1.1.0-rc.2 → 1.1.0-rc.4

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.
Files changed (87) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/adb.test.js +283 -10
  3. package/dist/__tests__/avd-config.test.d.ts +2 -0
  4. package/dist/__tests__/avd-config.test.d.ts.map +1 -0
  5. package/dist/__tests__/avd-config.test.js +174 -0
  6. package/dist/__tests__/ci-action.test.d.ts +2 -0
  7. package/dist/__tests__/ci-action.test.d.ts.map +1 -0
  8. package/dist/__tests__/ci-action.test.js +46 -0
  9. package/dist/__tests__/emulator-startup.test.d.ts +2 -0
  10. package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
  11. package/dist/__tests__/emulator-startup.test.js +19 -0
  12. package/dist/__tests__/environment.test.d.ts +2 -0
  13. package/dist/__tests__/environment.test.d.ts.map +1 -0
  14. package/dist/__tests__/environment.test.js +117 -0
  15. package/dist/__tests__/instance.test.d.ts +2 -0
  16. package/dist/__tests__/instance.test.d.ts.map +1 -0
  17. package/dist/__tests__/instance.test.js +423 -0
  18. package/dist/__tests__/targets.test.d.ts +2 -0
  19. package/dist/__tests__/targets.test.d.ts.map +1 -0
  20. package/dist/__tests__/targets.test.js +49 -0
  21. package/dist/adb.d.ts +23 -0
  22. package/dist/adb.d.ts.map +1 -1
  23. package/dist/adb.js +259 -16
  24. package/dist/app-monitor.d.ts.map +1 -1
  25. package/dist/app-monitor.js +27 -7
  26. package/dist/assertions.d.ts +5 -0
  27. package/dist/assertions.d.ts.map +1 -0
  28. package/dist/assertions.js +6 -0
  29. package/dist/avd-config.d.ts +41 -0
  30. package/dist/avd-config.d.ts.map +1 -0
  31. package/dist/avd-config.js +173 -0
  32. package/dist/config.d.ts +77 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +5 -0
  35. package/dist/emulator-startup.d.ts +3 -0
  36. package/dist/emulator-startup.d.ts.map +1 -0
  37. package/dist/emulator-startup.js +17 -0
  38. package/dist/emulator.d.ts +6 -0
  39. package/dist/emulator.d.ts.map +1 -0
  40. package/dist/emulator.js +27 -0
  41. package/dist/environment.d.ts +31 -0
  42. package/dist/environment.d.ts.map +1 -0
  43. package/dist/environment.js +317 -0
  44. package/dist/errors.d.ts +7 -0
  45. package/dist/errors.d.ts.map +1 -0
  46. package/dist/errors.js +14 -0
  47. package/dist/factory.d.ts.map +1 -1
  48. package/dist/factory.js +3 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -0
  52. package/dist/instance.d.ts +6 -0
  53. package/dist/instance.d.ts.map +1 -0
  54. package/dist/instance.js +232 -0
  55. package/dist/reader.d.ts +6 -0
  56. package/dist/reader.d.ts.map +1 -0
  57. package/dist/reader.js +57 -0
  58. package/dist/runner.d.ts +2 -2
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +9 -52
  61. package/dist/targets.d.ts +1 -1
  62. package/dist/targets.d.ts.map +1 -1
  63. package/dist/targets.js +4 -0
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +381 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +107 -0
  68. package/package.json +4 -4
  69. package/src/__tests__/adb.test.ts +419 -15
  70. package/src/__tests__/avd-config.test.ts +206 -0
  71. package/src/__tests__/ci-action.test.ts +81 -0
  72. package/src/__tests__/emulator-startup.test.ts +32 -0
  73. package/src/__tests__/environment.test.ts +212 -0
  74. package/src/__tests__/instance.test.ts +610 -0
  75. package/src/__tests__/targets.test.ts +53 -0
  76. package/src/adb.ts +430 -28
  77. package/src/app-monitor.ts +56 -18
  78. package/src/avd-config.ts +290 -0
  79. package/src/config.ts +8 -0
  80. package/src/emulator-startup.ts +28 -0
  81. package/src/environment.ts +554 -0
  82. package/src/errors.ts +19 -0
  83. package/src/factory.ts +4 -0
  84. package/src/index.ts +7 -1
  85. package/src/instance.ts +380 -0
  86. package/src/runner.ts +19 -70
  87. package/src/targets.ts +18 -8
@@ -0,0 +1,610 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ DEFAULT_METRO_PORT,
4
+ type Config as HarnessConfig,
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
+ import {
10
+ getAndroidEmulatorPlatformInstance,
11
+ getAndroidPhysicalDevicePlatformInstance,
12
+ } from '../instance.js';
13
+ import * as adb from '../adb.js';
14
+ import * as avdConfig from '../avd-config.js';
15
+ import * as sharedPrefs from '../shared-prefs.js';
16
+ import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js';
17
+
18
+ const harnessConfig = {
19
+ metroPort: DEFAULT_METRO_PORT,
20
+ } as HarnessConfig;
21
+ const harnessConfigWithoutNativeCrashDetection = {
22
+ metroPort: DEFAULT_METRO_PORT,
23
+ detectNativeCrashes: false,
24
+ } as HarnessConfig;
25
+ const init = {
26
+ signal: new AbortController().signal,
27
+ };
28
+
29
+ describe('Android platform instance', () => {
30
+ beforeEach(() => {
31
+ vi.restoreAllMocks();
32
+ vi.unstubAllEnvs();
33
+ });
34
+
35
+ it('reuses a running emulator and does not shut it down on dispose', async () => {
36
+ const ensureAndroidEmulatorEnvironment = vi
37
+ .spyOn(
38
+ await import('../environment.js'),
39
+ 'ensureAndroidEmulatorEnvironment',
40
+ )
41
+ .mockResolvedValue('/tmp/android-sdk');
42
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
43
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
44
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
45
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
46
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
47
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
48
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
49
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
50
+ undefined,
51
+ );
52
+ vi.spyOn(sharedPrefs, 'clearHarnessDebugHttpHost').mockResolvedValue(
53
+ undefined,
54
+ );
55
+ vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
56
+ const stopEmulator = vi.spyOn(adb, 'stopEmulator').mockResolvedValue();
57
+
58
+ const instance = await getAndroidEmulatorPlatformInstance(
59
+ {
60
+ name: 'android',
61
+ device: {
62
+ type: 'emulator',
63
+ name: 'Pixel_8_API_35',
64
+ avd: {
65
+ apiLevel: 35,
66
+ profile: 'pixel_8',
67
+ diskSize: '1G',
68
+ heapSize: '1G',
69
+ },
70
+ },
71
+ bundleId: 'com.harnessplayground',
72
+ activityName: '.MainActivity',
73
+ },
74
+ harnessConfig,
75
+ init,
76
+ );
77
+
78
+ await instance.dispose();
79
+
80
+ expect(ensureAndroidEmulatorEnvironment).not.toHaveBeenCalled();
81
+ expect(stopEmulator).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('creates and boots an emulator when missing and shuts it down on dispose', async () => {
85
+ vi.spyOn(
86
+ await import('../environment.js'),
87
+ 'ensureAndroidEmulatorEnvironment',
88
+ ).mockResolvedValue('/tmp/android-sdk');
89
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
90
+ vi.spyOn(adb, 'hasAvd').mockResolvedValue(false);
91
+ const createAvd = vi.spyOn(adb, 'createAvd').mockResolvedValue(undefined);
92
+ const startEmulator = vi
93
+ .spyOn(adb, 'startEmulator')
94
+ .mockResolvedValue(undefined);
95
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
96
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
97
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
98
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
99
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
100
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
101
+ undefined,
102
+ );
103
+ vi.spyOn(sharedPrefs, 'clearHarnessDebugHttpHost').mockResolvedValue(
104
+ undefined,
105
+ );
106
+ vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined);
107
+ const stopEmulator = vi.spyOn(adb, 'stopEmulator').mockResolvedValue();
108
+
109
+ const instance = await getAndroidEmulatorPlatformInstance(
110
+ {
111
+ name: 'android',
112
+ device: {
113
+ type: 'emulator',
114
+ name: 'Pixel_8_API_35',
115
+ avd: {
116
+ apiLevel: 35,
117
+ profile: 'pixel_8',
118
+ diskSize: '1G',
119
+ heapSize: '1G',
120
+ },
121
+ },
122
+ bundleId: 'com.harnessplayground',
123
+ activityName: '.MainActivity',
124
+ },
125
+ harnessConfig,
126
+ init,
127
+ );
128
+
129
+ expect(createAvd).toHaveBeenCalledWith({
130
+ name: 'Pixel_8_API_35',
131
+ apiLevel: 35,
132
+ profile: 'pixel_8',
133
+ diskSize: '1G',
134
+ heapSize: '1G',
135
+ });
136
+ expect(startEmulator).toHaveBeenCalledWith('Pixel_8_API_35', undefined);
137
+
138
+ await instance.dispose();
139
+
140
+ expect(stopEmulator).toHaveBeenCalledWith('emulator-5554');
141
+ });
142
+
143
+ it('verifies SDK assets before booting an existing AVD', async () => {
144
+ const ensureAndroidEmulatorEnvironment = vi
145
+ .spyOn(
146
+ await import('../environment.js'),
147
+ 'ensureAndroidEmulatorEnvironment',
148
+ )
149
+ .mockResolvedValue('/tmp/android-sdk');
150
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
151
+ vi.spyOn(adb, 'hasAvd').mockResolvedValue(true);
152
+ const createAvd = vi.spyOn(adb, 'createAvd').mockResolvedValue(undefined);
153
+ const startEmulator = vi
154
+ .spyOn(adb, 'startEmulator')
155
+ .mockResolvedValue(undefined);
156
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
157
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
158
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
159
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
160
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
161
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
162
+ undefined,
163
+ );
164
+
165
+ await expect(
166
+ getAndroidEmulatorPlatformInstance(
167
+ {
168
+ name: 'android',
169
+ device: {
170
+ type: 'emulator',
171
+ name: 'Pixel_8_API_35',
172
+ avd: {
173
+ apiLevel: 35,
174
+ profile: 'pixel_8',
175
+ diskSize: '1G',
176
+ heapSize: '1G',
177
+ },
178
+ },
179
+ bundleId: 'com.harnessplayground',
180
+ activityName: '.MainActivity',
181
+ },
182
+ harnessConfig,
183
+ init,
184
+ ),
185
+ ).resolves.toBeDefined();
186
+
187
+ expect(ensureAndroidEmulatorEnvironment).toHaveBeenCalledWith(35);
188
+ expect(createAvd).not.toHaveBeenCalled();
189
+ expect(startEmulator).toHaveBeenCalledWith('Pixel_8_API_35', undefined);
190
+ });
191
+
192
+ it('reuses a compatible cached AVD snapshot when caching is enabled', async () => {
193
+ vi.stubEnv('HARNESS_AVD_CACHING', 'true');
194
+ vi.spyOn(
195
+ await import('../environment.js'),
196
+ 'ensureAndroidEmulatorEnvironment',
197
+ ).mockResolvedValue('/tmp/android-sdk');
198
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
199
+ vi.spyOn(adb, 'hasAvd').mockResolvedValue(true);
200
+ vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({
201
+ imageSysdir1: 'system-images/android-35/default/arm64-v8a/',
202
+ abiType: 'arm64-v8a',
203
+ hwDeviceName: 'pixel_8',
204
+ diskDataPartitionSize: '1G',
205
+ vmHeapSize: '1G',
206
+ });
207
+ vi.spyOn(adb, 'startEmulator').mockResolvedValue(undefined);
208
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
209
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
210
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
211
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
212
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
213
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
214
+ undefined,
215
+ );
216
+
217
+ await expect(
218
+ getAndroidEmulatorPlatformInstance(
219
+ {
220
+ name: 'android',
221
+ device: {
222
+ type: 'emulator',
223
+ name: 'Pixel_8_API_35',
224
+ avd: {
225
+ apiLevel: 35,
226
+ profile: 'pixel_8',
227
+ diskSize: '1G',
228
+ heapSize: '1G',
229
+ snapshot: { enabled: false },
230
+ },
231
+ },
232
+ bundleId: 'com.harnessplayground',
233
+ activityName: '.MainActivity',
234
+ },
235
+ harnessConfig,
236
+ init,
237
+ ),
238
+ ).resolves.toBeDefined();
239
+
240
+ expect(adb.startEmulator).toHaveBeenCalledTimes(1);
241
+ expect(adb.startEmulator).toHaveBeenCalledWith(
242
+ 'Pixel_8_API_35',
243
+ 'snapshot-reuse',
244
+ );
245
+ });
246
+
247
+ it('recreates an incompatible cached AVD before the real boot', async () => {
248
+ vi.stubEnv('HARNESS_AVD_CACHING', 'true');
249
+ vi.spyOn(
250
+ await import('../environment.js'),
251
+ 'ensureAndroidEmulatorEnvironment',
252
+ ).mockResolvedValue('/tmp/android-sdk');
253
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
254
+ vi.spyOn(adb, 'hasAvd').mockResolvedValue(true);
255
+ vi.spyOn(avdConfig, 'readAvdConfig').mockResolvedValue({
256
+ imageSysdir1: 'system-images/android-34/default/x86_64/',
257
+ abiType: 'x86_64',
258
+ hwDeviceName: 'pixel_7',
259
+ diskDataPartitionSize: '2G',
260
+ vmHeapSize: '2G',
261
+ });
262
+ const deleteAvd = vi.spyOn(adb, 'deleteAvd').mockResolvedValue(undefined);
263
+ const createAvd = vi.spyOn(adb, 'createAvd').mockResolvedValue(undefined);
264
+ vi.spyOn(adb, 'startEmulator').mockResolvedValue(undefined);
265
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
266
+ const stopEmulator = vi.spyOn(adb, 'stopEmulator').mockResolvedValue();
267
+ const waitForEmulatorDisconnect = vi
268
+ .spyOn(adb, 'waitForEmulatorDisconnect')
269
+ .mockResolvedValue(undefined);
270
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
271
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
272
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
273
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
274
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
275
+ undefined,
276
+ );
277
+
278
+ await expect(
279
+ getAndroidEmulatorPlatformInstance(
280
+ {
281
+ name: 'android',
282
+ device: {
283
+ type: 'emulator',
284
+ name: 'Pixel_8_API_35',
285
+ avd: {
286
+ apiLevel: 35,
287
+ profile: 'pixel_8',
288
+ diskSize: '1G',
289
+ heapSize: '1G',
290
+ snapshot: { enabled: true },
291
+ },
292
+ },
293
+ bundleId: 'com.harnessplayground',
294
+ activityName: '.MainActivity',
295
+ },
296
+ harnessConfig,
297
+ init,
298
+ ),
299
+ ).resolves.toBeDefined();
300
+
301
+ expect(deleteAvd).toHaveBeenCalledWith('Pixel_8_API_35');
302
+ expect(createAvd).toHaveBeenCalled();
303
+ expect(stopEmulator).toHaveBeenCalledWith('emulator-5554');
304
+ expect(waitForEmulatorDisconnect).toHaveBeenCalledWith(
305
+ 'emulator-5554',
306
+ init.signal,
307
+ );
308
+ expect(adb.startEmulator).toHaveBeenNthCalledWith(
309
+ 1,
310
+ 'Pixel_8_API_35',
311
+ 'clean-snapshot-generation',
312
+ );
313
+ expect(adb.startEmulator).toHaveBeenNthCalledWith(
314
+ 2,
315
+ 'Pixel_8_API_35',
316
+ 'snapshot-reuse',
317
+ );
318
+ });
319
+
320
+ it('generates a snapshot on first run before the test boot', async () => {
321
+ vi.stubEnv('HARNESS_AVD_CACHING', 'true');
322
+ vi.spyOn(
323
+ await import('../environment.js'),
324
+ 'ensureAndroidEmulatorEnvironment',
325
+ ).mockResolvedValue('/tmp/android-sdk');
326
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
327
+ vi.spyOn(adb, 'hasAvd').mockResolvedValue(false);
328
+ vi.spyOn(adb, 'createAvd').mockResolvedValue(undefined);
329
+ const startEmulator = vi
330
+ .spyOn(adb, 'startEmulator')
331
+ .mockResolvedValue(undefined);
332
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
333
+ const stopEmulator = vi.spyOn(adb, 'stopEmulator').mockResolvedValue();
334
+ const waitForEmulatorDisconnect = vi
335
+ .spyOn(adb, 'waitForEmulatorDisconnect')
336
+ .mockResolvedValue(undefined);
337
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
338
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
339
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
340
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
341
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
342
+ undefined,
343
+ );
344
+
345
+ await expect(
346
+ getAndroidEmulatorPlatformInstance(
347
+ {
348
+ name: 'android',
349
+ device: {
350
+ type: 'emulator',
351
+ name: 'Pixel_8_API_35',
352
+ avd: {
353
+ apiLevel: 35,
354
+ profile: 'pixel_8',
355
+ diskSize: '1G',
356
+ heapSize: '1G',
357
+ snapshot: { enabled: true },
358
+ },
359
+ },
360
+ bundleId: 'com.harnessplayground',
361
+ activityName: '.MainActivity',
362
+ },
363
+ harnessConfig,
364
+ init,
365
+ ),
366
+ ).resolves.toBeDefined();
367
+
368
+ expect(startEmulator).toHaveBeenNthCalledWith(
369
+ 1,
370
+ 'Pixel_8_API_35',
371
+ 'clean-snapshot-generation',
372
+ );
373
+ expect(stopEmulator).toHaveBeenCalledWith('emulator-5554');
374
+ expect(waitForEmulatorDisconnect).toHaveBeenCalledWith(
375
+ 'emulator-5554',
376
+ init.signal,
377
+ );
378
+ expect(startEmulator).toHaveBeenNthCalledWith(
379
+ 2,
380
+ 'Pixel_8_API_35',
381
+ 'snapshot-reuse',
382
+ );
383
+ });
384
+
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
+ it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
433
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
434
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
435
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
436
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(false);
437
+
438
+ await expect(
439
+ getAndroidEmulatorPlatformInstance(
440
+ {
441
+ name: 'android',
442
+ device: {
443
+ type: 'emulator',
444
+ name: 'Pixel_8_API_35',
445
+ avd: {
446
+ apiLevel: 35,
447
+ profile: 'pixel_8',
448
+ diskSize: '1G',
449
+ heapSize: '1G',
450
+ },
451
+ },
452
+ bundleId: 'com.harnessplayground',
453
+ activityName: '.MainActivity',
454
+ },
455
+ harnessConfig,
456
+ init,
457
+ ),
458
+ ).rejects.toBeInstanceOf(HarnessAppPathError);
459
+ });
460
+
461
+ it('throws a HarnessAppPathError when HARNESS_APP_PATH points to a missing app', async () => {
462
+ vi.stubEnv('HARNESS_APP_PATH', '/tmp/missing.apk');
463
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
464
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
465
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
466
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(false);
467
+
468
+ await expect(
469
+ getAndroidEmulatorPlatformInstance(
470
+ {
471
+ name: 'android',
472
+ device: {
473
+ type: 'emulator',
474
+ name: 'Pixel_8_API_35',
475
+ avd: {
476
+ apiLevel: 35,
477
+ profile: 'pixel_8',
478
+ diskSize: '1G',
479
+ heapSize: '1G',
480
+ },
481
+ },
482
+ bundleId: 'com.harnessplayground',
483
+ activityName: '.MainActivity',
484
+ },
485
+ harnessConfig,
486
+ init,
487
+ ),
488
+ ).rejects.toBeInstanceOf(HarnessAppPathError);
489
+ });
490
+
491
+ it('throws a HarnessEmulatorConfigError when the emulator is missing and avd config is absent', async () => {
492
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
493
+
494
+ await expect(
495
+ getAndroidEmulatorPlatformInstance(
496
+ {
497
+ name: 'android',
498
+ device: {
499
+ type: 'emulator',
500
+ name: 'Pixel_8_API_35',
501
+ },
502
+ bundleId: 'com.harnessplayground',
503
+ activityName: '.MainActivity',
504
+ },
505
+ harnessConfig,
506
+ init,
507
+ ),
508
+ ).rejects.toBeInstanceOf(HarnessEmulatorConfigError);
509
+ });
510
+
511
+ it('returns a noop emulator app monitor when native crash detection is disabled', async () => {
512
+ vi.spyOn(
513
+ await import('../environment.js'),
514
+ 'ensureAndroidEmulatorEnvironment',
515
+ ).mockResolvedValue('/tmp/android-sdk');
516
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
517
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
518
+ vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
519
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
520
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
521
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
522
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
523
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
524
+ undefined,
525
+ );
526
+
527
+ const instance = await getAndroidEmulatorPlatformInstance(
528
+ {
529
+ name: 'android',
530
+ device: {
531
+ type: 'emulator',
532
+ name: 'Pixel_8_API_35',
533
+ avd: {
534
+ apiLevel: 35,
535
+ profile: 'pixel_8',
536
+ diskSize: '1G',
537
+ heapSize: '1G',
538
+ },
539
+ },
540
+ bundleId: 'com.harnessplayground',
541
+ activityName: '.MainActivity',
542
+ },
543
+ harnessConfigWithoutNativeCrashDetection,
544
+ init,
545
+ );
546
+
547
+ const listener = vi.fn();
548
+ const appMonitor = instance.createAppMonitor();
549
+
550
+ await expect(appMonitor.start()).resolves.toBeUndefined();
551
+ await expect(appMonitor.stop()).resolves.toBeUndefined();
552
+ await expect(appMonitor.dispose()).resolves.toBeUndefined();
553
+ expect(appMonitor.addListener(listener)).toBeUndefined();
554
+ expect(appMonitor.removeListener(listener)).toBeUndefined();
555
+ });
556
+
557
+ it('returns a noop physical device app monitor when native crash detection is disabled', async () => {
558
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
559
+ vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
560
+ manufacturer: 'motorola',
561
+ model: 'moto g72',
562
+ });
563
+ vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
564
+ vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
565
+ vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
566
+ vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
567
+ vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
568
+ undefined,
569
+ );
570
+
571
+ await expect(
572
+ getAndroidPhysicalDevicePlatformInstance(
573
+ {
574
+ name: 'android-device',
575
+ device: {
576
+ type: 'physical',
577
+ manufacturer: 'motorola',
578
+ model: 'moto g72',
579
+ },
580
+ bundleId: 'com.harnessplayground',
581
+ activityName: '.MainActivity',
582
+ },
583
+ harnessConfigWithoutNativeCrashDetection,
584
+ ),
585
+ ).resolves.toBeDefined();
586
+
587
+ const instance = await getAndroidPhysicalDevicePlatformInstance(
588
+ {
589
+ name: 'android-device',
590
+ device: {
591
+ type: 'physical',
592
+ manufacturer: 'motorola',
593
+ model: 'moto g72',
594
+ },
595
+ bundleId: 'com.harnessplayground',
596
+ activityName: '.MainActivity',
597
+ },
598
+ harnessConfigWithoutNativeCrashDetection,
599
+ );
600
+
601
+ const listener = vi.fn();
602
+ const appMonitor = instance.createAppMonitor();
603
+
604
+ await expect(appMonitor.start()).resolves.toBeUndefined();
605
+ await expect(appMonitor.stop()).resolves.toBeUndefined();
606
+ await expect(appMonitor.dispose()).resolves.toBeUndefined();
607
+ expect(appMonitor.addListener(listener)).toBeUndefined();
608
+ expect(appMonitor.removeListener(listener)).toBeUndefined();
609
+ });
610
+ });
@@ -0,0 +1,53 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRunTargets } from '../targets.js';
3
+ import * as adb from '../adb.js';
4
+ import * as environment from '../environment.js';
5
+
6
+ describe('Android target discovery', () => {
7
+ beforeEach(() => {
8
+ vi.restoreAllMocks();
9
+ vi.unstubAllEnvs();
10
+ });
11
+
12
+ it('installs adb and emulator only for discovery', async () => {
13
+ const ensureAndroidAdbAvailable = vi
14
+ .spyOn(environment, 'ensureAndroidAdbAvailable')
15
+ .mockResolvedValue('/tmp/android-sdk');
16
+ const ensureAndroidEmulatorAvailable = vi
17
+ .spyOn(environment, 'ensureAndroidEmulatorAvailable')
18
+ .mockResolvedValue('/tmp/android-sdk');
19
+ vi.spyOn(adb, 'getAvds').mockResolvedValue(['Pixel_8_API_35']);
20
+ vi.spyOn(adb, 'getConnectedDevices').mockResolvedValue([
21
+ {
22
+ id: 'device-1',
23
+ manufacturer: 'Google',
24
+ model: 'Pixel 8',
25
+ },
26
+ ]);
27
+
28
+ await expect(getRunTargets()).resolves.toEqual([
29
+ {
30
+ type: 'emulator',
31
+ name: 'Pixel_8_API_35',
32
+ platform: 'android',
33
+ description: 'Android emulator',
34
+ device: {
35
+ name: 'Pixel_8_API_35',
36
+ },
37
+ },
38
+ {
39
+ type: 'physical',
40
+ name: 'Google Pixel 8',
41
+ platform: 'android',
42
+ description: 'Physical device (device-1)',
43
+ device: {
44
+ manufacturer: 'Google',
45
+ model: 'Pixel 8',
46
+ },
47
+ },
48
+ ]);
49
+
50
+ expect(ensureAndroidAdbAvailable).toHaveBeenCalledTimes(1);
51
+ expect(ensureAndroidEmulatorAvailable).toHaveBeenCalledTimes(1);
52
+ });
53
+ });