@react-native-harness/platform-android 1.1.0-rc.1 → 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__/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/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +2 -1
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +5 -1
- 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/package.json +4 -4
- package/src/__tests__/shared-prefs.test.ts +144 -0
- package/src/app-monitor.ts +3 -1
- package/src/runner.ts +11 -3
- package/src/shared-prefs.ts +205 -0
- package/tsconfig.json +2 -2
- package/tsconfig.lib.json +2 -2
- 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":"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
|
+
});
|
|
@@ -1 +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;
|
|
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"}
|
package/dist/app-monitor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { escapeRegExp, getEmitter, logger, spawn, SubprocessError } from '@react-native-harness/tools';
|
|
2
2
|
import * as adb from './adb.js';
|
|
3
3
|
import { androidCrashParser } from './crash-parser.js';
|
|
4
|
+
const androidAppMonitorLogger = logger.child('android-app-monitor');
|
|
4
5
|
const getLogcatArgs = (uid, fromTime) => ['logcat', '-v', 'threadtime', '-b', 'crash', `--uid=${uid}`, '-T', fromTime];
|
|
5
6
|
const MAX_RECENT_LOG_LINES = 200;
|
|
6
7
|
const MAX_RECENT_CRASH_ARTIFACTS = 10;
|
|
@@ -285,7 +286,7 @@ export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifact
|
|
|
285
286
|
}
|
|
286
287
|
catch (error) {
|
|
287
288
|
if (!(error instanceof SubprocessError && error.signalName === 'SIGTERM')) {
|
|
288
|
-
|
|
289
|
+
androidAppMonitorLogger.debug('Android logcat monitor stopped', error);
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
})();
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
-
import { Config } from '@react-native-harness/config';
|
|
2
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
3
3
|
import { type AndroidPlatformConfig } from './config.js';
|
|
4
|
-
declare const getAndroidRunner: (config: AndroidPlatformConfig, harnessConfig:
|
|
4
|
+
declare const getAndroidRunner: (config: AndroidPlatformConfig, harnessConfig: HarnessConfig) => Promise<HarnessPlatformRunner>;
|
|
5
5
|
export default getAndroidRunner;
|
|
6
6
|
//# sourceMappingURL=runner.d.ts.map
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,aAAa,CAAC;AAUrB,QAAA,MAAM,gBAAgB,GACpB,QAAQ,qBAAqB,EAC7B,eAAe,aAAa,KAC3B,OAAO,CAAC,qBAAqB,CAmE/B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
package/dist/runner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DeviceNotFoundError, AppNotInstalledError, } from '@react-native-harnes
|
|
|
2
2
|
import { AndroidPlatformConfigSchema, } from './config.js';
|
|
3
3
|
import { getAdbId } from './adb-id.js';
|
|
4
4
|
import * as adb from './adb.js';
|
|
5
|
+
import { applyHarnessDebugHttpHost, clearHarnessDebugHttpHost, } from './shared-prefs.js';
|
|
5
6
|
import { getDeviceName } from './utils.js';
|
|
6
7
|
import { createAndroidAppMonitor } from './app-monitor.js';
|
|
7
8
|
const getAndroidRunner = async (config, harnessConfig) => {
|
|
@@ -14,11 +15,13 @@ const getAndroidRunner = async (config, harnessConfig) => {
|
|
|
14
15
|
if (!isInstalled) {
|
|
15
16
|
throw new AppNotInstalledError(parsedConfig.bundleId, getDeviceName(parsedConfig.device));
|
|
16
17
|
}
|
|
18
|
+
const metroPort = harnessConfig.metroPort;
|
|
17
19
|
await Promise.all([
|
|
18
|
-
adb.reversePort(adbId,
|
|
20
|
+
adb.reversePort(adbId, metroPort),
|
|
19
21
|
adb.reversePort(adbId, 8080),
|
|
20
22
|
adb.reversePort(adbId, harnessConfig.webSocketPort),
|
|
21
23
|
adb.setHideErrorDialogs(adbId, true),
|
|
24
|
+
applyHarnessDebugHttpHost(adbId, parsedConfig.bundleId, `localhost:${metroPort}`),
|
|
22
25
|
]);
|
|
23
26
|
const appUid = await adb.getAppUid(adbId, parsedConfig.bundleId);
|
|
24
27
|
return {
|
|
@@ -36,6 +39,7 @@ const getAndroidRunner = async (config, harnessConfig) => {
|
|
|
36
39
|
},
|
|
37
40
|
dispose: async () => {
|
|
38
41
|
await adb.stopApp(adbId, parsedConfig.bundleId);
|
|
42
|
+
await clearHarnessDebugHttpHost(adbId, parsedConfig.bundleId);
|
|
39
43
|
await adb.setHideErrorDialogs(adbId, false);
|
|
40
44
|
},
|
|
41
45
|
isAppRunning: async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-prefs.d.ts","sourceRoot":"","sources":["../src/shared-prefs.ts"],"names":[],"mappings":"AAsJA,eAAO,MAAM,yBAAyB,GACpC,OAAO,MAAM,EACb,UAAU,MAAM,EAChB,MAAM,MAAM,KACX,OAAO,CAAC,IAAI,CAoBd,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,OAAO,MAAM,EACb,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAyBd,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn, SubprocessError } from '@react-native-harness/tools';
|
|
2
|
+
const DEBUG_HTTP_HOST_BLOCK_START = '<!-- react-native-harness:debug_http_host:start -->';
|
|
3
|
+
const DEBUG_HTTP_HOST_BLOCK_END = '<!-- react-native-harness:debug_http_host:end -->';
|
|
4
|
+
const DEBUG_HTTP_HOST_BACKUP_KEY = 'harness_debug_http_host_backup';
|
|
5
|
+
const getSharedPrefsPath = (bundleId) => `shared_prefs/${bundleId}_preferences.xml`;
|
|
6
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7
|
+
const escapeXml = (value) => value
|
|
8
|
+
.replaceAll('&', '&')
|
|
9
|
+
.replaceAll('<', '<')
|
|
10
|
+
.replaceAll('>', '>')
|
|
11
|
+
.replaceAll('"', '"')
|
|
12
|
+
.replaceAll("'", ''');
|
|
13
|
+
const unescapeXml = (value) => value
|
|
14
|
+
.replaceAll(''', "'")
|
|
15
|
+
.replaceAll('"', '"')
|
|
16
|
+
.replaceAll('>', '>')
|
|
17
|
+
.replaceAll('<', '<')
|
|
18
|
+
.replaceAll('&', '&');
|
|
19
|
+
const getStringPreferenceRegex = (key) => new RegExp(`<string\\s+name="${escapeRegExp(key)}">([\\s\\S]*?)<\\/string>`, 'g');
|
|
20
|
+
const getStringPreferenceValue = (content, key) => {
|
|
21
|
+
const matches = [...content.matchAll(getStringPreferenceRegex(key))];
|
|
22
|
+
const value = matches.at(-1)?.[1];
|
|
23
|
+
return value == null ? null : unescapeXml(value);
|
|
24
|
+
};
|
|
25
|
+
const renameStringPreference = (content, fromKey, toKey) => content.replace(new RegExp(`(<string\\s+name=")${escapeRegExp(fromKey)}(">[\\s\\S]*?<\\/string>)`, 'g'), `$1${toKey}$2`);
|
|
26
|
+
const stripStringPreference = (content, key) => content.replace(new RegExp(`\\s*<string\\s+name="${escapeRegExp(key)}">[\\s\\S]*?<\\/string>\\s*`, 'g'), '\n');
|
|
27
|
+
const normalizeEmptyMap = (content) => content.replace(/<map\s*\/>/g, '<map>\n</map>');
|
|
28
|
+
const getHarnessDebugHttpHostBlock = (host) => [
|
|
29
|
+
DEBUG_HTTP_HOST_BLOCK_START,
|
|
30
|
+
`<string name="debug_http_host">${escapeXml(host)}</string>`,
|
|
31
|
+
DEBUG_HTTP_HOST_BLOCK_END,
|
|
32
|
+
].join('\n');
|
|
33
|
+
const stripHarnessDebugHttpHostBlock = (content) => content.replace(new RegExp(`\\s*${escapeRegExp(DEBUG_HTTP_HOST_BLOCK_START)}\\s*\\n[\\s\\S]*?\\n\\s*${escapeRegExp(DEBUG_HTTP_HOST_BLOCK_END)}\\s*`, 'g'), '\n');
|
|
34
|
+
const normalizeSharedPrefsContent = (content) => {
|
|
35
|
+
if (!content?.trim()) {
|
|
36
|
+
return ['<?xml version="1.0" encoding="utf-8"?>', '<map>', '</map>'].join('\n');
|
|
37
|
+
}
|
|
38
|
+
return normalizeEmptyMap(stripHarnessDebugHttpHostBlock(content)).trim();
|
|
39
|
+
};
|
|
40
|
+
const insertBeforeClosingMap = (content, block) => {
|
|
41
|
+
if (!content.includes('</map>')) {
|
|
42
|
+
throw new Error('Android shared preferences file is missing </map>.');
|
|
43
|
+
}
|
|
44
|
+
return content.replace(/<\/map>\s*$/, ` ${block.replace(/\n/g, '\n ')}\n</map>`);
|
|
45
|
+
};
|
|
46
|
+
const readSharedPrefsFile = async (adbId, bundleId) => {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await spawn('adb', [
|
|
49
|
+
'-s',
|
|
50
|
+
adbId,
|
|
51
|
+
'shell',
|
|
52
|
+
`run-as ${bundleId} cat ${getSharedPrefsPath(bundleId)}`,
|
|
53
|
+
]);
|
|
54
|
+
return stdout;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof SubprocessError && error.exitCode === 1) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const writeSharedPrefsFile = async (adbId, bundleId, content) => {
|
|
64
|
+
await spawn('adb', [
|
|
65
|
+
'-s',
|
|
66
|
+
adbId,
|
|
67
|
+
'shell',
|
|
68
|
+
`run-as ${bundleId} sh -c 'mkdir -p shared_prefs && cat > ${getSharedPrefsPath(bundleId)}'`,
|
|
69
|
+
], { stdin: { string: `${content.trim()}\n` } });
|
|
70
|
+
};
|
|
71
|
+
export const applyHarnessDebugHttpHost = async (adbId, bundleId, host) => {
|
|
72
|
+
const existingContent = await readSharedPrefsFile(adbId, bundleId);
|
|
73
|
+
const normalizedContent = normalizeSharedPrefsContent(existingContent);
|
|
74
|
+
const existingHost = getStringPreferenceValue(normalizedContent, 'debug_http_host');
|
|
75
|
+
const contentWithBackup = existingHost == null
|
|
76
|
+
? normalizedContent
|
|
77
|
+
: renameStringPreference(stripStringPreference(normalizedContent, DEBUG_HTTP_HOST_BACKUP_KEY), 'debug_http_host', DEBUG_HTTP_HOST_BACKUP_KEY);
|
|
78
|
+
const nextContent = insertBeforeClosingMap(contentWithBackup, getHarnessDebugHttpHostBlock(host));
|
|
79
|
+
await writeSharedPrefsFile(adbId, bundleId, nextContent);
|
|
80
|
+
};
|
|
81
|
+
export const clearHarnessDebugHttpHost = async (adbId, bundleId) => {
|
|
82
|
+
const existingContent = await readSharedPrefsFile(adbId, bundleId);
|
|
83
|
+
if (!existingContent) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const nextContentWithoutHarnessBlock = stripHarnessDebugHttpHostBlock(existingContent).trim();
|
|
87
|
+
if (nextContentWithoutHarnessBlock === existingContent.trim()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const restoredContent = renameStringPreference(nextContentWithoutHarnessBlock, DEBUG_HTTP_HOST_BACKUP_KEY, 'debug_http_host');
|
|
91
|
+
await writeSharedPrefsFile(adbId, bundleId, normalizeEmptyMap(restoredContent).trim());
|
|
92
|
+
};
|