@react-native-harness/platform-android 1.2.0-rc.1 → 1.3.0

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adb-id.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb-id.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adb-id.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getAdbId, isAdbIdEmulator } from '../adb-id.js';
3
+ import * as adb from '../adb.js';
4
+ describe('adb id resolution', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('identifies emulator adb ids', () => {
9
+ expect(isAdbIdEmulator('emulator-5554')).toBe(true);
10
+ expect(isAdbIdEmulator('device-serial-001')).toBe(false);
11
+ });
12
+ it('skips non-emulator ids when resolving an emulator device', async () => {
13
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([
14
+ 'device-serial-001',
15
+ 'emulator-5554',
16
+ ]);
17
+ vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Test_AVD_API_35');
18
+ await expect(getAdbId({
19
+ type: 'emulator',
20
+ name: 'Test_AVD_API_35',
21
+ })).resolves.toBe('emulator-5554');
22
+ });
23
+ it('resolves matching physical device by manufacturer and model', async () => {
24
+ vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([
25
+ 'device-serial-001',
26
+ 'emulator-5554',
27
+ ]);
28
+ vi.spyOn(adb, 'getDeviceInfo').mockImplementation(async (adbId) => {
29
+ const results = {
30
+ 'device-serial-001': { manufacturer: 'Acme', model: 'Model A1' },
31
+ 'emulator-5554': { manufacturer: 'Emulator', model: 'Emulator Model' },
32
+ };
33
+ return results[adbId] || null;
34
+ });
35
+ await expect(getAdbId({
36
+ type: 'physical',
37
+ manufacturer: 'acme',
38
+ model: 'model a1',
39
+ })).resolves.toBe('device-serial-001');
40
+ });
41
+ });
@@ -1,12 +1,17 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
2
5
  import { PassThrough } from 'node:stream';
3
6
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
7
  import { SubprocessError } from '@react-native-harness/tools';
5
- import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startEmulator, uninstallApp, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
8
+ vi.unmock('../adb.js');
9
+ import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startLogcat, startEmulator, uninstallApp, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
6
10
  import * as tools from '@react-native-harness/tools';
7
11
  import * as environment from '../environment.js';
8
12
  import * as avdConfig from '../avd-config.js';
9
13
  const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
14
+ let sdkRoot;
10
15
  const createMockChildProcess = () => {
11
16
  const process = new EventEmitter();
12
17
  process.stdout = new PassThrough();
@@ -16,6 +21,14 @@ const createMockChildProcess = () => {
16
21
  };
17
22
  beforeEach(() => {
18
23
  vi.restoreAllMocks();
24
+ vi.unstubAllEnvs();
25
+ sdkRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'android-sdk-'));
26
+ fs.mkdirSync(path.join(sdkRoot, 'emulator'), { recursive: true });
27
+ fs.writeFileSync(path.join(sdkRoot, 'emulator', 'emulator'), '');
28
+ fs.mkdirSync(path.join(sdkRoot, 'platform-tools'), { recursive: true });
29
+ fs.writeFileSync(path.join(sdkRoot, 'platform-tools', 'adb'), '');
30
+ vi.stubEnv('ANDROID_HOME', sdkRoot);
31
+ vi.spyOn(environment, 'ensureAndroidEmulatorAvailable').mockResolvedValue(sdkRoot);
19
32
  });
