@react-native-harness/platform-android 1.1.0-rc.1 → 1.1.0-rc.3

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 (81) 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 +51 -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__/shared-prefs.test.d.ts +2 -0
  19. package/dist/__tests__/shared-prefs.test.d.ts.map +1 -0
  20. package/dist/__tests__/shared-prefs.test.js +87 -0
  21. package/dist/adb.d.ts +23 -0
  22. package/dist/adb.d.ts.map +1 -1
  23. package/dist/adb.js +265 -16
  24. package/dist/app-monitor.d.ts.map +1 -1
  25. package/dist/app-monitor.js +29 -8
  26. package/dist/avd-config.d.ts +41 -0
  27. package/dist/avd-config.d.ts.map +1 -0
  28. package/dist/avd-config.js +173 -0
  29. package/dist/config.d.ts +77 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +5 -0
  32. package/dist/emulator-startup.d.ts +3 -0
  33. package/dist/emulator-startup.d.ts.map +1 -0
  34. package/dist/emulator-startup.js +17 -0
  35. package/dist/environment.d.ts +28 -0
  36. package/dist/environment.d.ts.map +1 -0
  37. package/dist/environment.js +295 -0
  38. package/dist/errors.d.ts +4 -12
  39. package/dist/errors.d.ts.map +1 -1
  40. package/dist/errors.js +10 -24
  41. package/dist/factory.d.ts.map +1 -1
  42. package/dist/factory.js +3 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -0
  46. package/dist/instance.d.ts +6 -0
  47. package/dist/instance.d.ts.map +1 -0
  48. package/dist/instance.js +234 -0
  49. package/dist/runner.d.ts +3 -3
  50. package/dist/runner.d.ts.map +1 -1
  51. package/dist/runner.js +12 -48
  52. package/dist/shared-prefs.d.ts +3 -0
  53. package/dist/shared-prefs.d.ts.map +1 -0
  54. package/dist/shared-prefs.js +92 -0
  55. package/dist/targets.d.ts +1 -1
  56. package/dist/targets.d.ts.map +1 -1
  57. package/dist/targets.js +2 -0
  58. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  59. package/package.json +4 -4
  60. package/src/__tests__/adb.test.ts +419 -15
  61. package/src/__tests__/avd-config.test.ts +206 -0
  62. package/src/__tests__/ci-action.test.ts +81 -0
  63. package/src/__tests__/emulator-startup.test.ts +32 -0
  64. package/src/__tests__/environment.test.ts +87 -0
  65. package/src/__tests__/instance.test.ts +610 -0
  66. package/src/__tests__/shared-prefs.test.ts +144 -0
  67. package/src/adb.ts +423 -16
  68. package/src/app-monitor.ts +58 -18
  69. package/src/avd-config.ts +290 -0
  70. package/src/config.ts +8 -0
  71. package/src/emulator-startup.ts +28 -0
  72. package/src/environment.ts +510 -0
  73. package/src/errors.ts +19 -0
  74. package/src/factory.ts +4 -0
  75. package/src/index.ts +7 -1
  76. package/src/instance.ts +380 -0
  77. package/src/runner.ts +25 -63
  78. package/src/shared-prefs.ts +205 -0
  79. package/src/targets.ts +11 -8
  80. package/tsconfig.json +2 -2
  81. package/tsconfig.lib.json +2 -2
@@ -1,6 +1,43 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { getAppUid, getLogcatTimestamp, getStartAppArgs } from '../adb.js';
1
+ import { EventEmitter } from 'node:events';
2
+ import { PassThrough } from 'node:stream';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { SubprocessError } from '@react-native-harness/tools';
5
+ import {
6
+ createAvd,
7
+ deleteAvd,
8
+ emulatorProcess,
9
+ getAppUid,
10
+ getLogcatTimestamp,
11
+ getStartAppArgs,
12
+ hasAvd,
13
+ installApp,
14
+ startEmulator,
15
+ waitForBoot,
16
+ waitForEmulatorDisconnect,
17
+ } from '../adb.js';
3
18
  import * as tools from '@react-native-harness/tools';
