@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.
@@ -0,0 +1,205 @@
1
+ import { spawn, SubprocessError } from '@react-native-harness/tools';
2
+
3
+ const DEBUG_HTTP_HOST_BLOCK_START =
4
+ '<!-- react-native-harness:debug_http_host:start -->';
5
+ const DEBUG_HTTP_HOST_BLOCK_END =
6
+ '<!-- react-native-harness:debug_http_host:end -->';
7
+ const DEBUG_HTTP_HOST_BACKUP_KEY = 'harness_debug_http_host_backup';
8
+
9
+ const getSharedPrefsPath = (bundleId: string) =>
10
+ `shared_prefs/${bundleId}_preferences.xml`;
11
+
12
+ const escapeRegExp = (value: string): string =>
13
+ value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
+
15
+ const escapeXml = (value: string): string =>
16
+ value
17
+ .replaceAll('&', '&amp;')
18
+ .replaceAll('<', '&lt;')
19
+ .replaceAll('>', '&gt;')
20
+ .replaceAll('"', '&quot;')
21
+ .replaceAll("'", '&apos;');
22
+
23
+ const unescapeXml = (value: string): string =>
24
+ value
25
+ .replaceAll('&apos;', "'")
26
+ .replaceAll('&quot;', '"')
27
+ .replaceAll('&gt;', '>')
28
+ .replaceAll('&lt;', '<')
29
+ .replaceAll('&amp;', '&');
30
+
31
+ const getStringPreferenceRegex = (key: string) =>
32
+ new RegExp(
33
+ `<string\\s+name="${escapeRegExp(key)}">([\\s\\S]*?)<\\/string>`,
34
+ 'g'
35
+ );
36
+
37
+ const getStringPreferenceValue = (
38
+ content: string,
39
+ key: string
40
+ ): string | null => {
41
+ const matches = [...content.matchAll(getStringPreferenceRegex(key))];
42
+ const value = matches.at(-1)?.[1];
43
+
44
+ return value == null ? null : unescapeXml(value);
45
+ };
46
+
47
+ const renameStringPreference = (
48
+ content: string,
49
+ fromKey: string,
50
+ toKey: string
51
+ ): string =>
52
+ content.replace(
53
+ new RegExp(
54
+ `(<string\\s+name=")${escapeRegExp(fromKey)}(">[\\s\\S]*?<\\/string>)`,
55
+ 'g'
56
+ ),
57
+ `$1${toKey}$2`
58
+ );
59
+
60
+ const stripStringPreference = (content: string, key: string): string =>
61
+ content.replace(
62
+ new RegExp(
63
+ `\\s*<string\\s+name="${escapeRegExp(key)}">[\\s\\S]*?<\\/string>\\s*`,
64
+ 'g'
65
+ ),
66
+ '\n'
67
+ );
68
+
69
+ const normalizeEmptyMap = (content: string): string =>
70
+ content.replace(/<map\s*\/>/g, '<map>\n</map>');
71
+
72
+ const getHarnessDebugHttpHostBlock = (host: string) =>
73
+ [
74
+ DEBUG_HTTP_HOST_BLOCK_START,
75
+ `<string name="debug_http_host">${escapeXml(host)}</string>`,
76
+ DEBUG_HTTP_HOST_BLOCK_END,
77
+ ].join('\n');
78
+
79
+ const stripHarnessDebugHttpHostBlock = (content: string): string =>
80
+ content.replace(
81
+ new RegExp(
82
+ `\\s*${escapeRegExp(
83
+ DEBUG_HTTP_HOST_BLOCK_START
84
+ )}\\s*\\n[\\s\\S]*?\\n\\s*${escapeRegExp(DEBUG_HTTP_HOST_BLOCK_END)}\\s*`,
85
+ 'g'
86
+ ),
87
+ '\n'
88
+ );
89
+
90
+ const normalizeSharedPrefsContent = (content: string | null): string => {
91
+ if (!content?.trim()) {
92
+ return ['<?xml version="1.0" encoding="utf-8"?>', '<map>', '</map>'].join(
93
+ '\n'
94
+ );
95
+ }
96
+
97
+ return normalizeEmptyMap(stripHarnessDebugHttpHostBlock(content)).trim();
98
+ };
99
+
100
+ const insertBeforeClosingMap = (content: string, block: string): string => {
101
+ if (!content.includes('</map>')) {
102
+ throw new Error('Android shared preferences file is missing </map>.');
103
+ }
104
+
105
+ return content.replace(
106
+ /<\/map>\s*$/,
107
+ ` ${block.replace(/\n/g, '\n ')}\n</map>`
108
+ );
109
+ };
110
+
111
+ const readSharedPrefsFile = async (
112
+ adbId: string,
113
+ bundleId: string
114
+ ): Promise<string | null> => {
115
+ try {
116
+ const { stdout } = await spawn('adb', [
117
+ '-s',
118
+ adbId,
119
+ 'shell',
120
+ `run-as ${bundleId} cat ${getSharedPrefsPath(bundleId)}`,
121
+ ]);
122
+ return stdout;
123
+ } catch (error) {
124
+ if (error instanceof SubprocessError && error.exitCode === 1) {
125
+ return null;
126
+ }
127
+
128
+ throw error;
129
+ }
130
+ };
131
+
132
+ const writeSharedPrefsFile = async (
133
+ adbId: string,
134
+ bundleId: string,
135
+ content: string
136
+ ): Promise<void> => {
137
+ await spawn(
138
+ 'adb',
139
+ [
140
+ '-s',
141
+ adbId,
142
+ 'shell',
143
+ `run-as ${bundleId} sh -c 'mkdir -p shared_prefs && cat > ${getSharedPrefsPath(
144
+ bundleId
145
+ )}'`,
146
+ ],
147
+ { stdin: { string: `${content.trim()}\n` } }
148
+ );
149
+ };
150
+
151
+ export const applyHarnessDebugHttpHost = async (
152
+ adbId: string,
153
+ bundleId: string,
154
+ host: string
155
+ ): Promise<void> => {
156
+ const existingContent = await readSharedPrefsFile(adbId, bundleId);
157
+ const normalizedContent = normalizeSharedPrefsContent(existingContent);
158
+ const existingHost = getStringPreferenceValue(
159
+ normalizedContent,
160
+ 'debug_http_host'
161
+ );
162
+ const contentWithBackup =
163
+ existingHost == null
164
+ ? normalizedContent
165
+ : renameStringPreference(
166
+ stripStringPreference(normalizedContent, DEBUG_HTTP_HOST_BACKUP_KEY),
167
+ 'debug_http_host',
168
+ DEBUG_HTTP_HOST_BACKUP_KEY
169
+ );
170
+ const nextContent = insertBeforeClosingMap(
171
+ contentWithBackup,
172
+ getHarnessDebugHttpHostBlock(host)
173
+ );
174
+ await writeSharedPrefsFile(adbId, bundleId, nextContent);
175
+ };
176
+
177
+ export const clearHarnessDebugHttpHost = async (
178
+ adbId: string,
179
+ bundleId: string
180
+ ): Promise<void> => {
181
+ const existingContent = await readSharedPrefsFile(adbId, bundleId);
182
+
183
+ if (!existingContent) {
184
+ return;
185
+ }
186
+
187
+ const nextContentWithoutHarnessBlock =
188
+ stripHarnessDebugHttpHostBlock(existingContent).trim();
189
+
190
+ if (nextContentWithoutHarnessBlock === existingContent.trim()) {
191
+ return;
192
+ }
193
+
194
+ const restoredContent = renameStringPreference(
195
+ nextContentWithoutHarnessBlock,
196
+ DEBUG_HTTP_HOST_BACKUP_KEY,
197
+ 'debug_http_host'
198
+ );
199
+
200
+ await writeSharedPrefsFile(
201
+ adbId,
202
+ bundleId,
203
+ normalizeEmptyMap(restoredContent).trim()
204
+ );
205
+ };
package/tsconfig.json CHANGED
@@ -4,10 +4,10 @@
4
4
  "include": [],
