@react-native-harness/platform-android 1.0.0 → 1.1.0-rc.2
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/dist/__tests__/adb.test.d.ts +2 -0
- package/dist/__tests__/adb.test.d.ts.map +1 -0
- package/dist/__tests__/adb.test.js +72 -0
- package/dist/__tests__/app-monitor.test.d.ts +2 -0
- package/dist/__tests__/app-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/app-monitor.test.js +202 -0
- package/dist/__tests__/crash-parser.test.d.ts +2 -0
- package/dist/__tests__/crash-parser.test.d.ts.map +1 -0
- package/dist/__tests__/crash-parser.test.js +45 -0
- package/dist/__tests__/shared-prefs.test.d.ts +2 -0
- package/dist/__tests__/shared-prefs.test.d.ts.map +1 -0
- package/dist/__tests__/shared-prefs.test.js +87 -0
- package/dist/adb.d.ts +6 -1
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +84 -18
- package/dist/app-monitor.d.ts +13 -0
- package/dist/app-monitor.d.ts.map +1 -0
- package/dist/app-monitor.js +359 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/crash-parser.d.ts +11 -0
- package/dist/crash-parser.d.ts.map +1 -0
- package/dist/crash-parser.js +39 -0
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +21 -5
- package/dist/shared-prefs.d.ts +3 -0
- package/dist/shared-prefs.d.ts.map +1 -0
- package/dist/shared-prefs.js +92 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/eslint.config.mjs +4 -1
- package/package.json +4 -4
- package/src/__tests__/adb.test.ts +89 -0
- package/src/__tests__/app-monitor.test.ts +273 -0
- package/src/__tests__/crash-parser.test.ts +52 -0
- package/src/__tests__/shared-prefs.test.ts +144 -0
- package/src/adb.ts +111 -18
- package/src/app-monitor.ts +544 -0
- package/src/config.ts +10 -0
- package/src/crash-parser.ts +66 -0
- package/src/runner.ts +31 -7
- package/src/shared-prefs.ts +205 -0
- package/tsconfig.json +2 -2
- package/tsconfig.lib.json +2 -2
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/assertions.d.ts +0 -5
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -6
- package/dist/emulator.d.ts +0 -6
- package/dist/emulator.d.ts.map +0 -1
- package/dist/emulator.js +0 -27
- package/dist/errors.d.ts +0 -15
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -28
- package/dist/reader.d.ts +0 -6
- package/dist/reader.d.ts.map +0 -1
- package/dist/reader.js +0 -57
- package/dist/types.d.ts +0 -381
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -107
|
@@ -0,0 +1,273 @@
|
|
|
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';
|
|
5
|
+
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
|
+
|
|
9
|
+
const createMockSubprocess = (): tools.Subprocess =>
|
|
10
|
+
({
|
|
11
|
+
nodeChildProcess: Promise.resolve({
|
|
12
|
+
kill: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
15
|
+
[Symbol.asyncIterator]: async function* () {},
|
|
16
|
+
}) as unknown as tools.Subprocess;
|
|
17
|
+
|
|
18
|
+
const createStreamingSubprocess = (
|
|
19
|
+
chunks: Array<{ line: string; delayMs?: number }>
|
|
20
|
+
): tools.Subprocess =>
|
|
21
|
+
({
|
|
22
|
+
nodeChildProcess: Promise.resolve({
|
|
23
|
+
kill: vi.fn(),
|
|
24
|
+
}),
|
|
25
|
+
[Symbol.asyncIterator]: async function* () {
|
|
26
|
+
for (const { line, delayMs = 0 } of chunks) {
|
|
27
|
+
if (delayMs > 0) {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
yield line;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}) as unknown as tools.Subprocess;
|
|
35
|
+
|
|
36
|
+
const artifactRoot = fs.mkdtempSync(
|
|
37
|
+
path.join(tmpdir(), 'rn-harness-android-monitor-artifacts-')
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
fs.rmSync(artifactRoot, { recursive: true, force: true });
|
|
42
|
+
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('createAndroidLogEvent', () => {
|
|
46
|
+
it('extracts crash details from fatal signal log lines', () => {
|
|
47
|
+
const event = createAndroidLogEvent(
|
|
48
|
+
'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)',
|
|
49
|
+
'com.harnessplayground'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(event).toMatchObject({
|
|
53
|
+
type: 'possible_crash',
|
|
54
|
+
source: 'logs',
|
|
55
|
+
crashDetails: {
|
|
56
|
+
source: 'logs',
|
|
57
|
+
signal: 'SIGSEGV',
|
|
58
|
+
summary:
|
|
59
|
+
'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)',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('extracts process and pid when AndroidRuntime reports a crash', () => {
|
|
65
|
+
const event = createAndroidLogEvent(
|
|
66
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
67
|
+
'com.harnessplayground'
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(event).toMatchObject({
|
|
71
|
+
type: 'possible_crash',
|
|
72
|
+
pid: 1234,
|
|
73
|
+
crashDetails: {
|
|
74
|
+
processName: 'com.harnessplayground',
|
|
75
|
+
pid: 1234,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('starts logcat from the current device timestamp', async () => {
|
|
81
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
82
|
+
|
|
83
|
+
spawnSpy.mockImplementation(
|
|
84
|
+
((file: string, args?: readonly string[]) => {
|
|
85
|
+
if (file === 'adb' && args?.includes('date')) {
|
|
86
|
+
return {
|
|
87
|
+
stdout: '03-12 11:35:08.000\n',
|
|
88
|
+
} as Awaited<ReturnType<typeof tools.spawn>>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return createMockSubprocess();
|
|
92
|
+
}) as typeof tools.spawn
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const monitor = createAndroidAppMonitor({
|
|
96
|
+
adbId: 'emulator-5554',
|
|
97
|
+
bundleId: 'com.harnessplayground',
|
|
98
|
+
appUid: 10234,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await monitor.start();
|
|
102
|
+
await monitor.stop();
|
|
103
|
+
|
|
104
|
+
expect(spawnSpy).toHaveBeenNthCalledWith(2, 'adb', [
|
|
105
|
+
'-s',
|
|
106
|
+
'emulator-5554',
|
|
107
|
+
'logcat',
|
|
108
|
+
'-v',
|
|
109
|
+
'threadtime',
|
|
110
|
+
'-b',
|
|
111
|
+
'crash',
|
|
112
|
+
'--uid=10234',
|
|
113
|
+
'-T',
|
|
114
|
+
'03-12 11:35:08.000',
|
|
115
|
+
], {
|
|
116
|
+
stdout: 'pipe',
|
|
117
|
+
stderr: 'pipe',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('hydrates crash details with stack lines that arrive after the first crash event', async () => {
|
|
122
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
123
|
+
|
|
124
|
+
spawnSpy.mockImplementation(
|
|
125
|
+
((file: string, args?: readonly string[]) => {
|
|
126
|
+
if (file === 'adb' && args?.includes('date')) {
|
|
127
|
+
return {
|
|
128
|
+
stdout: '03-12 10:44:40.000\n',
|
|
129
|
+
} as Awaited<ReturnType<typeof tools.spawn>>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return createStreamingSubprocess([
|
|
133
|
+
{ line: '--------- beginning of crash' },
|
|
134
|
+
{
|
|
135
|
+
line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
139
|
+
delayMs: 25,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
line: '03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
143
|
+
delayMs: 25,
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
}) as typeof tools.spawn
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const monitor = createAndroidAppMonitor({
|
|
150
|
+
adbId: 'emulator-5554',
|
|
151
|
+
bundleId: 'com.harnessplayground',
|
|
152
|
+
appUid: 10234,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await monitor.start();
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
157
|
+
|
|
158
|
+
const details = await monitor.getCrashDetails({
|
|
159
|
+
pid: 13861,
|
|
160
|
+
occurredAt: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await monitor.stop();
|
|
164
|
+
|
|
165
|
+
expect(details?.rawLines).toEqual([
|
|
166
|
+
'--------- beginning of crash',
|
|
167
|
+
'03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
|
|
168
|
+
'03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
169
|
+
'03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('persists resolved Android crash blocks into .harness', async () => {
|
|
174
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
175
|
+
|
|
176
|
+
spawnSpy.mockImplementation(
|
|
177
|
+
((file: string, args?: readonly string[]) => {
|
|
178
|
+
if (file === 'adb' && args?.includes('date')) {
|
|
179
|
+
return {
|
|
180
|
+
stdout: '03-12 10:44:40.000\n',
|
|
181
|
+
} as Awaited<ReturnType<typeof tools.spawn>>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return createStreamingSubprocess([
|
|
185
|
+
{ line: '--------- beginning of crash' },
|
|
186
|
+
{
|
|
187
|
+
line: '03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
191
|
+
delayMs: 20,
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
}) as typeof tools.spawn
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const monitor = createAndroidAppMonitor({
|
|
198
|
+
adbId: 'emulator-5554',
|
|
199
|
+
bundleId: 'com.harnessplayground',
|
|
200
|
+
appUid: 10234,
|
|
201
|
+
crashArtifactWriter: createCrashArtifactWriter({
|
|
202
|
+
runnerName: 'android',
|
|
203
|
+
platformId: 'android',
|
|
204
|
+
rootDir: path.join(artifactRoot, '.harness', 'crash-reports'),
|
|
205
|
+
runTimestamp: '2026-03-12T11-35-08-000Z',
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await monitor.start();
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
211
|
+
|
|
212
|
+
const details = await monitor.getCrashDetails({
|
|
213
|
+
pid: 13861,
|
|
214
|
+
occurredAt: Date.now(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await monitor.stop();
|
|
218
|
+
|
|
219
|
+
expect(details?.artifactPath).toContain('/.harness/crash-reports/');
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
221
|
+
expect(fs.readFileSync(details!.artifactPath!, 'utf8')).toContain(
|
|
222
|
+
'RuntimeException: boom'
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('can be started again after timestamp lookup fails', async () => {
|
|
227
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
228
|
+
const timestampError = new Error('date failed');
|
|
229
|
+
|
|
230
|
+
spawnSpy.mockImplementation(
|
|
231
|
+
((file: string, args?: readonly string[]) => {
|
|
232
|
+
if (file === 'adb' && args?.includes('date')) {
|
|
233
|
+
if (
|
|
234
|
+
spawnSpy.mock.calls.filter(
|
|
235
|
+
([calledFile, calledArgs]) =>
|
|
236
|
+
calledFile === 'adb' &&
|
|
237
|
+
Array.isArray(calledArgs) &&
|
|
238
|
+
calledArgs.includes('date')
|
|
239
|
+
).length === 1
|
|
240
|
+
) {
|
|
241
|
+
throw timestampError;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
stdout: '03-12 11:35:08.000\n',
|
|
246
|
+
} as Awaited<ReturnType<typeof tools.spawn>>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return createMockSubprocess();
|
|
250
|
+
}) as typeof tools.spawn
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const monitor = createAndroidAppMonitor({
|
|
254
|
+
adbId: 'emulator-5554',
|
|
255
|
+
bundleId: 'com.harnessplayground',
|
|
256
|
+
appUid: 10234,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await expect(monitor.start()).rejects.toThrow(timestampError);
|
|
260
|
+
await expect(monitor.start()).resolves.toBeUndefined();
|
|
261
|
+
await monitor.stop();
|
|
262
|
+
|
|
263
|
+
expect(
|
|
264
|
+
spawnSpy.mock.calls.some(
|
|
265
|
+
([file, args]) =>
|
|
266
|
+
file === 'adb' &&
|
|
267
|
+
Array.isArray(args) &&
|
|
268
|
+
args.includes('logcat') &&
|
|
269
|
+
args.includes('--uid=10234')
|
|
270
|
+
)
|
|
271
|
+
).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { androidCrashParser } from '../crash-parser.js';
|
|
3
|
+
|
|
4
|
+
describe('androidCrashParser.parse', () => {
|
|
5
|
+
it('parses an AndroidRuntime crash block into a crash details object', () => {
|
|
6
|
+
expect(
|
|
7
|
+
androidCrashParser.parse({
|
|
8
|
+
contents: [
|
|
9
|
+
'--------- beginning of crash',
|
|
10
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
11
|
+
'03-12 11:35:09.001 1234 1234 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
12
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
13
|
+
].join('\n'),
|
|
14
|
+
bundleId: 'com.harnessplayground',
|
|
15
|
+
})
|
|
16
|
+
).toEqual({
|
|
17
|
+
source: 'logs',
|
|
18
|
+
summary: [
|
|
19
|
+
'--------- beginning of crash',
|
|
20
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
21
|
+
'03-12 11:35:09.001 1234 1234 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
22
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
23
|
+
].join('\n'),
|
|
24
|
+
signal: undefined,
|
|
25
|
+
exceptionType: 'java.lang.RuntimeException: boom',
|
|
26
|
+
processName: 'com.harnessplayground',
|
|
27
|
+
pid: 1234,
|
|
28
|
+
rawLines: [
|
|
29
|
+
'--------- beginning of crash',
|
|
30
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
31
|
+
'03-12 11:35:09.001 1234 1234 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
32
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
33
|
+
],
|
|
34
|
+
stackTrace: [
|
|
35
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('extracts fatal signals from a native crash block', () => {
|
|
41
|
+
expect(
|
|
42
|
+
androidCrashParser.parse({
|
|
43
|
+
contents:
|
|
44
|
+
'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)',
|
|
45
|
+
bundleId: 'com.harnessplayground',
|
|
46
|
+
})
|
|
47
|
+
).toMatchObject({
|
|
48
|
+
signal: 'SIGSEGV',
|
|
49
|
+
processName: 'com.harnessplayground',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as tools from '@react-native-harness/tools';
|
|
3
|
+
import {
|
|
4
|
+
applyHarnessDebugHttpHost,
|
|
5
|
+
clearHarnessDebugHttpHost,
|
|
6
|
+
} from '../shared-prefs.js';
|
|
7
|
+
|
|
8
|
+
const bundleId = 'com.example.app';
|
|
9
|
+
const adbId = 'emulator-5554';
|
|
10
|
+
|
|
11
|
+
const getWrittenContent = (
|
|
12
|
+
calls: ReadonlyArray<readonly unknown[]>
|
|
13
|
+
): string => {
|
|
14
|
+
const writeCall = calls.find(([, , options]) => {
|
|
15
|
+
if (!options || typeof options !== 'object' || !('stdin' in options)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Boolean(options.stdin);
|
|
20
|
+
});
|
|
21
|
+
const options = writeCall?.[2];
|
|
22
|
+
|
|
23
|
+
if (!options || typeof options !== 'object' || !('stdin' in options)) {
|
|
24
|
+
throw new Error('Expected write call options.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const content = options.stdin;
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
!content ||
|
|
31
|
+
typeof content !== 'object' ||
|
|
32
|
+
!('string' in content) ||
|
|
33
|
+
typeof content.string !== 'string'
|
|
34
|
+
) {
|
|
35
|
+
throw new Error('Expected write call with string stdin.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return content.string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getWrittenContents = (
|
|
42
|
+
calls: ReadonlyArray<readonly unknown[]>
|
|
43
|
+
): string[] =>
|
|
44
|
+
calls
|
|
45
|
+
.filter(([, , options]) => {
|
|
46
|
+
if (!options || typeof options !== 'object' || !('stdin' in options)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Boolean(options.stdin);
|
|
51
|
+
})
|
|
52
|
+
.map((call) => getWrittenContent([call]));
|
|
53
|
+
|
|
54
|
+
describe('Android shared preferences Metro host override', () => {
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('handles empty self-closing map files', async () => {
|
|
60
|
+
const spawnSpy = vi
|
|
61
|
+
.spyOn(tools, 'spawn')
|
|
62
|
+
.mockResolvedValueOnce({
|
|
63
|
+
stdout: '<?xml version="1.0" encoding="utf-8"?>\n<map />\n',
|
|
64
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
65
|
+
.mockResolvedValueOnce({} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
66
|
+
|
|
67
|
+
await applyHarnessDebugHttpHost(adbId, bundleId, 'localhost:9090');
|
|
68
|
+
|
|
69
|
+
expect(getWrittenContent(spawnSpy.mock.calls)).toContain('<map>');
|
|
70
|
+
expect(getWrittenContent(spawnSpy.mock.calls)).toContain('</map>');
|
|
71
|
+
expect(getWrittenContent(spawnSpy.mock.calls)).toContain(
|
|
72
|
+
'<string name="debug_http_host">localhost:9090</string>'
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('restores the previous debug host on cleanup', async () => {
|
|
77
|
+
const spawnSpy = vi
|
|
78
|
+
.spyOn(tools, 'spawn')
|
|
79
|
+
.mockResolvedValueOnce({
|
|
80
|
+
stdout: [
|
|
81
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
82
|
+
'<map>',
|
|
83
|
+
' <string name="debug_http_host">10.0.2.2:8081</string>',
|
|
84
|
+
'</map>',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
87
|
+
.mockResolvedValueOnce({} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
88
|
+
.mockResolvedValueOnce({
|
|
89
|
+
stdout: [
|
|
90
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
91
|
+
'<map>',
|
|
92
|
+
' <string name="debug_http_host">10.0.2.2:8081</string>',
|
|
93
|
+
' <!-- react-native-harness:debug_http_host:start -->',
|
|
94
|
+
' <string name="debug_http_host">localhost:9090</string>',
|
|
95
|
+
' <!-- react-native-harness:debug_http_host:end -->',
|
|
96
|
+
'</map>',
|
|
97
|
+
].join('\n'),
|
|
98
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
99
|
+
.mockResolvedValueOnce({} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
100
|
+
|
|
101
|
+
await applyHarnessDebugHttpHost(adbId, bundleId, 'localhost:9090');
|
|
102
|
+
await clearHarnessDebugHttpHost(adbId, bundleId);
|
|
103
|
+
|
|
104
|
+
const writes = getWrittenContents(spawnSpy.mock.calls);
|
|
105
|
+
const firstWrite = writes[0];
|
|
106
|
+
const secondWrite = writes[1];
|
|
107
|
+
|
|
108
|
+
expect(firstWrite).toEqual(
|
|
109
|
+
expect.stringContaining(
|
|
110
|
+
'<string name="harness_debug_http_host_backup">10.0.2.2:8081</string>'
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
expect(firstWrite).toEqual(
|
|
114
|
+
expect.stringContaining(
|
|
115
|
+
'<string name="debug_http_host">localhost:9090</string>'
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
expect(firstWrite).toEqual(
|
|
119
|
+
expect.not.stringContaining(
|
|
120
|
+
'<string name="debug_http_host">10.0.2.2:8081</string>'
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
expect(secondWrite).toEqual(
|
|
124
|
+
expect.stringContaining(
|
|
125
|
+
'<string name="debug_http_host">10.0.2.2:8081</string>'
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
expect(secondWrite).toEqual(
|
|
129
|
+
expect.not.stringContaining(
|
|
130
|
+
'<string name="harness_debug_http_host_backup">10.0.2.2:8081</string>'
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
expect(secondWrite).toEqual(
|
|
134
|
+
expect.not.stringContaining(
|
|
135
|
+
'<!-- react-native-harness:debug_http_host:start -->'
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
expect(secondWrite).toEqual(
|
|
139
|
+
expect.not.stringContaining(
|
|
140
|
+
'<string name="debug_http_host">localhost:9090</string>'
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/adb.ts
CHANGED
|
@@ -1,4 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
|
|
2
|
+
import { spawn, SubprocessError } from '@react-native-harness/tools';
|
|
3
|
+
|
|
4
|
+
export const getStartAppArgs = (
|
|
5
|
+
bundleId: string,
|
|
6
|
+
activityName: string,
|
|
7
|
+
options?: AndroidAppLaunchOptions
|
|
8
|
+
): string[] => {
|
|
9
|
+
const args = [
|
|
10
|
+
'shell',
|
|
11
|
+
'am',
|
|
12
|
+
'start',
|
|
13
|
+
'-a',
|
|
14
|
+
'android.intent.action.MAIN',
|
|
15
|
+
'-c',
|
|
16
|
+
'android.intent.category.LAUNCHER',
|
|
17
|
+
'-n',
|
|
18
|
+
`${bundleId}/${activityName}`,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const extras = options?.extras ?? {};
|
|
22
|
+
|
|
23
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
args.push('--es', key, value);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof value === 'boolean') {
|
|
30
|
+
args.push('--ez', key, value ? 'true' : 'false');
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!Number.isSafeInteger(value)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Android app launch option "${key}" must be a safe integer.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
args.push('--ei', key, value.toString());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return args;
|
|
44
|
+
};
|
|
2
45
|
|
|
3
46
|
export const isAppInstalled = async (
|
|
4
47
|
adbId: string,
|
|
@@ -40,21 +83,10 @@ export const stopApp = async (
|
|
|
40
83
|
export const startApp = async (
|
|
41
84
|
adbId: string,
|
|
42
85
|
bundleId: string,
|
|
43
|
-
activityName: string
|
|
86
|
+
activityName: string,
|
|
87
|
+
options?: AndroidAppLaunchOptions
|
|
44
88
|
): Promise<void> => {
|
|
45
|
-
await spawn('adb', [
|
|
46
|
-
'-s',
|
|
47
|
-
adbId,
|
|
48
|
-
'shell',
|
|
49
|
-
'am',
|
|
50
|
-
'start',
|
|
51
|
-
'-a',
|
|
52
|
-
'android.intent.action.MAIN',
|
|
53
|
-
'-c',
|
|
54
|
-
'android.intent.category.LAUNCHER',
|
|
55
|
-
'-n',
|
|
56
|
-
`${bundleId}/${activityName}`,
|
|
57
|
-
]);
|
|
89
|
+
await spawn('adb', ['-s', adbId, ...getStartAppArgs(bundleId, activityName, options)]);
|
|
58
90
|
};
|
|
59
91
|
|
|
60
92
|
export const getDeviceIds = async (): Promise<string[]> => {
|
|
@@ -113,14 +145,75 @@ export const isAppRunning = async (
|
|
|
113
145
|
adbId: string,
|
|
114
146
|
bundleId: string
|
|
115
147
|
): Promise<boolean> => {
|
|
148
|
+
try {
|
|
149
|
+
const { stdout } = await spawn('adb', [
|
|
150
|
+
'-s',
|
|
151
|
+
adbId,
|
|
152
|
+
'shell',
|
|
153
|
+
'pidof',
|
|
154
|
+
bundleId,
|
|
155
|
+
]);
|
|
156
|
+
return stdout.trim() !== '';
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof SubprocessError && error.exitCode === 1) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const getAppUid = async (
|
|
167
|
+
adbId: string,
|
|
168
|
+
bundleId: string
|
|
169
|
+
): Promise<number> => {
|
|
116
170
|
const { stdout } = await spawn('adb', [
|
|
117
171
|
'-s',
|
|
118
172
|
adbId,
|
|
119
173
|
'shell',
|
|
120
|
-
'
|
|
121
|
-
|
|
174
|
+
'pm',
|
|
175
|
+
'list',
|
|
176
|
+
'packages',
|
|
177
|
+
'-U',
|
|
122
178
|
]);
|
|
123
|
-
|
|
179
|
+
const line = stdout
|
|
180
|
+
.split('\n')
|
|
181
|
+
.find((entry) => entry.includes(`package:${bundleId}`));
|
|
182
|
+
const match = line?.match(/\buid:(\d+)\b/);
|
|
183
|
+
|
|
184
|
+
if (!match) {
|
|
185
|
+
throw new Error(`Failed to resolve Android app UID for "${bundleId}".`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Number(match[1]);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const setHideErrorDialogs = async (
|
|
192
|
+
adbId: string,
|
|
193
|
+
hide: boolean
|
|
194
|
+
): Promise<void> => {
|
|
195
|
+
await spawn('adb', [
|
|
196
|
+
'-s',
|
|
197
|
+
adbId,
|
|
198
|
+
'shell',
|
|
199
|
+
'settings',
|
|
200
|
+
'put',
|
|
201
|
+
'global',
|
|
202
|
+
'hide_error_dialogs',
|
|
203
|
+
hide ? '1' : '0',
|
|
204
|
+
]);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
|
|
208
|
+
const { stdout } = await spawn('adb', [
|
|
209
|
+
'-s',
|
|
210
|
+
adbId,
|
|
211
|
+
'shell',
|
|
212
|
+
'date',
|
|
213
|
+
"+'%m-%d %H:%M:%S.000'",
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
return stdout.trim().replace(/^'+|'+$/g, '');
|
|
124
217
|
};
|
|
125
218
|
|
|
126
219
|
export const getAvds = async (): Promise<string[]> => {
|