19
+ import * as environment from '../environment.js';
20
+
21
+ const createAbortError = () =>
22
+ new DOMException('The operation was aborted', 'AbortError');
23
+
24
+ const createMockChildProcess = () => {
25
+ const process = new EventEmitter() as EventEmitter & {
26
+ stdout: PassThrough;
27
+ stderr: PassThrough;
28
+ unref: ReturnType<typeof vi.fn>;
29
+ };
30
+
31
+ process.stdout = new PassThrough();
32
+ process.stderr = new PassThrough();
33
+ process.unref = vi.fn();
34
+
35
+ return process;
36
+ };
37
+
38
+ beforeEach(() => {
39
+ vi.restoreAllMocks();
40
+ });
4
41
 
5
42
  describe('getStartAppArgs', () => {
6
43
  it('maps supported extras to adb am start flags', () => {
@@ -45,18 +82,16 @@ describe('getStartAppArgs', () => {
45
82
  });
46
83
 
47
84
  it('extracts app uid from pm list packages output', async () => {
48
- const spawnSpy = vi
49
- .spyOn(tools, 'spawn')
50
- .mockResolvedValueOnce({
51
- stdout:
52
- 'package:com.other.app uid:10123\npackage:com.example.app uid:10234\n',
53
- } as Awaited<ReturnType<typeof tools.spawn>>);
85
+ const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
86
+ stdout:
87
+ 'package:com.other.app uid:10123\npackage:com.example.app uid:10234\n',
88
+ } as Awaited<ReturnType<typeof tools.spawn>>);
54
89
 
55
90
  await expect(getAppUid('emulator-5554', 'com.example.app')).resolves.toBe(
56
91
  10234
57
92
  );
58
93
 
59
- expect(spawnSpy).toHaveBeenCalledWith('adb', [
94
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
60
95
  '-s',
61
96
  'emulator-5554',
62
97
  'shell',
@@ -68,17 +103,15 @@ describe('getStartAppArgs', () => {
68
103
  });
69
104
 
70
105
  it('reads the device timestamp in logcat format', async () => {
71
- const spawnSpy = vi
72
- .spyOn(tools, 'spawn')
73
- .mockResolvedValueOnce({
74
- stdout: "'03-12 11:35:08.000'\n",
75
- } as Awaited<ReturnType<typeof tools.spawn>>);
106
+ const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
107
+ stdout: "'03-12 11:35:08.000'\n",
108
+ } as Awaited<ReturnType<typeof tools.spawn>>);
76
109
 
77
110
  await expect(getLogcatTimestamp('emulator-5554')).resolves.toBe(
78
111
  '03-12 11:35:08.000'
79
112
  );
80
113
 
81
- expect(spawnSpy).toHaveBeenCalledWith('adb', [
114
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
82
115
  '-s',
83
116
  'emulator-5554',
84
117
  'shell',
@@ -86,4 +119,375 @@ describe('getStartAppArgs', () => {
86
119
  "+'%m-%d %H:%M:%S.000'",
87
120
  ]);
88
121
  });
122
+
123
+ it('checks whether an AVD exists', async () => {
124
+ vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
125
+ stdout: 'Pixel_6_API_33\nPixel_8_API_35\n',
126
+ } as Awaited<ReturnType<typeof tools.spawn>>);
127
+
128
+ await expect(hasAvd('Pixel_8_API_35')).resolves.toBe(true);
129
+ await expect(hasAvd('Missing_AVD')).resolves.toBe(false);
130
+ });
131
+
132
+ it('installs the app via adb', async () => {
133
+ const spawnSpy = vi
134
+ .spyOn(tools, 'spawn')
135
+ .mockResolvedValueOnce({} as Awaited<ReturnType<typeof tools.spawn>>);
136
+
137
+ await installApp('emulator-5554', '/tmp/app.apk');
138
+
139
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
140
+ '-s',
141
+ 'emulator-5554',
142
+ 'install',
143
+ '-r',
144
+ '/tmp/app.apk',
145
+ ]);
146
+ });
147
+
148
+ it('creates an AVD and appends config overrides', async () => {
149
+ const spawnSpy = vi
150
+ .spyOn(tools, 'spawn')
151
+ .mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>);
152
+ const verifyAndroidEmulatorSdk = vi
153
+ .spyOn(environment, 'ensureAndroidSdkPackages')
154
+ .mockResolvedValue('/tmp/android-sdk');
155
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
156
+ 'x86_64'
157
+ );
158
+
159
+ await createAvd({
160
+ name: 'Pixel_8_API_35',
161
+ apiLevel: 35,
162
+ profile: 'pixel_8',
163
+ diskSize: '1G',
164
+ heapSize: '1G',
165
+ });
166
+
167
+ expect(verifyAndroidEmulatorSdk).toHaveBeenCalledWith([
168
+ 'platform-tools',
169
+ 'emulator',
170
+ 'platforms;android-35',
171
+ 'system-images;android-35;default;x86_64',
172
+ ]);
173
+ expect(spawnSpy).toHaveBeenNthCalledWith(1, 'bash', [
174
+ '-lc',
175
+ expect.stringContaining(
176
+ 'create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8"'
177
+ ),
178
+ ]);
179
+ expect(spawnSpy).toHaveBeenNthCalledWith(2, 'bash', [
180
+ '-lc',
181
+ expect.stringContaining(
182
+ `'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `
183
+ ),
184
+ ]);
185
+ });
186
+
187
+ it('creates an AVD with arm64 system image packages on arm64 hosts', async () => {
188
+ vi.spyOn(tools, 'spawn').mockResolvedValue(
189
+ {} as Awaited<ReturnType<typeof tools.spawn>>
190
+ );
191
+ const ensureAndroidSdkPackages = vi
192
+ .spyOn(environment, 'ensureAndroidSdkPackages')
193
+ .mockResolvedValue('/tmp/android-sdk');
194
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
195
+ 'arm64-v8a'
196
+ );
197
+
198
+ await createAvd({
199
+ name: 'Pixel_8_API_35',
200
+ apiLevel: 35,
201
+ profile: 'pixel_8',
202
+ diskSize: '1G',
203
+ heapSize: '1G',
204
+ });
205
+
206
+ expect(ensureAndroidSdkPackages).toHaveBeenCalledWith([
207
+ 'platform-tools',
208
+ 'emulator',
209
+ 'platforms;android-35',
210
+ 'system-images;android-35;default;arm64-v8a',
211
+ ]);
212
+ });
213
+
214
+ it.skip('deletes both AVD directory and ini file', async () => {
215
+ await deleteAvd('Pixel_8_API_35');
216
+ });
217
+
218
+ it('surfaces emulator stdout when startup fails immediately', async () => {
219
+ const child = createMockChildProcess();
220
+ let launcherReadyResolve: (() => void) | undefined;
221
+ const launcherReady = new Promise<void>((resolve) => {
222
+ launcherReadyResolve = resolve;
223
+ });
224
+
225
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
226
+ stdout: 'List of devices attached\n\n',
227
+ } as Awaited<ReturnType<typeof tools.spawn>>);
228
+ vi.spyOn(emulatorProcess, 'startDetachedProcess').mockImplementation(() => {
229
+ launcherReadyResolve?.();
230
+ return child as unknown as ReturnType<
231
+ typeof emulatorProcess.startDetachedProcess
232
+ >;
233
+ });
234
+
235
+ const startPromise = startEmulator('Pixel_8_API_35');
236
+ await launcherReady;
237
+
238
+ child.stdout.write('Unknown AVD name [Pixel_8_API_35]\n');
239
+ child.stdout.end();
240
+ child.stderr.end();
241
+ child.emit('close', 1, null);
242
+
243
+ await expect(startPromise).rejects.toThrow(
244
+ 'Unknown AVD name [Pixel_8_API_35]'
245
+ );
246
+ });
247
+
248
+ it('surfaces emulator stderr when startup fails immediately', async () => {
249
+ const child = createMockChildProcess();
250
+ let launcherReadyResolve: (() => void) | undefined;
251
+ const launcherReady = new Promise<void>((resolve) => {
252
+ launcherReadyResolve = resolve;
253
+ });
254
+
255
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
256
+ stdout: 'List of devices attached\n\n',
257
+ } as Awaited<ReturnType<typeof tools.spawn>>);
258
+ vi.spyOn(emulatorProcess, 'startDetachedProcess').mockImplementation(() => {
259
+ launcherReadyResolve?.();
260
+ return child as unknown as ReturnType<
261
+ typeof emulatorProcess.startDetachedProcess
262
+ >;
263
+ });
264
+
265
+ const startPromise = startEmulator('Pixel_8_API_35');
266
+ await launcherReady;
267
+
268
+ child.stderr.write('emulator: panic: broken config\n');
269
+ child.stdout.end();
270
+ child.stderr.end();
271
+ child.emit('close', 1, null);
272
+
273
+ await expect(startPromise).rejects.toThrow(
274
+ 'emulator: panic: broken config'
275
+ );
276
+ });
277
+
278
+ it('returns after the emulator appears without waiting for process exit', async () => {
279
+ vi.useFakeTimers();
280
+ const child = createMockChildProcess();
281
+ const spawnSpy = vi.spyOn(tools, 'spawn');
282
+
283
+ spawnSpy
284
+ .mockResolvedValueOnce({
285
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
286
+ } as Awaited<ReturnType<typeof tools.spawn>>)
287
+ .mockResolvedValueOnce({
288
+ stdout: 'Pixel_8_API_35\n',
289
+ } as Awaited<ReturnType<typeof tools.spawn>>);
290
+
291
+ vi.spyOn(emulatorProcess, 'startDetachedProcess').mockReturnValue(
292
+ child as unknown as ReturnType<
293
+ typeof emulatorProcess.startDetachedProcess
294
+ >
295
+ );
296
+
297
+ const startPromise = startEmulator('Pixel_8_API_35');
298
+
299
+ await vi.runAllTimersAsync();
300
+
301
+ await expect(startPromise).resolves.toBeUndefined();
302
+ expect(child.unref).toHaveBeenCalled();
303
+ });
304
+
305
+ it('passes default boot args to the emulator process', async () => {
306
+ vi.useFakeTimers();
307
+ const child = createMockChildProcess();
308
+ vi.spyOn(tools, 'spawn')
309
+ .mockResolvedValueOnce({
310
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
311
+ } as Awaited<ReturnType<typeof tools.spawn>>)
312
+ .mockResolvedValueOnce({
313
+ stdout: 'Pixel_8_API_35\n',
314
+ } as Awaited<ReturnType<typeof tools.spawn>>);
315
+ const startDetachedProcess = vi
316
+ .spyOn(emulatorProcess, 'startDetachedProcess')
317
+ .mockReturnValue(
318
+ child as unknown as ReturnType<
319
+ typeof emulatorProcess.startDetachedProcess
320
+ >
321
+ );
322
+
323
+ const startPromise = startEmulator('Pixel_8_API_35');
324
+ await vi.runAllTimersAsync();
325
+ await startPromise;
326
+
327
+ expect(startDetachedProcess).toHaveBeenCalledWith(
328
+ expect.stringMatching(/emulator$/),
329
+ expect.arrayContaining(['-no-snapshot-load', '-no-snapshot-save'])
330
+ );
331
+ });
332
+
333
+ it('passes clean snapshot generation args to the emulator process', async () => {
334
+ vi.useFakeTimers();
335
+ const child = createMockChildProcess();
336
+ vi.spyOn(tools, 'spawn')
337
+ .mockResolvedValueOnce({
338
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
339
+ } as Awaited<ReturnType<typeof tools.spawn>>)
340
+ .mockResolvedValueOnce({
341
+ stdout: 'Pixel_8_API_35\n',
342
+ } as Awaited<ReturnType<typeof tools.spawn>>);
343
+ const startDetachedProcess = vi
344
+ .spyOn(emulatorProcess, 'startDetachedProcess')
345
+ .mockReturnValue(
346
+ child as unknown as ReturnType<
347
+ typeof emulatorProcess.startDetachedProcess
348
+ >
349
+ );
350
+
351
+ const startPromise = startEmulator(
352
+ 'Pixel_8_API_35',
353
+ 'clean-snapshot-generation'
354
+ );
355
+ await vi.runAllTimersAsync();
356
+ await startPromise;
357
+
358
+ expect(startDetachedProcess).toHaveBeenCalledWith(
359
+ expect.stringMatching(/emulator$/),
360
+ expect.arrayContaining(['-no-snapshot-load'])
361
+ );
362
+ expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain(
363
+ '-no-snapshot-save'
364
+ );
365
+ });
366
+
367
+ it('passes snapshot reuse args to the emulator process', async () => {
368
+ vi.useFakeTimers();
369
+ const child = createMockChildProcess();
370
+ vi.spyOn(tools, 'spawn')
371
+ .mockResolvedValueOnce({
372
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
373
+ } as Awaited<ReturnType<typeof tools.spawn>>)
374
+ .mockResolvedValueOnce({
375
+ stdout: 'Pixel_8_API_35\n',
376
+ } as Awaited<ReturnType<typeof tools.spawn>>);
377
+ const startDetachedProcess = vi
378
+ .spyOn(emulatorProcess, 'startDetachedProcess')
379
+ .mockReturnValue(
380
+ child as unknown as ReturnType<
381
+ typeof emulatorProcess.startDetachedProcess
382
+ >
383
+ );
384
+
385
+ const startPromise = startEmulator('Pixel_8_API_35', 'snapshot-reuse');
386
+ await vi.runAllTimersAsync();
387
+ await startPromise;
388
+
389
+ expect(startDetachedProcess).toHaveBeenCalledWith(
390
+ expect.stringMatching(/emulator$/),
391
+ expect.arrayContaining(['-no-snapshot-save'])
392
+ );
393
+ expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain(
394
+ '-no-snapshot-load'
395
+ );
396
+ });
397
+
398
+ it('aborts while waiting for an emulator to boot', async () => {
399
+ vi.useFakeTimers();
400
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
401
+ stdout: 'List of devices attached\n\n',
402
+ } as Awaited<ReturnType<typeof tools.spawn>>);
403
+ const controller = new AbortController();
404
+ const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
405
+
406
+ await vi.advanceTimersByTimeAsync(1000);
407
+ controller.abort(createAbortError());
408
+
409
+ await expect(waitPromise).rejects.toBeInstanceOf(DOMException);
410
+ });
411
+
412
+ it('aborts while waiting for boot completion', async () => {
413
+ vi.useFakeTimers();
414
+ const spawnSpy = vi.spyOn(tools, 'spawn');
415
+ spawnSpy
416
+ .mockResolvedValueOnce({
417
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
418
+ } as Awaited<ReturnType<typeof tools.spawn>>)
419
+ .mockResolvedValueOnce({
420
+ stdout: 'Pixel_8_API_35\n',
421
+ } as Awaited<ReturnType<typeof tools.spawn>>)
422
+ .mockResolvedValueOnce({
423
+ stdout: '0\n',
424
+ } as Awaited<ReturnType<typeof tools.spawn>>);
425
+ const controller = new AbortController();
426
+ const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
427
+
428
+ await vi.advanceTimersByTimeAsync(1000);
429
+ controller.abort(createAbortError());
430
+
431
+ await expect(waitPromise).rejects.toBeInstanceOf(DOMException);
432
+ });
433
+
434
+ it('treats transient adb shell failures as not-yet-booted', async () => {
435
+ vi.useFakeTimers();
436
+ const spawnSpy = vi.spyOn(tools, 'spawn');
437
+ const transientShellError = Object.assign(new Error('adb shell failed'), {
438
+ exitCode: 1,
439
+ });
440
+ Object.setPrototypeOf(transientShellError, SubprocessError.prototype);
441
+
442
+ spawnSpy
443
+ .mockResolvedValueOnce({
444
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
445
+ } as Awaited<ReturnType<typeof tools.spawn>>)
446
+ .mockResolvedValueOnce({
447
+ stdout: 'Pixel_8_API_35\n',
448
+ } as Awaited<ReturnType<typeof tools.spawn>>)
449
+ .mockRejectedValueOnce(transientShellError)
450
+ .mockResolvedValueOnce({
451
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
452
+ } as Awaited<ReturnType<typeof tools.spawn>>)
453
+ .mockResolvedValueOnce({
454
+ stdout: 'Pixel_8_API_35\n',
455
+ } as Awaited<ReturnType<typeof tools.spawn>>)
456
+ .mockResolvedValueOnce({
457
+ stdout: '1\n',
458
+ } as Awaited<ReturnType<typeof tools.spawn>>);
459
+
460
+ const waitPromise = waitForBoot(
461
+ 'Pixel_8_API_35',
462
+ new AbortController().signal
463
+ );
464
+
465
+ await vi.advanceTimersByTimeAsync(1000);
466
+
467
+ await expect(waitPromise).resolves.toBe('emulator-5554');
468
+ expect(spawnSpy).toHaveBeenCalledTimes(6);
469
+ });
470
+
471
+ it('waits for an emulator to disconnect from adb', async () => {
472
+ vi.useFakeTimers();
473
+ const spawnSpy = vi.spyOn(tools, 'spawn');
474
+
475
+ spawnSpy
476
+ .mockResolvedValueOnce({
477
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
478
+ } as Awaited<ReturnType<typeof tools.spawn>>)
479
+ .mockResolvedValueOnce({
480
+ stdout: 'List of devices attached\n\n',
481
+ } as Awaited<ReturnType<typeof tools.spawn>>);
482
+
483
+ const waitPromise = waitForEmulatorDisconnect(
484
+ 'emulator-5554',
485
+ new AbortController().signal
486
+ );
487
+
488
+ await vi.advanceTimersByTimeAsync(1000);
489
+
490
+ await expect(waitPromise).resolves.toBeUndefined();
491
+ expect(spawnSpy).toHaveBeenCalledTimes(2);
492
+ });
89
493
  });