5
5
  "references": [
6
6
  {
7
- "path": "../config"
7
+ "path": "../tools"
8
8
  },
9
9
  {
10
- "path": "../tools"
10
+ "path": "../config"
11
11
  },
12
12
  {
13
13
  "path": "../platforms"
package/tsconfig.lib.json CHANGED
@@ -12,10 +12,10 @@
12
12
  "include": ["src/**/*.ts"],
13
13
  "references": [
14
14
  {
15
- "path": "../config/tsconfig.lib.json"
15
+ "path": "../tools/tsconfig.lib.json"
16
16
  },
17
17
  {
18
- "path": "../tools/tsconfig.lib.json"
18
+ "path": "../config/tsconfig.lib.json"
19
19
  },
20
20
  {
21
21
  "path": "../platforms/tsconfig.lib.json"
@@ -1,5 +0,0 @@
1
- import { Config, NativeTestRunnerConfig } from './types.js';
2
- export declare function assertNativeRunner(config: Config): asserts config is Config & {
3
- runner: NativeTestRunnerConfig;
4
- };
5
- //# sourceMappingURL=assertions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE5D,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,IAAI,MAAM,GAAG;IAAE,MAAM,EAAE,sBAAsB,CAAA;CAAE,CAO/D"}
@@ -1,6 +0,0 @@
1
- export function assertNativeRunner(config) {
2
- if (config.runner.platform !== 'ios' &&
3
- config.runner.platform !== 'android') {
4
- throw new Error('Runner is not a native runner');
5
- }
6
- }
@@ -1,6 +0,0 @@
1
- export type AndroidEmulator = {
2
- adbId: string;
3
- stop: () => Promise<void>;
4
- };
5
- export declare const runEmulator: (avdName: string) => Promise<AndroidEmulator>;
6
- //# sourceMappingURL=emulator.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"emulator.d.ts","sourceRoot":"","sources":["../src/emulator.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,SAAS,MAAM,KACd,OAAO,CAAC,eAAe,CA8BzB,CAAC"}
package/dist/emulator.js DELETED
@@ -1,27 +0,0 @@
1
- import { spawn } from '@react-native-harness/tools';
2
- import * as adb from './adb.js';
3
- export const runEmulator = async (avdName) => {
4
- const process = spawn('emulator', ['-avd', avdName]);
5
- await process.nodeChildProcess;
6
- const adbId = await adb.getEmulatorName(avdName);
7
- if (!adbId) {
8
- throw new Error('Emulator not found');
9
- }
10
- // Poll for emulator status until it's fully running
11
- const checkStatus = async () => {
12
- const status = await adb.isBootCompleted(adbId);
13
- if (!status) {
14
- await new Promise((resolve) => setTimeout(resolve, 2000));
15
- await checkStatus();
16
- }
17
- };
18
- // Start checking status after a brief delay to allow emulator to start
19
- await new Promise((resolve) => setTimeout(resolve, 3000));
20
- await checkStatus();
21
- return {
22
- adbId,
23
- stop: async () => {
24
- await adb.stopEmulator(adbId);
25
- },
26
- };
27
- };
package/dist/errors.d.ts DELETED
@@ -1,15 +0,0 @@
1
- export declare class ConfigValidationError extends Error {
2
- readonly filePath: string;
3
- readonly validationErrors: string[];
4
- constructor(filePath: string, validationErrors: string[]);
5
- }
6
- export declare class ConfigNotFoundError extends Error {
7
- readonly searchPath: string;
8
- constructor(searchPath: string);
9
- }
10
- export declare class ConfigLoadError extends Error {
11
- readonly filePath: string;
12
- readonly cause?: Error;
13
- constructor(filePath: string, cause?: Error);
14
- }
15
- //# sourceMappingURL=errors.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,qBAAsB,SAAQ,KAAK;aAExB,QAAQ,EAAE,MAAM;aAChB,gBAAgB,EAAE,MAAM,EAAE;gBAD1B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,EAAE;CAKjD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aACd,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAIjD;AAED,qBAAa,eAAgB,SAAQ,KAAK;aAGV,QAAQ,EAAE,MAAM;IAF5C,SAAyB,KAAK,CAAC,EAAE,KAAK,CAAC;gBAEX,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK;CAK9D"}
package/dist/errors.js DELETED
@@ -1,28 +0,0 @@
1
- export class ConfigValidationError extends Error {
2
- filePath;
3
- validationErrors;
4
- constructor(filePath, validationErrors) {
5
- super(`Invalid configuration in ${filePath}`);
6
- this.filePath = filePath;
7
- this.validationErrors = validationErrors;
8
- this.name = 'ConfigValidationError';
9
- }
10
- }
11
- export class ConfigNotFoundError extends Error {
12
- searchPath;
13
- constructor(searchPath) {
14
- super(`Config file not found in ${searchPath} or any parent directories`);
15
- this.searchPath = searchPath;
16
- this.name = 'ConfigNotFoundError';
17
- }
18
- }
19
- export class ConfigLoadError extends Error {
20
- filePath;
21
- cause;
22
- constructor(filePath, cause) {
23
- super(`Failed to load config file ${filePath}`);
24
- this.filePath = filePath;
25
- this.name = 'ConfigLoadError';
26
- this.cause = cause;
27
- }
28
- }
package/dist/reader.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { Config } from './types.js';
2
- export declare const getConfig: (dir: string) => Promise<{
3
- config: Config;
4
- projectRoot: string;
5
- }>;
6
- //# sourceMappingURL=reader.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAgB,MAAM,YAAY,CAAC;AAsElD,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,KACV,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAUjD,CAAC"}
package/dist/reader.js DELETED
@@ -1,57 +0,0 @@
1
- import { ConfigSchema } from './types.js';
2
- import { ConfigValidationError, ConfigNotFoundError, ConfigLoadError, } from './errors.js';
3
- import path from 'node:path';
4
- import fs from 'node:fs';
5
- import { createRequire } from 'node:module';
6
- import { ZodError } from 'zod';
7
- const extensions = ['.js', '.mjs', '.cjs', '.json'];
8
- const importUp = async (dir, name) => {
9
- const filePath = path.join(dir, name);
10
- for (const ext of extensions) {
11
- const filePathWithExt = `${filePath}${ext}`;
12
- if (fs.existsSync(filePathWithExt)) {
13
- let rawConfig;
14
- try {
15
- if (ext === '.mjs') {
16
- rawConfig = await import(filePathWithExt).then((module) => module.default);
17
- }
18
- else {
19
- const require = createRequire(import.meta.url);
20
- rawConfig = require(filePathWithExt);
21
- }
22
- }
23
- catch (error) {
24
- throw new ConfigLoadError(filePathWithExt, error instanceof Error ? error : undefined);
25
- }
26
- try {
27
- const config = ConfigSchema.parse(rawConfig);
28
- return { config, filePathWithExt, configDir: dir };
29
- }
30
- catch (error) {
31
- if (error instanceof ZodError) {
32
- const validationErrors = error.errors.map((err) => {
33
- const path = err.path.length > 0 ? ` at "${err.path.join('.')}"` : '';
34
- return `${err.message}${path}`;
35
- });
36
- throw new ConfigValidationError(filePathWithExt, validationErrors);
37
- }
38
- throw error;
39
- }
40
- }
41
- }
42
- const parentDir = path.dirname(dir);
43
- if (parentDir === dir) {
44
- throw new ConfigNotFoundError(dir);
45
- }
46
- return importUp(parentDir, name);
47
- };
48
- export const getConfig = async (dir) => {
49
- const { config, configDir } = await importUp(dir, 'rn-harness.config');
50
- return {
51
- config: {
52
- ...config,
53
- reporter: config.reporter,
54
- },
55
- projectRoot: configDir,
56
- };
57
- };