@react-native-harness/platform-android 1.1.0-rc.2 → 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.
- package/README.md +9 -2
- package/dist/__tests__/adb.test.js +283 -10
- package/dist/__tests__/avd-config.test.d.ts +2 -0
- package/dist/__tests__/avd-config.test.d.ts.map +1 -0
- package/dist/__tests__/avd-config.test.js +174 -0
- package/dist/__tests__/ci-action.test.d.ts +2 -0
- package/dist/__tests__/ci-action.test.d.ts.map +1 -0
- package/dist/__tests__/ci-action.test.js +46 -0
- package/dist/__tests__/emulator-startup.test.d.ts +2 -0
- package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
- package/dist/__tests__/emulator-startup.test.js +19 -0
- package/dist/__tests__/environment.test.d.ts +2 -0
- package/dist/__tests__/environment.test.d.ts.map +1 -0
- package/dist/__tests__/environment.test.js +51 -0
- package/dist/__tests__/instance.test.d.ts +2 -0
- package/dist/__tests__/instance.test.d.ts.map +1 -0
- package/dist/__tests__/instance.test.js +423 -0
- package/dist/adb.d.ts +23 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +265 -16
- package/dist/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +27 -7
- package/dist/assertions.d.ts +5 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +6 -0
- package/dist/avd-config.d.ts +41 -0
- package/dist/avd-config.d.ts.map +1 -0
- package/dist/avd-config.js +173 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/emulator-startup.d.ts +3 -0
- package/dist/emulator-startup.d.ts.map +1 -0
- package/dist/emulator-startup.js +17 -0
- package/dist/emulator.d.ts +6 -0
- package/dist/emulator.d.ts.map +1 -0
- package/dist/emulator.js +27 -0
- package/dist/environment.d.ts +28 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +295 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/instance.d.ts +6 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +234 -0
- package/dist/reader.d.ts +6 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +57 -0
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +12 -52
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +2 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +381 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +107 -0
- package/package.json +4 -4
- package/src/__tests__/adb.test.ts +419 -15
- package/src/__tests__/avd-config.test.ts +206 -0
- package/src/__tests__/ci-action.test.ts +81 -0
- package/src/__tests__/emulator-startup.test.ts +32 -0
- package/src/__tests__/environment.test.ts +87 -0
- package/src/__tests__/instance.test.ts +610 -0
- package/src/adb.ts +423 -16
- package/src/app-monitor.ts +56 -18
- package/src/avd-config.ts +290 -0
- package/src/config.ts +8 -0
- package/src/emulator-startup.ts +28 -0
- package/src/environment.ts +510 -0
- package/src/errors.ts +19 -0
- package/src/factory.ts +4 -0
- package/src/index.ts +7 -1
- package/src/instance.ts +380 -0
- package/src/runner.ts +23 -69
- package/src/targets.ts +11 -8
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
|
-
-
|
|
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 {
|
|
2
|
-
import {
|
|
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(
|
|
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(
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
});
|