@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 @@
|
|
|
1
|
+
{"version":3,"file":"adb.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adb.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getAppUid, getLogcatTimestamp, getStartAppArgs } from '../adb.js';
|
|
3
|
+
import * as tools from '@react-native-harness/tools';
|
|
4
|
+
describe('getStartAppArgs', () => {
|
|
5
|
+
it('maps supported extras to adb am start flags', () => {
|
|
6
|
+
expect(getStartAppArgs('com.example.app', '.MainActivity', {
|
|
7
|
+
extras: {
|
|
8
|
+
feature_flag: true,
|
|
9
|
+
user_id: 42,
|
|
10
|
+
mode: 'debug',
|
|
11
|
+
},
|
|
12
|
+
})).toEqual([
|
|
13
|
+
'shell',
|
|
14
|
+
'am',
|
|
15
|
+
'start',
|
|
16
|
+
'-a',
|
|
17
|
+
'android.intent.action.MAIN',
|
|
18
|
+
'-c',
|
|
19
|
+
'android.intent.category.LAUNCHER',
|
|
20
|
+
'-n',
|
|
21
|
+
'com.example.app/.MainActivity',
|
|
22
|
+
'--ez',
|
|
23
|
+
'feature_flag',
|
|
24
|
+
'true',
|
|
25
|
+
'--ei',
|
|
26
|
+
'user_id',
|
|
27
|
+
'42',
|
|
28
|
+
'--es',
|
|
29
|
+
'mode',
|
|
30
|
+
'debug',
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it('rejects unsafe integer extras', () => {
|
|
34
|
+
expect(() => getStartAppArgs('com.example.app', '.MainActivity', {
|
|
35
|
+
extras: {
|
|
36
|
+
count: Number.MAX_SAFE_INTEGER + 1,
|
|
37
|
+
},
|
|
38
|
+
})).toThrow('must be a safe integer');
|
|
39
|
+
});
|
|
40
|
+
it('extracts app uid from pm list packages output', async () => {
|
|
41
|
+
const spawnSpy = vi
|
|
42
|
+
.spyOn(tools, 'spawn')
|
|
43
|
+
.mockResolvedValueOnce({
|
|
44
|
+
stdout: 'package:com.other.app uid:10123\npackage:com.example.app uid:10234\n',
|
|
45
|
+
});
|
|
46
|
+
await expect(getAppUid('emulator-5554', 'com.example.app')).resolves.toBe(10234);
|
|
47
|
+
expect(spawnSpy).toHaveBeenCalledWith('adb', [
|
|
48
|
+
'-s',
|
|
49
|
+
'emulator-5554',
|
|
50
|
+
'shell',
|
|
51
|
+
'pm',
|
|
52
|
+
'list',
|
|
53
|
+
'packages',
|
|
54
|
+
'-U',
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
it('reads the device timestamp in logcat format', async () => {
|
|
58
|
+
const spawnSpy = vi
|
|
59
|
+
.spyOn(tools, 'spawn')
|
|
60
|
+
.mockResolvedValueOnce({
|
|
61
|
+
stdout: "'03-12 11:35:08.000'\n",
|
|
62
|
+
});
|
|
63
|
+
await expect(getLogcatTimestamp('emulator-5554')).resolves.toBe('03-12 11:35:08.000');
|
|
64
|
+
expect(spawnSpy).toHaveBeenCalledWith('adb', [
|
|
65
|
+
'-s',
|
|
66
|
+
'emulator-5554',
|
|
67
|
+
'shell',
|
|
68
|
+
'date',
|
|
69
|
+
"+'%m-%d %H:%M:%S.000'",
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-monitor.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/app-monitor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
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
|
+
});
|
|
15
|
+
const createStreamingSubprocess = (chunks) => ({
|
|
16
|
+
nodeChildProcess: Promise.resolve({
|
|
17
|
+
kill: vi.fn(),
|
|
18
|
+
}),
|
|
19
|
+
[Symbol.asyncIterator]: async function* () {
|
|
20
|
+
for (const { line, delayMs = 0 } of chunks) {
|
|
21
|
+
if (delayMs > 0) {
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
23
|
+
}
|
|
24
|
+
yield line;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
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
|
+
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');
|
|
36
|
+
expect(event).toMatchObject({
|
|
37
|
+
type: 'possible_crash',
|
|
38
|
+
source: 'logs',
|
|
39
|
+
crashDetails: {
|
|
40
|
+
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
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
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
|
+
});
|
|
56
|
+
});
|
|
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
|
+
}));
|
|
67
|
+
const monitor = createAndroidAppMonitor({
|
|
68
|
+
adbId: 'emulator-5554',
|
|
69
|
+
bundleId: 'com.harnessplayground',
|
|
70
|
+
appUid: 10234,
|
|
71
|
+
});
|
|
72
|
+
await monitor.start();
|
|
73
|
+
await monitor.stop();
|
|
74
|
+
expect(spawnSpy).toHaveBeenNthCalledWith(2, 'adb', [
|
|
75
|
+
'-s',
|
|
76
|
+
'emulator-5554',
|
|
77
|
+
'logcat',
|
|
78
|
+
'-v',
|
|
79
|
+
'threadtime',
|
|
80
|
+
'-b',
|
|
81
|
+
'crash',
|
|
82
|
+
'--uid=10234',
|
|
83
|
+
'-T',
|
|
84
|
+
'03-12 11:35:08.000',
|
|
85
|
+
], {
|
|
86
|
+
stdout: 'pipe',
|
|
87
|
+
stderr: 'pipe',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
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
|
+
]);
|
|
112
|
+
}));
|
|
113
|
+
const monitor = createAndroidAppMonitor({
|
|
114
|
+
adbId: 'emulator-5554',
|
|
115
|
+
bundleId: 'com.harnessplayground',
|
|
116
|
+
appUid: 10234,
|
|
117
|
+
});
|
|
118
|
+
await monitor.start();
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
120
|
+
const details = await monitor.getCrashDetails({
|
|
121
|
+
pid: 13861,
|
|
122
|
+
occurredAt: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
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
|
+
});
|
|
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
|
+
}));
|
|
151
|
+
const monitor = createAndroidAppMonitor({
|
|
152
|
+
adbId: 'emulator-5554',
|
|
153
|
+
bundleId: 'com.harnessplayground',
|
|
154
|
+
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
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
await monitor.start();
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
164
|
+
const details = await monitor.getCrashDetails({
|
|
165
|
+
pid: 13861,
|
|
166
|
+
occurredAt: Date.now(),
|
|
167
|
+
});
|
|
168
|
+
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
|
+
});
|
|
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
|
+
}));
|
|
189
|
+
const monitor = createAndroidAppMonitor({
|
|
190
|
+
adbId: 'emulator-5554',
|
|
191
|
+
bundleId: 'com.harnessplayground',
|
|
192
|
+
appUid: 10234,
|
|
193
|
+
});
|
|
194
|
+
await expect(monitor.start()).rejects.toThrow(timestampError);
|
|
195
|
+
await expect(monitor.start()).resolves.toBeUndefined();
|
|
196
|
+
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);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crash-parser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crash-parser.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { androidCrashParser } from '../crash-parser.js';
|
|
3
|
+
describe('androidCrashParser.parse', () => {
|
|
4
|
+
it('parses an AndroidRuntime crash block into a crash details object', () => {
|
|
5
|
+
expect(androidCrashParser.parse({
|
|
6
|
+
contents: [
|
|
7
|
+
'--------- beginning of crash',
|
|
8
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
9
|
+
'03-12 11:35:09.001 1234 1234 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
10
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
11
|
+
].join('\n'),
|
|
12
|
+
bundleId: 'com.harnessplayground',
|
|
13
|
+
})).toEqual({
|
|
14
|
+
source: 'logs',
|
|
15
|
+
summary: [
|
|
16
|
+
'--------- beginning of crash',
|
|
17
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
18
|
+
'03-12 11:35:09.001 1234 1234 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
19
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
20
|
+
].join('\n'),
|
|
21
|
+
signal: undefined,
|
|
22
|
+
exceptionType: 'java.lang.RuntimeException: boom',
|
|
23
|
+
processName: 'com.harnessplayground',
|
|
24
|
+
pid: 1234,
|
|
25
|
+
rawLines: [
|
|
26
|
+
'--------- beginning of crash',
|
|
27
|
+
'03-12 11:35:09.000 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
28
|
+
'03-12 11:35:09.001 1234 1234 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
29
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
30
|
+
],
|
|
31
|
+
stackTrace: [
|
|
32
|
+
'03-12 11:35:09.002 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('extracts fatal signals from a native crash block', () => {
|
|
37
|
+
expect(androidCrashParser.parse({
|
|
38
|
+
contents: '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)',
|
|
39
|
+
bundleId: 'com.harnessplayground',
|
|
40
|
+
})).toMatchObject({
|
|
41
|
+
signal: 'SIGSEGV',
|
|
42
|
+
processName: 'com.harnessplayground',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-prefs.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/shared-prefs.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as tools from '@react-native-harness/tools';
|
|
3
|
+
import { applyHarnessDebugHttpHost, clearHarnessDebugHttpHost, } from '../shared-prefs.js';
|
|
4
|
+
const bundleId = 'com.example.app';
|
|
5
|
+
const adbId = 'emulator-5554';
|
|
6
|
+
const getWrittenContent = (calls) => {
|
|
7
|
+
const writeCall = calls.find(([, , options]) => {
|
|
8
|
+
if (!options || typeof options !== 'object' || !('stdin' in options)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return Boolean(options.stdin);
|
|
12
|
+
});
|
|
13
|
+
const options = writeCall?.[2];
|
|
14
|
+
if (!options || typeof options !== 'object' || !('stdin' in options)) {
|
|
15
|
+
throw new Error('Expected write call options.');
|
|
16
|
+
}
|
|
17
|
+
const content = options.stdin;
|
|
18
|
+
if (!content ||
|
|
19
|
+
typeof content !== 'object' ||
|
|
20
|
+
!('string' in content) ||
|
|
21
|
+
typeof content.string !== 'string') {
|
|
22
|
+
throw new Error('Expected write call with string stdin.');
|
|
23
|
+
}
|
|
24
|
+
return content.string;
|
|
25
|
+
};
|
|
26
|
+
const getWrittenContents = (calls) => calls
|
|
27
|
+
.filter(([, , options]) => {
|
|
28
|
+
if (!options || typeof options !== 'object' || !('stdin' in options)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return Boolean(options.stdin);
|
|
32
|
+
})
|
|
33
|
+
.map((call) => getWrittenContent([call]));
|
|
34
|
+
describe('Android shared preferences Metro host override', () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
it('handles empty self-closing map files', async () => {
|
|
39
|
+
const spawnSpy = vi
|
|
40
|
+
.spyOn(tools, 'spawn')
|
|
41
|
+
.mockResolvedValueOnce({
|
|
42
|
+
stdout: '<?xml version="1.0" encoding="utf-8"?>\n<map />\n',
|
|
43
|
+
})
|
|
44
|
+
.mockResolvedValueOnce({});
|
|
45
|
+
await applyHarnessDebugHttpHost(adbId, bundleId, 'localhost:9090');
|
|
46
|
+
expect(getWrittenContent(spawnSpy.mock.calls)).toContain('<map>');
|
|
47
|
+
expect(getWrittenContent(spawnSpy.mock.calls)).toContain('</map>');
|
|
48
|
+
expect(getWrittenContent(spawnSpy.mock.calls)).toContain('<string name="debug_http_host">localhost:9090</string>');
|
|
49
|
+
});
|
|
50
|
+
it('restores the previous debug host on cleanup', async () => {
|
|
51
|
+
const spawnSpy = vi
|
|
52
|
+
.spyOn(tools, 'spawn')
|
|
53
|
+
.mockResolvedValueOnce({
|
|
54
|
+
stdout: [
|
|
55
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
56
|
+
'<map>',
|
|
57
|
+
' <string name="debug_http_host">10.0.2.2:8081</string>',
|
|
58
|
+
'</map>',
|
|
59
|
+
].join('\n'),
|
|
60
|
+
})
|
|
61
|
+
.mockResolvedValueOnce({})
|
|
62
|
+
.mockResolvedValueOnce({
|
|
63
|
+
stdout: [
|
|
64
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
65
|
+
'<map>',
|
|
66
|
+
' <string name="debug_http_host">10.0.2.2:8081</string>',
|
|
67
|
+
' <!-- react-native-harness:debug_http_host:start -->',
|
|
68
|
+
' <string name="debug_http_host">localhost:9090</string>',
|
|
69
|
+
' <!-- react-native-harness:debug_http_host:end -->',
|
|
70
|
+
'</map>',
|
|
71
|
+
].join('\n'),
|
|
72
|
+
})
|
|
73
|
+
.mockResolvedValueOnce({});
|
|
74
|
+
await applyHarnessDebugHttpHost(adbId, bundleId, 'localhost:9090');
|
|
75
|
+
await clearHarnessDebugHttpHost(adbId, bundleId);
|
|
76
|
+
const writes = getWrittenContents(spawnSpy.mock.calls);
|
|
77
|
+
const firstWrite = writes[0];
|
|
78
|
+
const secondWrite = writes[1];
|
|
79
|
+
expect(firstWrite).toEqual(expect.stringContaining('<string name="harness_debug_http_host_backup">10.0.2.2:8081</string>'));
|
|
80
|
+
expect(firstWrite).toEqual(expect.stringContaining('<string name="debug_http_host">localhost:9090</string>'));
|
|
81
|
+
expect(firstWrite).toEqual(expect.not.stringContaining('<string name="debug_http_host">10.0.2.2:8081</string>'));
|
|
82
|
+
expect(secondWrite).toEqual(expect.stringContaining('<string name="debug_http_host">10.0.2.2:8081</string>'));
|
|
83
|
+
expect(secondWrite).toEqual(expect.not.stringContaining('<string name="harness_debug_http_host_backup">10.0.2.2:8081</string>'));
|
|
84
|
+
expect(secondWrite).toEqual(expect.not.stringContaining('<!-- react-native-harness:debug_http_host:start -->'));
|
|
85
|
+
expect(secondWrite).toEqual(expect.not.stringContaining('<string name="debug_http_host">localhost:9090</string>'));
|
|
86
|
+
});
|
|
87
|
+
});
|
package/dist/adb.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
|
|
2
|
+
export declare const getStartAppArgs: (bundleId: string, activityName: string, options?: AndroidAppLaunchOptions) => string[];
|
|
1
3
|
export declare const isAppInstalled: (adbId: string, bundleId: string) => Promise<boolean>;
|
|
2
4
|
export declare const reversePort: (adbId: string, port: number, hostPort?: number) => Promise<void>;
|
|
3
5
|
export declare const stopApp: (adbId: string, bundleId: string) => Promise<void>;
|
|
4
|
-
export declare const startApp: (adbId: string, bundleId: string, activityName: string) => Promise<void>;
|
|
6
|
+
export declare const startApp: (adbId: string, bundleId: string, activityName: string, options?: AndroidAppLaunchOptions) => Promise<void>;
|
|
5
7
|
export declare const getDeviceIds: () => Promise<string[]>;
|
|
6
8
|
export declare const getEmulatorName: (adbId: string) => Promise<string | null>;
|
|
7
9
|
export declare const getShellProperty: (adbId: string, property: string) => Promise<string | null>;
|
|
@@ -13,6 +15,9 @@ export declare const getDeviceInfo: (adbId: string) => Promise<DeviceInfo | null
|
|
|
13
15
|
export declare const isBootCompleted: (adbId: string) => Promise<boolean>;
|
|
14
16
|
export declare const stopEmulator: (adbId: string) => Promise<void>;
|
|
15
17
|
export declare const isAppRunning: (adbId: string, bundleId: string) => Promise<boolean>;
|
|
18
|
+
export declare const getAppUid: (adbId: string, bundleId: string) => Promise<number>;
|
|
19
|
+
export declare const setHideErrorDialogs: (adbId: string, hide: boolean) => Promise<void>;
|
|
20
|
+
export declare const getLogcatTimestamp: (adbId: string) => Promise<string>;
|
|
16
21
|
export declare const getAvds: () => Promise<string[]>;
|
|
17
22
|
export type AdbDevice = {
|
|
18
23
|
id: string;
|
package/dist/adb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG/E,eAAO,MAAM,eAAe,GAC1B,UAAU,MAAM,EAChB,cAAc,MAAM,EACpB,UAAU,uBAAuB,KAChC,MAAM,EAoCR,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,OAAO,CAWjB,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,OAAO,MAAM,EACb,MAAM,MAAM,EACZ,WAAU,MAAa,KACtB,OAAO,CAAC,IAAI,CAQd,CAAC;AAEF,eAAO,MAAM,OAAO,GAClB,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF,eAAO,MAAM,QAAQ,GACnB,OAAO,MAAM,EACb,UAAU,MAAM,EAChB,cAAc,MAAM,EACpB,UAAU,uBAAuB,KAChC,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,MAAM,EAAE,CAOrD,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,OAAO,MAAM,KACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAGvB,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,MAAM,GAAG,IAAI,CASvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,OAAO,MAAM,KACZ,OAAO,CAAC,UAAU,GAAG,IAAI,CAI3B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,OAAO,MAAM,KAAG,OAAO,CAAC,OAAO,CAGpE,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,OAAO,MAAM,KAAG,OAAO,CAAC,IAAI,CAE9D,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,OAAO,CAiBjB,CAAC;AAEF,eAAO,MAAM,SAAS,GACpB,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,MAAM,CAoBhB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,MAAM,EACb,MAAM,OAAO,KACZ,OAAO,CAAC,IAAI,CAWd,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,OAAO,MAAM,KAAG,OAAO,CAAC,MAAM,CAUtE,CAAC;AAEF,eAAO,MAAM,OAAO,QAAa,OAAO,CAAC,MAAM,EAAE,CAUhD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAa,OAAO,CAAC,SAAS,EAAE,CA8B/D,CAAC"}
|
package/dist/adb.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
|
-
import { spawn } from '@react-native-harness/tools';
|
|
1
|
+
import { spawn, SubprocessError } from '@react-native-harness/tools';
|
|
2
|
+
export const getStartAppArgs = (bundleId, activityName, options) => {
|
|
3
|
+
const args = [
|
|
4
|
+
'shell',
|
|
5
|
+
'am',
|
|
6
|
+
'start',
|
|
7
|
+
'-a',
|
|
8
|
+
'android.intent.action.MAIN',
|
|
9
|
+
'-c',
|
|
10
|
+
'android.intent.category.LAUNCHER',
|
|
11
|
+
'-n',
|
|
12
|
+
`${bundleId}/${activityName}`,
|
|
13
|
+
];
|
|
14
|
+
const extras = options?.extras ?? {};
|
|
15
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
args.push('--es', key, value);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'boolean') {
|
|
21
|
+
args.push('--ez', key, value ? 'true' : 'false');
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isSafeInteger(value)) {
|
|
25
|
+
throw new Error(`Android app launch option "${key}" must be a safe integer.`);
|
|
26
|
+
}
|
|
27
|
+
args.push('--ei', key, value.toString());
|
|
28
|
+
}
|
|
29
|
+
return args;
|
|
30
|
+
};
|
|
2
31
|
export const isAppInstalled = async (adbId, bundleId) => {
|
|
3
32
|
const { stdout } = await spawn('adb', [
|
|
4
33
|
'-s',
|
|
@@ -23,20 +52,8 @@ export const reversePort = async (adbId, port, hostPort = port) => {
|
|
|
23
52
|
export const stopApp = async (adbId, bundleId) => {
|
|
24
53
|
await spawn('adb', ['-s', adbId, 'shell', 'am', 'force-stop', bundleId]);
|
|
25
54
|
};
|
|
26
|
-
export const startApp = async (adbId, bundleId, activityName) => {
|
|
27
|
-
await spawn('adb', [
|
|
28
|
-
'-s',
|
|
29
|
-
adbId,
|
|
30
|
-
'shell',
|
|
31
|
-
'am',
|
|
32
|
-
'start',
|
|
33
|
-
'-a',
|
|
34
|
-
'android.intent.action.MAIN',
|
|
35
|
-
'-c',
|
|
36
|
-
'android.intent.category.LAUNCHER',
|
|
37
|
-
'-n',
|
|
38
|
-
`${bundleId}/${activityName}`,
|
|
39
|
-
]);
|
|
55
|
+
export const startApp = async (adbId, bundleId, activityName, options) => {
|
|
56
|
+
await spawn('adb', ['-s', adbId, ...getStartAppArgs(bundleId, activityName, options)]);
|
|
40
57
|
};
|
|
41
58
|
export const getDeviceIds = async () => {
|
|
42
59
|
const { stdout } = await spawn('adb', ['devices']);
|
|
@@ -73,14 +90,63 @@ export const stopEmulator = async (adbId) => {
|
|
|
73
90
|
await spawn('adb', ['-s', adbId, 'emu', 'kill']);
|
|
74
91
|
};
|
|
75
92
|
export const isAppRunning = async (adbId, bundleId) => {
|
|
93
|
+
try {
|
|
94
|
+
const { stdout } = await spawn('adb', [
|
|
95
|
+
'-s',
|
|
96
|
+
adbId,
|
|
97
|
+
'shell',
|
|
98
|
+
'pidof',
|
|
99
|
+
bundleId,
|
|
100
|
+
]);
|
|
101
|
+
return stdout.trim() !== '';
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof SubprocessError && error.exitCode === 1) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
export const getAppUid = async (adbId, bundleId) => {
|
|
76
111
|
const { stdout } = await spawn('adb', [
|
|
77
112
|
'-s',
|
|
78
113
|
adbId,
|
|
79
114
|
'shell',
|
|
80
|
-
'
|
|
81
|
-
|
|
115
|
+
'pm',
|
|
116
|
+
'list',
|
|
117
|
+
'packages',
|
|
118
|
+
'-U',
|
|
82
119
|
]);
|
|
83
|
-
|
|
120
|
+
const line = stdout
|
|
121
|
+
.split('\n')
|
|
122
|
+
.find((entry) => entry.includes(`package:${bundleId}`));
|
|
123
|
+
const match = line?.match(/\buid:(\d+)\b/);
|
|
124
|
+
if (!match) {
|
|
125
|
+
throw new Error(`Failed to resolve Android app UID for "${bundleId}".`);
|
|
126
|
+
}
|
|
127
|
+
return Number(match[1]);
|
|
128
|
+
};
|
|
129
|
+
export const setHideErrorDialogs = async (adbId, hide) => {
|
|
130
|
+
await spawn('adb', [
|
|
131
|
+
'-s',
|
|
132
|
+
adbId,
|
|
133
|
+
'shell',
|
|
134
|
+
'settings',
|
|
135
|
+
'put',
|
|
136
|
+
'global',
|
|
137
|
+
'hide_error_dialogs',
|
|
138
|
+
hide ? '1' : '0',
|
|
139
|
+
]);
|
|
140
|
+
};
|
|
141
|
+
export const getLogcatTimestamp = async (adbId) => {
|
|
142
|
+
const { stdout } = await spawn('adb', [
|
|
143
|
+
'-s',
|
|
144
|
+
adbId,
|
|
145
|
+
'shell',
|
|
146
|
+
'date',
|
|
147
|
+
"+'%m-%d %H:%M:%S.000'",
|
|
148
|
+
]);
|
|
149
|
+
return stdout.trim().replace(/^'+|'+$/g, '');
|
|
84
150
|
};
|
|
85
151
|
export const getAvds = async () => {
|
|
86
152
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AppMonitor, type AppCrashDetails, type CrashArtifactWriter, type CrashDetailsLookupOptions, type AppMonitorEvent } from '@react-native-harness/platforms';
|
|
2
|
+
declare const createAndroidLogEvent: (line: string, bundleId: string) => AppMonitorEvent | null;
|
|
3
|
+
export declare const createAndroidAppMonitor: ({ adbId, bundleId, appUid, crashArtifactWriter, }: {
|
|
4
|
+
adbId: string;
|
|
5
|
+
bundleId: string;
|
|
6
|
+
appUid: number;
|
|
7
|
+
crashArtifactWriter?: CrashArtifactWriter;
|
|
8
|
+
}) => AndroidAppMonitor;
|
|
9
|
+
export { createAndroidLogEvent };
|
|
10
|
+
export type AndroidAppMonitor = AppMonitor & {
|
|
11
|
+
getCrashDetails: (options: CrashDetailsLookupOptions) => Promise<AppCrashDetails | null>;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=app-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-monitor.d.ts","sourceRoot":"","sources":["../src/app-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EAErB,MAAM,iCAAiC,CAAC;AAuRzC,QAAA,MAAM,qBAAqB,GACzB,MAAM,MAAM,EACZ,UAAU,MAAM,KACf,eAAe,GAAG,IAwEpB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,mDAKrC;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,KAAG,iBAmKH,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAC3C,eAAe,EAAE,CACf,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC,CAAC"}
|