@@ -0,0 +1,206 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getNormalizedAvdCacheConfig,
4
+ isAvdCompatible,
5
+ parseAvdConfig,
6
+ resolveAvdCachingEnabled,
7
+ } from '../avd-config.js';
8
+ import { AndroidPlatformConfigSchema } from '../config.js';
9
+
10
+ describe('AVD config helpers', () => {
11
+ it('parses snapshot config from Android schema', () => {
12
+ const config = AndroidPlatformConfigSchema.parse({
13
+ name: 'android',
14
+ bundleId: 'com.example.app',
15
+ device: {
16
+ type: 'emulator',
17
+ name: 'Pixel_8_API_35',
18
+ avd: {
19
+ apiLevel: 35,
20
+ profile: 'pixel_8',
21
+ diskSize: '1G',
22
+ heapSize: '512M',
23
+ snapshot: {
24
+ enabled: true,
25
+ },
26
+ },
27
+ },
28
+ });
29
+
30
+ expect(config.device.type).toBe('emulator');
31
+ if (config.device.type === 'emulator') {
32
+ expect(config.device.avd?.snapshot?.enabled).toBe(true);
33
+ }
34
+ });
35
+
36
+ it('lets HARNESS_AVD_CACHING override config before interactive gating', () => {
37
+ expect(
38
+ resolveAvdCachingEnabled({
39
+ avd: {
40
+ apiLevel: 35,
41
+ profile: 'pixel_8',
42
+ diskSize: '1G',
43
+ heapSize: '1G',
44
+ snapshot: { enabled: false },
45
+ },
46
+ isInteractive: false,
47
+ env: {
48
+ HARNESS_AVD_CACHING: 'true',
49
+ },
50
+ })
51
+ ).toBe(true);
52
+ });
53
+
54
+ it('disables caching for interactive sessions even when requested', () => {
55
+ expect(
56
+ resolveAvdCachingEnabled({
57
+ avd: {
58
+ apiLevel: 35,
59
+ profile: 'pixel_8',
60
+ diskSize: '1G',
61
+ heapSize: '1G',
62
+ snapshot: { enabled: true },
63
+ },
64
+ isInteractive: true,
65
+ })
66
+ ).toBe(false);
67
+ });
68
+
69
+ it('parses config.ini and matches compatible AVD metadata', () => {
70
+ const avdConfig = parseAvdConfig(`
71
+ image.sysdir.1=system-images/android-35/default/x86_64/
72
+ abi.type=x86_64
73
+ hw.device.name=pixel_8
74
+ disk.dataPartition.size=1G
75
+ vm.heapSize=512M
76
+ `);
77
+
78
+ expect(
79
+ isAvdCompatible({
80
+ emulator: {
81
+ type: 'emulator',
82
+ name: 'Pixel_8_API_35',
83
+ avd: {
84
+ apiLevel: 35,
85
+ profile: 'pixel_8',
86
+ diskSize: '1G',
87
+ heapSize: '512M',
88
+ },
89
+ },
90
+ avdConfig,
91
+ hostArch: 'x86_64',
92
+ })
93
+ ).toEqual({ compatible: true });
94
+ });
95
+
96
+ it('accepts disk partition sizes rewritten to bytes', () => {
97
+ const avdConfig = parseAvdConfig(`
98
+ image.sysdir.1=system-images/android-35/default/x86_64/
99
+ abi.type=x86_64
100
+ hw.device.name=pixel_8
101
+ disk.dataPartition.size=6442450944
102
+ vm.heapSize=512M
103
+ `);
104
+
105
+ expect(
106
+ isAvdCompatible({
107
+ emulator: {
108
+ type: 'emulator',
109
+ name: 'Pixel_8_API_35',
110
+ avd: {
111
+ apiLevel: 35,
112
+ profile: 'pixel_8',
113
+ diskSize: '1G',
114
+ heapSize: '512M',
115
+ },
116
+ },
117
+ avdConfig,
118
+ hostArch: 'x86_64',
119
+ })
120
+ ).toEqual({ compatible: true });
121
+ });
122
+
123
+ it('rejects smaller disk partitions even when sizes are normalized', () => {
124
+ const avdConfig = parseAvdConfig(`
125
+ image.sysdir.1=system-images/android-35/default/x86_64/
126
+ abi.type=x86_64
127
+ hw.device.name=pixel_8
128
+ disk.dataPartition.size=536870912
129
+ vm.heapSize=512M
130
+ `);
131
+
132
+ expect(
133
+ isAvdCompatible({
134
+ emulator: {
135
+ type: 'emulator',
136
+ name: 'Pixel_8_API_35',
137
+ avd: {
138
+ apiLevel: 35,
139
+ profile: 'pixel_8',
140
+ diskSize: '1G',
141
+ heapSize: '512M',
142
+ },
143
+ },
144
+ avdConfig,
145
+ hostArch: 'x86_64',
146
+ })
147
+ ).toMatchObject({
148
+ compatible: false,
149
+ reason: 'Disk size mismatch: expected 1G, got 536870912.',
150
+ });
151
+ });
152
+
153
+ it('reports incompatibility when AVD metadata differs', () => {
154
+ const avdConfig = parseAvdConfig(`
155
+ image.sysdir.1=system-images/android-34/default/x86_64/
156
+ abi.type=x86_64
157
+ hw.device.name=pixel_7
158
+ disk.dataPartition.size=2G
159
+ vm.heapSize=1G
160
+ `);
161
+
162
+ expect(
163
+ isAvdCompatible({
164
+ emulator: {
165
+ type: 'emulator',
166
+ name: 'Pixel_8_API_35',
167
+ avd: {
168
+ apiLevel: 35,
169
+ profile: 'pixel_8',
170
+ diskSize: '1G',
171
+ heapSize: '512M',
172
+ },
173
+ },
174
+ avdConfig,
175
+ hostArch: 'x86_64',
176
+ })
177
+ ).toMatchObject({
178
+ compatible: false,
179
+ });
180
+ });
181
+
182
+ it('normalizes AVD cache key input with name and host arch', () => {
183
+ expect(
184
+ getNormalizedAvdCacheConfig({
185
+ emulator: {
186
+ type: 'emulator',
187
+ name: 'Pixel_8_API_35',
188
+ avd: {
189
+ apiLevel: 35,
190
+ profile: ' Pixel_8 ',
191
+ diskSize: '1G',
192
+ heapSize: '512M',
193
+ },
194
+ },
195
+ hostArch: 'arm64-v8a',
196
+ })
197
+ ).toEqual({
198
+ name: 'Pixel_8_API_35',
199
+ apiLevel: 35,
200
+ arch: 'arm64-v8a',
201
+ profile: 'pixel_8',
202
+ diskSize: '1g',
203
+ heapSize: '512m',
204
+ });
205
+ });
206
+ });