20
33
  describe('getStartAppArgs', () => {
21
34
  it('maps supported extras to adb am start flags', () => {
@@ -81,8 +94,31 @@ describe('getStartAppArgs', () => {
81
94
  "+'%m-%d %H:%M:%S.000'",
82
95
  ]);
83
96
  });
97
+ it('starts logcat using the resolved adb binary path', async () => {
98
+ const subprocess = {};
99
+ const spawnSpy = vi.spyOn(tools, 'spawn').mockReturnValue(subprocess);
100
+ expect(startLogcat('emulator-5554', [
101
+ 'logcat',
102
+ '-v',
103
+ 'threadtime',
104
+ '-b',
105
+ 'crash',
106
+ ])).toBe(subprocess);
107
+ expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
108
+ '-s',
109
+ 'emulator-5554',
110
+ 'logcat',
111
+ '-v',
112
+ 'threadtime',
113
+ '-b',
114
+ 'crash',
115
+ ], {
116
+ stdout: 'pipe',
117
+ stderr: 'pipe',
118
+ });
119
+ });
84
120
  it('checks whether an AVD exists', async () => {
85
- vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
121
+ vi.spyOn(tools, 'spawn').mockResolvedValue({
86
122
  stdout: 'Pixel_6_API_33\nPixel_8_API_35\n',
87
123
  });
88
124
  await expect(hasAvd('Pixel_8_API_35')).resolves.toBe(true);
@@ -365,17 +401,24 @@ describe('getStartAppArgs', () => {
365
401
  });
366
402
  it('aborts while waiting for boot completion', async () => {
367
403
  vi.useFakeTimers();
368
- const spawnSpy = vi.spyOn(tools, 'spawn');
369
- spawnSpy
370
- .mockResolvedValueOnce({
371
- stdout: 'List of devices attached\nemulator-5554\tdevice\n',
372
- })
373
- .mockResolvedValueOnce({
374
- stdout: 'Pixel_8_API_35\n',
375
- })
376
- .mockResolvedValueOnce({
377
- stdout: '0\n',
378
- });
404
+ vi.spyOn(tools, 'spawn').mockImplementation((async (_file, args) => {
405
+ if (args?.[0] === 'devices') {
406
+ return {
407
+ stdout: 'List of devices attached\nemulator-5554\tdevice\n',
408
+ };
409
+ }
410
+ if (args?.includes('emu') && args?.includes('avd') && args?.includes('name')) {
411
+ return {
412
+ stdout: 'Pixel_8_API_35\n',
413
+ };
414
+ }
415
+ if (args?.includes('getprop')) {
416
+ return {
417
+ stdout: '0\n',
418
+ };
419
+ }
420
+ throw new Error(`Unexpected adb call: ${args?.join(' ')}`);
421
+ }));
379
422
  const controller = new AbortController();
380
423
  const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
381
424
  await vi.advanceTimersByTimeAsync(1000);
@@ -1,17 +1,7 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { tmpdir } from 'node:os';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { NativeCrashError } from '@react-native-harness/platforms';
5
3
  import { createAndroidAppMonitor, createAndroidLogEvent } from '../app-monitor.js';
6
- import * as tools from '@react-native-harness/tools';
7
- import { createCrashArtifactWriter } from '@react-native-harness/tools';
8
- const createMockSubprocess = () => ({
9
- nodeChildProcess: Promise.resolve({
10
- kill: vi.fn(),
11
- }),
12
- // eslint-disable-next-line @typescript-eslint/no-empty-function
13
- [Symbol.asyncIterator]: async function* () { },
14
- });
4
+ import * as adb from '../adb.js';
15
5
  const createStreamingSubprocess = (chunks) => ({
16
6
  nodeChildProcess: Promise.resolve({
17
7
  kill: vi.fn(),
@@ -25,178 +15,243 @@ const createStreamingSubprocess = (chunks) => ({
25
15
  }
26
16
  },
27
17
  });
28
- const artifactRoot = fs.mkdtempSync(path.join(tmpdir(), 'rn-harness-android-monitor-artifacts-'));
29
- afterEach(() => {
30
- fs.rmSync(artifactRoot, { recursive: true, force: true });
31
- fs.mkdirSync(artifactRoot, { recursive: true });
32
- });
33
18
  describe('createAndroidLogEvent', () => {
34
- it('extracts crash details from fatal signal log lines', () => {
35
- const event = createAndroidLogEvent('03-12 11:35:08.000 1234 1234 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) in tid 1234 (com.harnessplayground), pid 1234 (com.harnessplayground)', 'com.harnessplayground');
19
+ it('detects confirmation-grade am_crash events', () => {
20
+ const event = createAndroidLogEvent('05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,boom]', 'com.harnessplayground');
36
21
  expect(event).toMatchObject({
37
- type: 'possible_crash',
38
- source: 'logs',
22
+ type: 'crash_confirmed',
39
23
  crashDetails: {
24
+ platform: 'android',
25
+ kind: 'java-exception',
26
+ confidence: 'high',
40
27
  source: 'logs',
41
- signal: 'SIGSEGV',
42
- summary: '03-12 11:35:08.000 1234 1234 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) in tid 1234 (com.harnessplayground), pid 1234 (com.harnessplayground)',
43
28
  },
44
29
  });
45
30
  });
46
- it('extracts process and pid when AndroidRuntime reports a crash', () => {
47
- const event = createAndroidLogEvent('03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234', 'com.harnessplayground');
48
- expect(event).toMatchObject({
49
- type: 'possible_crash',
50
- pid: 1234,
51
- crashDetails: {
52
- processName: 'com.harnessplayground',
53
- pid: 1234,
54
- },
55
- });
31
+ });
32
+ describe('createAndroidAppMonitor', () => {
33
+ beforeEach(() => {
34
+ vi.restoreAllMocks();
35
+ vi.useFakeTimers();
36
+ vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('05-20 12:00:00.000');
56
37
  });
57
- it('starts logcat from the current device timestamp', async () => {
58
- const spawnSpy = vi.spyOn(tools, 'spawn');
59
- spawnSpy.mockImplementation(((file, args) => {
60
- if (file === 'adb' && args?.includes('date')) {
61
- return {
62
- stdout: '03-12 11:35:08.000\n',
63
- };
64
- }
65
- return createMockSubprocess();
66
- }));
38
+ afterEach(() => {
39
+ vi.useRealTimers();
40
+ });
41
+ it('starts logcat with crash, main, system, and events buffers', async () => {
42
+ const startLogcat = vi
43
+ .spyOn(adb, 'startLogcat')
44
+ .mockReturnValue(createStreamingSubprocess([]));
67
45
  const monitor = createAndroidAppMonitor({
68
46
  adbId: 'emulator-5554',
69
47
  bundleId: 'com.harnessplayground',
70
48
  appUid: 10234,
49
+ isAppRunning: async () => true,
71
50
  });
72
51
  await monitor.start();
73
52
  await monitor.stop();
74
- expect(spawnSpy).toHaveBeenNthCalledWith(2, 'adb', [
75
- '-s',
76
- 'emulator-5554',
53
+ expect(startLogcat).toHaveBeenCalledWith('emulator-5554', [
77
54
  'logcat',
78
- '-v',
79
- 'threadtime',
80
55
  '-b',
81
56
  'crash',
82
- '--uid=10234',
57
+ '-b',
58
+ 'main',
59
+ '-b',
60
+ 'system',
61
+ '-b',
62
+ 'events',
63
+ '-v',
64
+ 'threadtime',
83
65
  '-T',
84
- '03-12 11:35:08.000',
85
- ], {
86
- stdout: 'pipe',
87
- stderr: 'pipe',
88
- });
66
+ '05-20 12:00:00.000',
67
+ ]);
89
68
  });
90
- it('hydrates crash details with stack lines that arrive after the first crash event', async () => {
91
- const spawnSpy = vi.spyOn(tools, 'spawn');
92
- spawnSpy.mockImplementation(((file, args) => {
93
- if (file === 'adb' && args?.includes('date')) {
94
- return {
95
- stdout: '03-12 10:44:40.000\n',
96
- };
97
- }
98
- return createStreamingSubprocess([
99
- { line: '--------- beginning of crash' },
100
- {
101
- line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
102
- },
103
- {
104
- line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
105
- delayMs: 25,
106
- },
107
- {
108
- line: '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
109
- delayMs: 25,
110
- },
111
- ]);
69
+ it('reports app lifecycle and crash events through the reporter', async () => {
70
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
71
+ {
72
+ line: '05-20 12:00:00.000 1234 1234 I ActivityManager: Start proc 1234:com.harnessplayground/u0a123 for activity',
73
+ },
74
+ {
75
+ line: '05-20 12:00:00.100 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
76
+ },
77
+ {
78
+ line: '05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,boom]',
79
+ },
80
+ ]));
81
+ const eventReporter = vi.fn();
82
+ const monitor = createAndroidAppMonitor({
83
+ adbId: 'emulator-5554',
84
+ bundleId: 'com.harnessplayground',
85
+ appUid: 10234,
86
+ isAppRunning: async () => true,
87
+ eventReporter,
88
+ });
89
+ const crashWatch = monitor.watch('example.test.ts', 'startup');
90
+ crashWatch.promise.catch(() => undefined);
91
+ monitor.launchRequested({
92
+ type: 'launch_requested',
93
+ launchId: 'launch-1',
94
+ at: Date.now(),
95
+ reason: 'start',
96
+ });
97
+ await monitor.start();
98
+ await vi.advanceTimersByTimeAsync(100);
99
+ expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
100
+ type: 'app:started',
101
+ appPlatform: 'android',
102
+ targetIdentifier: 'emulator-5554',
103
+ launchId: 'launch-1',
104
+ }));
105
+ expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
106
+ type: 'app:crash-suspected',
107
+ appPlatform: 'android',
108
+ processName: 'com.harnessplayground',
112
109
  }));
110
+ expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
111
+ type: 'app:crash-confirmed',
112
+ appPlatform: 'android',
113
+ processName: 'com.harnessplayground',
114
+ }));
115
+ await monitor.stop();
116
+ });
117
+ it('rejects the watch for Java crashes when am_crash arrives even if the process still looks alive', async () => {
118
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
119
+ {
120
+ line: '05-20 12:00:00.000 1234 1234 I ActivityManager: Start proc 1234:com.harnessplayground/u0a123 for activity',
121
+ },
122
+ {
123
+ line: '05-20 12:00:00.100 1234 1234 E AndroidRuntime: FATAL EXCEPTION: main',
124
+ },
125
+ {
126
+ line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
127
+ },
128
+ {
129
+ line: '05-20 12:00:00.102 1234 1234 E AndroidRuntime: java.lang.IllegalStateException: Intentional pre-RN startup crash',
130
+ },
131
+ {
132
+ line: '05-20 12:00:00.103 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
133
+ },
134
+ {
135
+ line: '05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,Intentional pre-RN startup crash]',
136
+ },
137
+ ]));
113
138
  const monitor = createAndroidAppMonitor({
114
139
  adbId: 'emulator-5554',
115
140
  bundleId: 'com.harnessplayground',
116
141
  appUid: 10234,
142
+ isAppRunning: async () => true,
117
143
  });
144
+ const crashWatch = monitor.watch('example.test.ts', 'startup');
145
+ crashWatch.promise.catch(() => undefined);
118
146
  await monitor.start();
119
- await new Promise((resolve) => setTimeout(resolve, 10));
120
- const details = await monitor.getCrashDetails({
121
- pid: 13861,
122
- occurredAt: Date.now(),
147
+ await vi.advanceTimersByTimeAsync(100);
148
+ await expect(crashWatch.promise).rejects.toMatchObject({
149
+ testFilePath: 'example.test.ts',
150
+ details: {
151
+ phase: 'startup',
152
+ platform: 'android',
153
+ kind: 'java-exception',
154
+ confidence: 'high',
155
+ processName: 'com.harnessplayground',
156
+ pid: 1234,
157
+ exceptionType: 'java.lang.IllegalStateException: Intentional pre-RN startup crash',
158
+ },
123
159
  });
160
+ await expect(crashWatch.promise).rejects.toBeInstanceOf(NativeCrashError);
124
161
  await monitor.stop();
125
- expect(details?.rawLines).toEqual([
126
- '--------- beginning of crash',
127
- '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
128
- '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
129
- '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
130
- ]);
131
162
  });
132
- it('persists resolved Android crash blocks into .harness', async () => {
133
- const spawnSpy = vi.spyOn(tools, 'spawn');
134
- spawnSpy.mockImplementation(((file, args) => {
135
- if (file === 'adb' && args?.includes('date')) {
136
- return {
137
- stdout: '03-12 10:44:40.000\n',
138
- };
139
- }
140
- return createStreamingSubprocess([
141
- { line: '--------- beginning of crash' },
142
- {
143
- line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
144
- },
145
- {
146
- line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
147
- delayMs: 20,
148
- },
149
- ]);
150
- }));
163
+ it('rejects the watch when a suspected crash is followed by process exit', async () => {
164
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
165
+ {
166
+ line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
167
+ },
168
+ {
169
+ line: '05-20 12:00:00.102 1234 1234 E AndroidRuntime: java.lang.IllegalStateException: Intentional delayed startup crash',
170
+ },
171
+ ]));
172
+ const isAppRunning = vi
173
+ .fn()
174
+ .mockResolvedValueOnce(true)
175
+ .mockResolvedValueOnce(false)
176
+ .mockResolvedValue(false);
151
177
  const monitor = createAndroidAppMonitor({
152
178
  adbId: 'emulator-5554',
153
179
  bundleId: 'com.harnessplayground',
154
180
  appUid: 10234,
155
- crashArtifactWriter: createCrashArtifactWriter({
156
- runnerName: 'android',
157
- platformId: 'android',
158
- rootDir: path.join(artifactRoot, '.harness', 'crash-reports'),
159
- runTimestamp: '2026-03-12T11-35-08-000Z',
160
- }),
181
+ isAppRunning,
161
182
  });
183
+ const crashWatch = monitor.watch('example.test.ts', 'execution');
184
+ crashWatch.promise.catch(() => undefined);
162
185
  await monitor.start();
163
- await new Promise((resolve) => setTimeout(resolve, 10));
164
- const details = await monitor.getCrashDetails({
165
- pid: 13861,
166
- occurredAt: Date.now(),
186
+ await vi.advanceTimersByTimeAsync(350);
187
+ await expect(crashWatch.promise).rejects.toMatchObject({
188
+ details: {
189
+ phase: 'execution',
190
+ platform: 'android',
191
+ kind: 'java-exception',
192
+ processName: 'com.harnessplayground',
193
+ pid: 1234,
194
+ },
167
195
  });
168
196
  await monitor.stop();
169
- expect(details?.artifactPath).toContain('/.harness/crash-reports/');
170
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
171
- expect(fs.readFileSync(details.artifactPath, 'utf8')).toContain('RuntimeException: boom');
172
197
  });
173
- it('can be started again after timestamp lookup fails', async () => {
174
- const spawnSpy = vi.spyOn(tools, 'spawn');
175
- const timestampError = new Error('date failed');
176
- spawnSpy.mockImplementation(((file, args) => {
177
- if (file === 'adb' && args?.includes('date')) {
178
- if (spawnSpy.mock.calls.filter(([calledFile, calledArgs]) => calledFile === 'adb' &&
179
- Array.isArray(calledArgs) &&
180
- calledArgs.includes('date')).length === 1) {
181
- throw timestampError;
182
- }
183
- return {
184
- stdout: '03-12 11:35:08.000\n',
185
- };
186
- }
187
- return createMockSubprocess();
188
- }));
198
+ it('does not reject the watch when the process exits without a recent crash signal', async () => {
199
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([]));
200
+ const isAppRunning = vi
201
+ .fn()
202
+ .mockResolvedValueOnce(true)
203
+ .mockResolvedValueOnce(false)
204
+ .mockResolvedValue(false);
189
205
  const monitor = createAndroidAppMonitor({
190
206
  adbId: 'emulator-5554',
191
207
  bundleId: 'com.harnessplayground',
192
208
  appUid: 10234,
209
+ isAppRunning,
210
+ });
211
+ const crashWatch = monitor.watch('example.test.ts', 'execution');
212
+ let settled = false;
213
+ const watchHandled = crashWatch.promise.catch(() => {
214
+ settled = true;
215
+ });
216
+ await monitor.start();
217
+ await vi.advanceTimersByTimeAsync(600);
218
+ expect(settled).toBe(false);
219
+ crashWatch.cancel();
220
+ await monitor.stop();
221
+ await watchHandled;
222
+ });
223
+ it('suppresses process-exit confirmation during a controlled stop window', async () => {
224
+ vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
225
+ {
226
+ line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
227
+ },
228
+ ]));
229
+ const isAppRunning = vi
230
+ .fn()
231
+ .mockResolvedValueOnce(true)
232
+ .mockResolvedValueOnce(false)
233
+ .mockResolvedValue(false);
234
+ const monitor = createAndroidAppMonitor({
235
+ adbId: 'emulator-5554',
236
+ bundleId: 'com.harnessplayground',
237
+ appUid: 10234,
238
+ isAppRunning,
239
+ });
240
+ const crashWatch = monitor.watch('example.test.ts', 'execution');
241
+ let settled = false;
242
+ const watchHandled = crashWatch.promise.catch(() => {
243
+ settled = true;
244
+ });
245
+ await monitor.start();
246
+ monitor.stopRequested({
247
+ type: 'stop_requested',
248
+ at: Date.now(),
249
+ reason: 'manual',
193
250
  });
194
- await expect(monitor.start()).rejects.toThrow(timestampError);
195
- await expect(monitor.start()).resolves.toBeUndefined();
251
+ await vi.advanceTimersByTimeAsync(400);
252
+ expect(settled).toBe(false);
253
+ crashWatch.cancel();
196
254
  await monitor.stop();
197
- expect(spawnSpy.mock.calls.some(([file, args]) => file === 'adb' &&
198
- Array.isArray(args) &&
199
- args.includes('logcat') &&
200
- args.includes('--uid=10234'))).toBe(true);
255
+ await watchHandled;
201
256
  });
202
257
  });
@@ -1,8 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { DEFAULT_METRO_PORT, } from '@react-native-harness/config';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
3
  import { getAndroidEmulatorPlatformInstance, getAndroidPhysicalDevicePlatformInstance, } from '../instance.js';
7
4
  import * as adb from '../adb.js';
8
5
  import * as avdConfig from '../avd-config.js';
@@ -19,9 +16,10 @@ const init = {
19
16
  signal: new AbortController().signal,
20
17
  };
21
18
  describe('Android platform instance', () => {
22
- beforeEach(() => {
19
+ beforeEach(async () => {
23
20
  vi.restoreAllMocks();
24
21
  vi.unstubAllEnvs();
22
+ vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorAvailable').mockResolvedValue('/tmp/android-sdk');
25
23
  });
26
24
  it('reuses a running emulator and does not shut it down on dispose', async () => {
27
25
  const ensureAndroidEmulatorEnvironment = vi
@@ -137,6 +135,7 @@ describe('Android platform instance', () => {
137
135
  });
138
136
  it('reuses a compatible cached AVD snapshot when caching is enabled', async () => {
139
137
  vi.stubEnv('HARNESS_AVD_CACHING', 'true');
138
+ vi.spyOn(await import('../environment.js'), 'getHostAndroidSystemImageArch').mockReturnValue('arm64-v8a');
140
139
  vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
141
140
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
142
141
  vi.spyOn(adb, 'hasAvd').mockResolvedValue(true);
@@ -261,74 +260,6 @@ describe('Android platform instance', () => {
261
260
  expect(waitForEmulatorDisconnect).toHaveBeenCalledWith('emulator-5554', init.signal);
262
261
  expect(startEmulator).toHaveBeenNthCalledWith(2, 'Pixel_8_API_35', 'snapshot-reuse');
263
262
  });
264
- it('installs the app from HARNESS_APP_PATH when missing', async () => {
265
- const appPath = path.join(os.tmpdir(), 'HarnessPlayground.apk');
266
- fs.writeFileSync(appPath, 'apk');
267
- vi.stubEnv('HARNESS_APP_PATH', appPath);
268
- vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
269
- vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
270
- vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
271
- vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
272
- vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(false);
273
- const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
274
- vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
275
- vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
276
- vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
277
- vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
278
- await expect(getAndroidEmulatorPlatformInstance({
279
- name: 'android',
280
- device: {
281
- type: 'emulator',
282
- name: 'Pixel_8_API_35',
283
- avd: {
284
- apiLevel: 35,
285
- profile: 'pixel_8',
286
- diskSize: '1G',
287
- heapSize: '1G',
288
- },
289
- },
290
- bundleId: 'com.harnessplayground',
291
- activityName: '.MainActivity',
292
- }, harnessConfig, init)).resolves.toBeDefined();
293
- expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
294
- fs.rmSync(appPath, { force: true });
295
- });
296
- it('reinstalls the app from HARNESS_APP_PATH when already installed', async () => {
297
- const appPath = path.join(os.tmpdir(), 'HarnessPlayground-installed.apk');
298
- fs.writeFileSync(appPath, 'apk');
299
- vi.stubEnv('HARNESS_APP_PATH', appPath);
300
- vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
301
- vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
302
- vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
303
- vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
304
- vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
305
- const uninstallApp = vi
306
- .spyOn(adb, 'uninstallApp')
307
- .mockResolvedValue(undefined);
308
- const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
309
- vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
310
- vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
311
- vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
312
- vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
313
- await expect(getAndroidEmulatorPlatformInstance({
314
- name: 'android',
315
- device: {
316
- type: 'emulator',
317
- name: 'Pixel_8_API_35',
318
- avd: {
319
- apiLevel: 35,
320
- profile: 'pixel_8',
321
- diskSize: '1G',
322
- heapSize: '1G',
323
- },
324
- },
325
- bundleId: 'com.harnessplayground',
326
- activityName: '.MainActivity',
327
- }, harnessConfig, init)).resolves.toBeDefined();
328
- expect(uninstallApp).toHaveBeenCalledWith('emulator-5554', 'com.harnessplayground');
329
- expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
330
- fs.rmSync(appPath, { force: true });
331
- });
332
263
  it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
333
264
  vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
334
265
  vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');