@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
package/README.md CHANGED
@@ -32,7 +32,12 @@ const config = {
32
32
  runners: [
33
33
  androidPlatform({
34
34
  name: 'android',
35
- device: androidEmulator('Pixel_8_API_35'),
35
+ device: androidEmulator('Pixel_8_API_35', {
36
+ apiLevel: 35,
37
+ profile: 'pixel_8',
38
+ diskSize: '1G',
39
+ heapSize: '1G',
40
+ }),
36
41
  bundleId: 'com.your.app',
37
42
  }),
38
43
  androidPlatform({
@@ -78,7 +83,9 @@ Creates a physical Android device configuration.
78
83
 
79
84
  ## Requirements
80
85
 
81
- - Android SDK installed
86
+ - On macOS and Linux, Harness can resolve the SDK root from `ANDROID_HOME`, then `ANDROID_SDK_ROOT`, then the default SDK path (`~/Library/Android/sdk` on macOS or `~/Android/Sdk` on Linux)
87
+ - For emulator runners with an `avd` config, Harness reads the runner config and automatically verifies or installs missing SDK packages, including `platform-tools`, `emulator`, `platforms;android-<apiLevel>`, and the matching system image for the host architecture
88
+ - If the SDK root does not exist yet on macOS or Linux, Harness bootstraps Android command-line tools and accepts licenses for non-interactive installs
82
89
  - Android emulator or physical device connected
83
90
  - React Native project configured for Android
84
91
 
@@ -1,6 +1,21 @@
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 { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startEmulator, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
3
6
  import * as tools from '@react-native-harness/tools';
7
+ import * as environment from '../environment.js';
8
+ const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
9
+ const createMockChildProcess = () => {
10
+ const process = new EventEmitter();
11
+ process.stdout = new PassThrough();
12
+ process.stderr = new PassThrough();
13
+ process.unref = vi.fn();
14
+ return process;
15
+ };
16
+ beforeEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
4
19
  describe('getStartAppArgs', () => {
5
20
  it('maps supported extras to adb am start flags', () => {
6
21
  expect(getStartAppArgs('com.example.app', '.MainActivity', {
@@ -38,13 +53,11 @@ describe('getStartAppArgs', () => {
38
53
  })).toThrow('must be a safe integer');
39
54
  });
40
55
  it('extracts app uid from pm list packages output', async () => {
41
- const spawnSpy = vi
42
- .spyOn(tools, 'spawn')
43
- .mockResolvedValueOnce({
56
+ const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
44
57
  stdout: 'package:com.other.app uid:10123\npackage:com.example.app uid:10234\n',
45
58
  });
46
59
  await expect(getAppUid('emulator-5554', 'com.example.app')).resolves.toBe(10234);
47
- expect(spawnSpy).toHaveBeenCalledWith('adb', [
60
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
48
61
  '-s',
49
62
  'emulator-5554',
50
63
  'shell',
@@ -55,13 +68,11 @@ describe('getStartAppArgs', () => {
55
68
  ]);
56
69
  });
57
70
  it('reads the device timestamp in logcat format', async () => {
58
- const spawnSpy = vi
59
- .spyOn(tools, 'spawn')
60
- .mockResolvedValueOnce({
71
+ const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
61
72
  stdout: "'03-12 11:35:08.000'\n",
62
73
  });
63
74
  await expect(getLogcatTimestamp('emulator-5554')).resolves.toBe('03-12 11:35:08.000');
64
- expect(spawnSpy).toHaveBeenCalledWith('adb', [
75
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
65
76
  '-s',
66
77
  'emulator-5554',
67
78
  'shell',
@@ -69,4 +80,266 @@ describe('getStartAppArgs', () => {
69
80
  "+'%m-%d %H:%M:%S.000'",
70
81
  ]);
71
82
  });
83
+ it('checks whether an AVD exists', async () => {
84
+ vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
85
+ stdout: 'Pixel_6_API_33\nPixel_8_API_35\n',
86
+ });
87
+ await expect(hasAvd('Pixel_8_API_35')).resolves.toBe(true);
88
+ await expect(hasAvd('Missing_AVD')).resolves.toBe(false);
89
+ });
90
+ it('installs the app via adb', async () => {
91
+ const spawnSpy = vi
92
+ .spyOn(tools, 'spawn')
93
+ .mockResolvedValueOnce({});
94
+ await installApp('emulator-5554', '/tmp/app.apk');
95
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
96
+ '-s',
97
+ 'emulator-5554',
98
+ 'install',
99
+ '-r',
100
+ '/tmp/app.apk',
101
+ ]);
102
+ });
103
+ it('creates an AVD and appends config overrides', async () => {
104
+ const spawnSpy = vi
105
+ .spyOn(tools, 'spawn')
106
+ .mockResolvedValue({});
107
+ const verifyAndroidEmulatorSdk = vi
108
+ .spyOn(environment, 'ensureAndroidSdkPackages')
109
+ .mockResolvedValue('/tmp/android-sdk');
110
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue('x86_64');
111
+ await createAvd({
112
+ name: 'Pixel_8_API_35',
113
+ apiLevel: 35,
114
+ profile: 'pixel_8',
115
+ diskSize: '1G',
116
+ heapSize: '1G',
117
+ });
118
+ expect(verifyAndroidEmulatorSdk).toHaveBeenCalledWith([
119
+ 'platform-tools',
120
+ 'emulator',
121
+ 'platforms;android-35',
122
+ 'system-images;android-35;default;x86_64',
123
+ ]);
124
+ expect(spawnSpy).toHaveBeenNthCalledWith(1, 'bash', [
125
+ '-lc',
126
+ expect.stringContaining('create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8"'),
127
+ ]);
128
+ expect(spawnSpy).toHaveBeenNthCalledWith(2, 'bash', [
129
+ '-lc',
130
+ expect.stringContaining(`'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `),
131
+ ]);
132
+ });
133
+ it('creates an AVD with arm64 system image packages on arm64 hosts', async () => {
134
+ vi.spyOn(tools, 'spawn').mockResolvedValue({});
135
+ const ensureAndroidSdkPackages = vi
136
+ .spyOn(environment, 'ensureAndroidSdkPackages')
137
+ .mockResolvedValue('/tmp/android-sdk');
138
+ vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue('arm64-v8a');
139
+ await createAvd({
140
+ name: 'Pixel_8_API_35',
141
+ apiLevel: 35,
142
+ profile: 'pixel_8',
143
+ diskSize: '1G',
144
+ heapSize: '1G',
145
+ });
146
+ expect(ensureAndroidSdkPackages).toHaveBeenCalledWith([
147
+ 'platform-tools',
148
+ 'emulator',
149
+ 'platforms;android-35',
150
+ 'system-images;android-35;default;arm64-v8a',
151
+ ]);
152
+ });
153
+ it.skip('deletes both AVD directory and ini file', async () => {
154
+ await deleteAvd('Pixel_8_API_35');
155
+ });
156
+ it('surfaces emulator stdout when startup fails immediately', async () => {
157
+ const child = createMockChildProcess();
158
+ let launcherReadyResolve;
159
+ const launcherReady = new Promise((resolve) => {
160
+ launcherReadyResolve = resolve;
161
+ });
162
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
163
+ stdout: 'List of devices attached\n\n',
164
+ });
165
+ vi.spyOn(emulatorProcess, 'startDetachedProcess').mockImplementation(() => {
166
+ launcherReadyResolve?.();
167
+ return child;
168
+ });
169
+ const startPromise = startEmulator('Pixel_8_API_35');
170
+ await launcherReady;
171
+ child.stdout.write('Unknown AVD name [Pixel_8_API_35]\n');
172
+ child.stdout.end();
173
+ child.stderr.end();
174
+ child.emit('close', 1, null);
175
+ await expect(startPromise).rejects.toThrow('Unknown AVD name [Pixel_8_API_35]');
176
+ });
177
+ it('surfaces emulator stderr when startup fails immediately', async () => {
178
+ const child = createMockChildProcess();
179
+ let launcherReadyResolve;
180
+ const launcherReady = new Promise((resolve) => {
181
+ launcherReadyResolve = resolve;
182
+ });
183
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
184
+ stdout: 'List of devices attached\n\n',
185
+ });
186
+ vi.spyOn(emulatorProcess, 'startDetachedProcess').mockImplementation(() => {
187
+ launcherReadyResolve?.();
188
+ return child;
189
+ });
190
+ const startPromise = startEmulator('Pixel_8_API_35');
191
+ await launcherReady;
192
+ child.stderr.write('emulator: panic: broken config\n');
193
+ child.stdout.end();
194
+ child.stderr.end();
195
+ child.emit('close', 1, null);
196
+ await expect(startPromise).rejects.toThrow('emulator: panic: broken config');
197
+ });
198
+ it('returns after the emulator appears without waiting for process exit', async () => {
199
+ vi.useFakeTimers();
200
+ const child = createMockChildProcess();
201
+ const spawnSpy = vi.spyOn(tools, 'spawn');
202
+ spawnSpy
203
+ .mockResolvedValueOnce({
204
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
205
+ })
206
+ .mockResolvedValueOnce({
207
+ stdout: 'Pixel_8_API_35\n',
208
+ });
209
+ vi.spyOn(emulatorProcess, 'startDetachedProcess').mockReturnValue(child);
210
+ const startPromise = startEmulator('Pixel_8_API_35');
211
+ await vi.runAllTimersAsync();
212
+ await expect(startPromise).resolves.toBeUndefined();
213
+ expect(child.unref).toHaveBeenCalled();
214
+ });
215
+ it('passes default boot args to the emulator process', async () => {
216
+ vi.useFakeTimers();
217
+ const child = createMockChildProcess();
218
+ vi.spyOn(tools, 'spawn')
219
+ .mockResolvedValueOnce({
220
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
221
+ })
222
+ .mockResolvedValueOnce({
223
+ stdout: 'Pixel_8_API_35\n',
224
+ });
225
+ const startDetachedProcess = vi
226
+ .spyOn(emulatorProcess, 'startDetachedProcess')
227
+ .mockReturnValue(child);
228
+ const startPromise = startEmulator('Pixel_8_API_35');
229
+ await vi.runAllTimersAsync();
230
+ await startPromise;
231
+ expect(startDetachedProcess).toHaveBeenCalledWith(expect.stringMatching(/emulator$/), expect.arrayContaining(['-no-snapshot-load', '-no-snapshot-save']));
232
+ });
233
+ it('passes clean snapshot generation args to the emulator process', async () => {
234
+ vi.useFakeTimers();
235
+ const child = createMockChildProcess();
236
+ vi.spyOn(tools, 'spawn')
237
+ .mockResolvedValueOnce({
238
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
239
+ })
240
+ .mockResolvedValueOnce({
241
+ stdout: 'Pixel_8_API_35\n',
242
+ });
243
+ const startDetachedProcess = vi
244
+ .spyOn(emulatorProcess, 'startDetachedProcess')
245
+ .mockReturnValue(child);
246
+ const startPromise = startEmulator('Pixel_8_API_35', 'clean-snapshot-generation');
247
+ await vi.runAllTimersAsync();
248
+ await startPromise;
249
+ expect(startDetachedProcess).toHaveBeenCalledWith(expect.stringMatching(/emulator$/), expect.arrayContaining(['-no-snapshot-load']));
250
+ expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain('-no-snapshot-save');
251
+ });
252
+ it('passes snapshot reuse args to the emulator process', async () => {
253
+ vi.useFakeTimers();
254
+ const child = createMockChildProcess();
255
+ vi.spyOn(tools, 'spawn')
256
+ .mockResolvedValueOnce({
257
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
258
+ })
259
+ .mockResolvedValueOnce({
260
+ stdout: 'Pixel_8_API_35\n',
261
+ });
262
+ const startDetachedProcess = vi
263
+ .spyOn(emulatorProcess, 'startDetachedProcess')
264
+ .mockReturnValue(child);
265
+ const startPromise = startEmulator('Pixel_8_API_35', 'snapshot-reuse');
266
+ await vi.runAllTimersAsync();
267
+ await startPromise;
268
+ expect(startDetachedProcess).toHaveBeenCalledWith(expect.stringMatching(/emulator$/), expect.arrayContaining(['-no-snapshot-save']));
269
+ expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain('-no-snapshot-load');
270
+ });
271
+ it('aborts while waiting for an emulator to boot', async () => {
272
+ vi.useFakeTimers();
273
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
274
+ stdout: 'List of devices attached\n\n',
275
+ });
276
+ const controller = new AbortController();
277
+ const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
278
+ await vi.advanceTimersByTimeAsync(1000);
279
+ controller.abort(createAbortError());
280
+ await expect(waitPromise).rejects.toBeInstanceOf(DOMException);
281
+ });
282
+ it('aborts while waiting for boot completion', async () => {
283
+ vi.useFakeTimers();
284
+ const spawnSpy = vi.spyOn(tools, 'spawn');
285
+ spawnSpy
286
+ .mockResolvedValueOnce({
287
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
288
+ })
289
+ .mockResolvedValueOnce({
290
+ stdout: 'Pixel_8_API_35\n',
291
+ })
292
+ .mockResolvedValueOnce({
293
+ stdout: '0\n',
294
+ });
295
+ const controller = new AbortController();
296
+ const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
297
+ await vi.advanceTimersByTimeAsync(1000);
298
+ controller.abort(createAbortError());
299
+ await expect(waitPromise).rejects.toBeInstanceOf(DOMException);
300
+ });
301
+ it('treats transient adb shell failures as not-yet-booted', async () => {
302
+ vi.useFakeTimers();
303
+ const spawnSpy = vi.spyOn(tools, 'spawn');
304
+ const transientShellError = Object.assign(new Error('adb shell failed'), {
305
+ exitCode: 1,
306
+ });
307
+ Object.setPrototypeOf(transientShellError, SubprocessError.prototype);
308
+ spawnSpy
309
+ .mockResolvedValueOnce({
310
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
311
+ })
312
+ .mockResolvedValueOnce({
313
+ stdout: 'Pixel_8_API_35\n',
314
+ })
315
+ .mockRejectedValueOnce(transientShellError)
316
+ .mockResolvedValueOnce({
317
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
318
+ })
319
+ .mockResolvedValueOnce({
320
+ stdout: 'Pixel_8_API_35\n',
321
+ })
322
+ .mockResolvedValueOnce({
323
+ stdout: '1\n',
324
+ });
325
+ const waitPromise = waitForBoot('Pixel_8_API_35', new AbortController().signal);
326
+ await vi.advanceTimersByTimeAsync(1000);
327
+ await expect(waitPromise).resolves.toBe('emulator-5554');
328
+ expect(spawnSpy).toHaveBeenCalledTimes(6);
329
+ });
330
+ it('waits for an emulator to disconnect from adb', async () => {
331
+ vi.useFakeTimers();
332
+ const spawnSpy = vi.spyOn(tools, 'spawn');
333
+ spawnSpy
334
+ .mockResolvedValueOnce({
335
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
336
+ })
337
+ .mockResolvedValueOnce({
338
+ stdout: 'List of devices attached\n\n',
339
+ });
340
+ const waitPromise = waitForEmulatorDisconnect('emulator-5554', new AbortController().signal);
341
+ await vi.advanceTimersByTimeAsync(1000);
342
+ await expect(waitPromise).resolves.toBeUndefined();
343
+ expect(spawnSpy).toHaveBeenCalledTimes(2);
344
+ });
72
345
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=avd-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avd-config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/avd-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,174 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getNormalizedAvdCacheConfig, isAvdCompatible, parseAvdConfig, resolveAvdCachingEnabled, } from '../avd-config.js';
3
+ import { AndroidPlatformConfigSchema } from '../config.js';
4
+ describe('AVD config helpers', () => {
5
+ it('parses snapshot config from Android schema', () => {
6
+ const config = AndroidPlatformConfigSchema.parse({
7
+ name: 'android',
8
+ bundleId: 'com.example.app',
9
+ device: {
10
+ type: 'emulator',
11
+ name: 'Pixel_8_API_35',
12
+ avd: {
13
+ apiLevel: 35,
14
+ profile: 'pixel_8',
15
+ diskSize: '1G',
16
+ heapSize: '512M',
17
+ snapshot: {
18
+ enabled: true,
19
+ },
20
+ },
21
+ },
22
+ });
23
+ expect(config.device.type).toBe('emulator');
24
+ if (config.device.type === 'emulator') {
25
+ expect(config.device.avd?.snapshot?.enabled).toBe(true);
26
+ }
27
+ });
28
+ it('lets HARNESS_AVD_CACHING override config before interactive gating', () => {
29
+ expect(resolveAvdCachingEnabled({
30
+ avd: {
31
+ apiLevel: 35,
32
+ profile: 'pixel_8',
33
+ diskSize: '1G',
34
+ heapSize: '1G',
35
+ snapshot: { enabled: false },
36
+ },
37
+ isInteractive: false,
38
+ env: {
39
+ HARNESS_AVD_CACHING: 'true',
40
+ },
41
+ })).toBe(true);
42
+ });
43
+ it('disables caching for interactive sessions even when requested', () => {
44
+ expect(resolveAvdCachingEnabled({
45
+ avd: {
46
+ apiLevel: 35,
47
+ profile: 'pixel_8',
48
+ diskSize: '1G',
49
+ heapSize: '1G',
50
+ snapshot: { enabled: true },
51
+ },
52
+ isInteractive: true,
53
+ })).toBe(false);
54
+ });
55
+ it('parses config.ini and matches compatible AVD metadata', () => {
56
+ const avdConfig = parseAvdConfig(`
57
+ image.sysdir.1=system-images/android-35/default/x86_64/
58
+ abi.type=x86_64
59
+ hw.device.name=pixel_8
60
+ disk.dataPartition.size=1G
61
+ vm.heapSize=512M
62
+ `);
63
+ expect(isAvdCompatible({
64
+ emulator: {
65
+ type: 'emulator',
66
+ name: 'Pixel_8_API_35',
67
+ avd: {
68
+ apiLevel: 35,
69
+ profile: 'pixel_8',
70
+ diskSize: '1G',
71
+ heapSize: '512M',
72
+ },
73
+ },
74
+ avdConfig,
75
+ hostArch: 'x86_64',
76
+ })).toEqual({ compatible: true });
77
+ });
78
+ it('accepts disk partition sizes rewritten to bytes', () => {
79
+ const avdConfig = parseAvdConfig(`
80
+ image.sysdir.1=system-images/android-35/default/x86_64/
81
+ abi.type=x86_64
82
+ hw.device.name=pixel_8
83
+ disk.dataPartition.size=6442450944
84
+ vm.heapSize=512M
85
+ `);
86
+ expect(isAvdCompatible({
87
+ emulator: {
88
+ type: 'emulator',
89
+ name: 'Pixel_8_API_35',
90
+ avd: {
91
+ apiLevel: 35,
92
+ profile: 'pixel_8',
93
+ diskSize: '1G',
94
+ heapSize: '512M',
95
+ },
96
+ },
97
+ avdConfig,
98
+ hostArch: 'x86_64',
99
+ })).toEqual({ compatible: true });
100
+ });
101
+ it('rejects smaller disk partitions even when sizes are normalized', () => {
102
+ const avdConfig = parseAvdConfig(`
103
+ image.sysdir.1=system-images/android-35/default/x86_64/
104
+ abi.type=x86_64
105
+ hw.device.name=pixel_8
106
+ disk.dataPartition.size=536870912
107
+ vm.heapSize=512M
108
+ `);
109
+ expect(isAvdCompatible({
110
+ emulator: {
111
+ type: 'emulator',
112
+ name: 'Pixel_8_API_35',
113
+ avd: {
114
+ apiLevel: 35,
115
+ profile: 'pixel_8',
116
+ diskSize: '1G',
117
+ heapSize: '512M',
118
+ },
119
+ },
120
+ avdConfig,
121
+ hostArch: 'x86_64',
122
+ })).toMatchObject({
123
+ compatible: false,
124
+ reason: 'Disk size mismatch: expected 1G, got 536870912.',
125
+ });
126
+ });
127
+ it('reports incompatibility when AVD metadata differs', () => {
128
+ const avdConfig = parseAvdConfig(`
129
+ image.sysdir.1=system-images/android-34/default/x86_64/
130
+ abi.type=x86_64
131
+ hw.device.name=pixel_7
132
+ disk.dataPartition.size=2G
133
+ vm.heapSize=1G
134
+ `);
135
+ expect(isAvdCompatible({
136
+ emulator: {
137
+ type: 'emulator',
138
+ name: 'Pixel_8_API_35',
139
+ avd: {
140
+ apiLevel: 35,
141
+ profile: 'pixel_8',
142
+ diskSize: '1G',
143
+ heapSize: '512M',
144
+ },
145
+ },
146
+ avdConfig,
147
+ hostArch: 'x86_64',
148
+ })).toMatchObject({
149
+ compatible: false,
150
+ });
151
+ });
152
+ it('normalizes AVD cache key input with name and host arch', () => {
153
+ expect(getNormalizedAvdCacheConfig({
154
+ emulator: {
155
+ type: 'emulator',
156
+ name: 'Pixel_8_API_35',
157
+ avd: {
158
+ apiLevel: 35,
159
+ profile: ' Pixel_8 ',
160
+ diskSize: '1G',
161
+ heapSize: '512M',
162
+ },
163
+ },
164
+ hostArch: 'arm64-v8a',
165
+ })).toEqual({
166
+ name: 'Pixel_8_API_35',
167
+ apiLevel: 35,
168
+ arch: 'arm64-v8a',
169
+ profile: 'pixel_8',
170
+ diskSize: '1g',
171
+ heapSize: '512m',
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ci-action.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-action.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ci-action.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ const workspaceRoot = path.resolve(import.meta.dirname, '../../../..');
5
+ describe('Android GitHub action config', () => {
6
+ it('does not duplicate Android SDK verification in the action YAML', async () => {
7
+ const [rootAction, packageAction] = await Promise.all([
8
+ readFile(path.join(workspaceRoot, 'action.yml'), 'utf8'),
9
+ readFile(path.join(workspaceRoot, 'packages/github-action/src/action.yml'), 'utf8'),
10
+ ]);
11
+ for (const actionYaml of [rootAction, packageAction]) {
12
+ expect(actionYaml).not.toContain('Verify Android SDK packages');
13
+ expect(actionYaml).toContain("steps.avd-cache.outputs.cache-hit != 'true'");
14
+ }
15
+ });
16
+ it('removes the third-party emulator runner and maps cacheAvd to HARNESS_AVD_CACHING', async () => {
17
+ const [rootAction, packageAction] = await Promise.all([
18
+ readFile(path.join(workspaceRoot, 'action.yml'), 'utf8'),
19
+ readFile(path.join(workspaceRoot, 'packages/github-action/src/action.yml'), 'utf8'),
20
+ ]);
21
+ for (const actionYaml of [rootAction, packageAction]) {
22
+ expect(actionYaml).not.toContain('reactivecircus/android-emulator-runner');
23
+ expect(actionYaml).toContain('HARNESS_AVD_CACHING: ${{ inputs.cacheAvd }}');
24
+ expect(actionYaml).toContain('fromJson(steps.load-config.outputs.config).action.avdCachingEnabled');
25
+ }
26
+ });
27
+ it('saves the AVD cache after the Harness run step', async () => {
28
+ const [rootAction, packageAction] = await Promise.all([
29
+ readFile(path.join(workspaceRoot, 'action.yml'), 'utf8'),
30
+ readFile(path.join(workspaceRoot, 'packages/github-action/src/action.yml'), 'utf8'),
31
+ ]);
32
+ for (const actionYaml of [rootAction, packageAction]) {
33
+ expect(actionYaml.indexOf('- name: Run E2E tests')).toBeLessThan(actionYaml.indexOf('- name: Save AVD cache'));
34
+ }
35
+ });
36
+ it('uses a cache key that includes the emulator name', async () => {
37
+ const [rootAction, packageAction] = await Promise.all([
38
+ readFile(path.join(workspaceRoot, 'action.yml'), 'utf8'),
39
+ readFile(path.join(workspaceRoot, 'packages/github-action/src/action.yml'), 'utf8'),
40
+ ]);
41
+ for (const actionYaml of [rootAction, packageAction]) {
42
+ expect(actionYaml).toContain("AVD_NAME='${{ fromJson(steps.load-config.outputs.config).config.device.name }}'");
43
+ expect(actionYaml).toContain('CACHE_KEY="avd-$AVD_NAME-$ARCH-$AVD_CONFIG_HASH"');
44
+ }
45
+ });
46
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emulator-startup.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulator-startup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/emulator-startup.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getEmulatorStartupArgs } from '../emulator-startup.js';
3
+ describe('emulator startup modes', () => {
4
+ it('builds default boot args', () => {
5
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).toEqual(expect.arrayContaining([
6
+ '@Pixel_8_API_35',
7
+ '-no-snapshot-load',
8
+ '-no-snapshot-save',
9
+ ]));
10
+ });
11
+ it('builds clean snapshot generation args', () => {
12
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-load']));
13
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).not.toContain('-no-snapshot-save');
14
+ });
15
+ it('builds snapshot reuse args', () => {
16
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-save']));
17
+ expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).not.toContain('-no-snapshot-load');
18
+ });
19
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=environment.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/environment.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getAndroidSdkRoot, getAndroidSystemImagePackage, getDefaultUnixAndroidSdkRoot, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from '../environment.js';
3
+ describe('Android environment', () => {
4
+ beforeEach(() => {
5
+ vi.restoreAllMocks();
6
+ vi.unstubAllEnvs();
7
+ });
8
+ it('uses the default Unix SDK root when env vars are missing', () => {
9
+ expect(getDefaultUnixAndroidSdkRoot({
10
+ platform: 'darwin',
11
+ homeDirectory: '/Users/tester',
12
+ })).toBe('/Users/tester/Library/Android/sdk');
13
+ expect(getAndroidSdkRoot({}, {
14
+ platform: 'linux',
15
+ homeDirectory: '/home/tester',
16
+ })).toBe('/home/tester/Android/Sdk');
17
+ });
18
+ it('prefers ANDROID_HOME and ANDROID_SDK_ROOT over default paths', () => {
19
+ expect(getAndroidSdkRoot({
20
+ ANDROID_HOME: '/env/android-home',
21
+ ANDROID_SDK_ROOT: '/env/android-sdk-root',
22
+ }, {
23
+ platform: 'darwin',
24
+ homeDirectory: '/Users/tester',
25
+ })).toBe('/env/android-home');
26
+ expect(getAndroidSdkRoot({
27
+ ANDROID_SDK_ROOT: '/env/android-sdk-root',
28
+ }, {
29
+ platform: 'linux',
30
+ homeDirectory: '/home/tester',
31
+ })).toBe('/env/android-sdk-root');
32
+ });
33
+ it('selects Android packages using the host architecture', () => {
34
+ expect(getHostAndroidSystemImageArch('x64')).toBe('x86_64');
35
+ expect(getHostAndroidSystemImageArch('arm64')).toBe('arm64-v8a');
36
+ expect(getAndroidSystemImagePackage(35, 'x86_64')).toBe('system-images;android-35;default;x86_64');
37
+ expect(getAndroidSystemImagePackage(35, 'arm64-v8a')).toBe('system-images;android-35;default;arm64-v8a');
38
+ });
39
+ it('derives emulator package requirements from runner config fields', () => {
40
+ expect(getRequiredAndroidSdkPackages({
41
+ apiLevel: 34,
42
+ includeEmulator: true,
43
+ architecture: 'x86_64',
44
+ })).toEqual([
45
+ 'platform-tools',
46
+ 'emulator',
47
+ 'platforms;android-34',
48
+ 'system-images;android-34;default;x86_64',
49
+ ]);
50
+ });
51
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=instance.test.d.ts.map