@react-native-harness/cli 1.0.0-alpha.14 → 1.0.0-alpha.17

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.
Files changed (52) hide show
  1. package/dist/bundlers/metro.d.ts.map +1 -1
  2. package/dist/bundlers/metro.js +8 -2
  3. package/dist/commands/test.d.ts.map +1 -1
  4. package/dist/commands/test.js +4 -2
  5. package/dist/errors/errorHandler.d.ts +1 -1
  6. package/dist/errors/errorHandler.d.ts.map +1 -1
  7. package/dist/errors/errorHandler.js +114 -98
  8. package/dist/errors/errors.d.ts +4 -0
  9. package/dist/errors/errors.d.ts.map +1 -1
  10. package/dist/errors/errors.js +8 -0
  11. package/dist/external.d.ts +11 -0
  12. package/dist/external.d.ts.map +1 -0
  13. package/dist/external.js +27 -0
  14. package/dist/index.js +58 -49
  15. package/dist/jest.d.ts +2 -0
  16. package/dist/jest.d.ts.map +1 -0
  17. package/dist/jest.js +7 -0
  18. package/dist/platforms/android/emulator.d.ts +0 -1
  19. package/dist/platforms/android/emulator.d.ts.map +1 -1
  20. package/dist/platforms/android/emulator.js +26 -20
  21. package/dist/platforms/ios/build.d.ts.map +1 -1
  22. package/dist/platforms/ios/build.js +5 -1
  23. package/dist/platforms/ios/device.d.ts +6 -2
  24. package/dist/platforms/ios/device.d.ts.map +1 -1
  25. package/dist/platforms/ios/simulator.d.ts.map +1 -1
  26. package/dist/platforms/ios/simulator.js +8 -3
  27. package/dist/platforms/platform-registry.js +1 -1
  28. package/dist/platforms/vega/device.d.ts.map +1 -1
  29. package/dist/process.js +1 -1
  30. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  31. package/dist/utils.d.ts +1 -0
  32. package/dist/utils.d.ts.map +1 -1
  33. package/dist/utils.js +15 -0
  34. package/eslint.config.mjs +1 -0
  35. package/package.json +26 -9
  36. package/src/bundlers/metro.ts +10 -2
  37. package/src/errors/errorHandler.ts +119 -109
  38. package/src/errors/errors.ts +7 -0
  39. package/src/external.ts +47 -0
  40. package/src/index.ts +65 -72
  41. package/src/platforms/android/emulator.ts +26 -28
  42. package/src/platforms/ios/build.ts +3 -0
  43. package/src/platforms/ios/device.ts +5 -2
  44. package/src/platforms/ios/simulator.ts +8 -3
  45. package/src/platforms/platform-registry.ts +1 -1
  46. package/src/platforms/vega/device.ts +2 -2
  47. package/src/process.ts +1 -1
  48. package/src/utils.ts +17 -0
  49. package/tsconfig.json +3 -0
  50. package/src/commands/test.ts +0 -228
  51. package/src/discovery/index.ts +0 -2
  52. package/src/discovery/testDiscovery.ts +0 -50
@@ -7,7 +7,13 @@ export const getEmulatorNameFromId = async (
7
7
  emulatorId: string
8
8
  ): Promise<string | null> => {
9
9
  try {
10
- const { stdout } = await spawn('adb', ['-s', emulatorId, 'emu', 'avd', 'name']);
10
+ const { stdout } = await spawn('adb', [
11
+ '-s',
12
+ emulatorId,
13
+ 'emu',
14
+ 'avd',
15
+ 'name',
16
+ ]);
11
17
  const avdName = stdout.split('\n')[0].trim();
12
18
  return avdName || null;
13
19
  } catch {
@@ -48,7 +54,13 @@ export const getEmulatorStatus = async (
48
54
 
49
55
  try {
50
56
  // Check if device is fully booted by checking boot completion
51
- const { stdout } = await spawn('adb', ['-s', emulatorId, 'shell', 'getprop', 'sys.boot_completed']);
57
+ const { stdout } = await spawn('adb', [
58
+ '-s',
59
+ emulatorId,
60
+ 'shell',
61
+ 'getprop',
62
+ 'sys.boot_completed',
63
+ ]);
52
64
  const bootCompleted = stdout.trim() === '1';
53
65
  return bootCompleted ? 'running' : 'loading';
54
66
  } catch {
@@ -69,17 +81,17 @@ export const runEmulator = async (name: string): Promise<ChildProcess> => {
69
81
  return;
70
82
  } else if (status === 'loading') {
71
83
  // Check again in 2 seconds
72
- await new Promise(resolve => setTimeout(resolve, 2000));
84
+ await new Promise((resolve) => setTimeout(resolve, 2000));
73
85
  await checkStatus();
74
86
  } else {
75
87
  // Still stopped, check again in 1 second
76
- await new Promise(resolve => setTimeout(resolve, 1000));
88
+ await new Promise((resolve) => setTimeout(resolve, 1000));
77
89
  await checkStatus();
78
90
  }
79
91
  };
80
92
 
81
93
  // Start checking status after a brief delay to allow emulator to start
82
- await new Promise(resolve => setTimeout(resolve, 3000));
94
+ await new Promise((resolve) => setTimeout(resolve, 3000));
83
95
  await checkStatus();
84
96
 
85
97
  return nodeChildProcess;
@@ -105,7 +117,15 @@ export const isAppInstalled = async (
105
117
  bundleId: string
106
118
  ): Promise<boolean> => {
107
119
  try {
108
- const { stdout } = await spawn('adb', ['-s', emulatorId, 'shell', 'pm', 'list', 'packages', bundleId]);
120
+ const { stdout } = await spawn('adb', [
121
+ '-s',
122
+ emulatorId,
123
+ 'shell',
124
+ 'pm',
125
+ 'list',
126
+ 'packages',
127
+ bundleId,
128
+ ]);
109
129
  return stdout.trim() !== '';
110
130
  } catch {
111
131
  return false;
@@ -115,25 +135,3 @@ export const isAppInstalled = async (
115
135
  export const reversePort = async (port: number): Promise<void> => {
116
136
  await spawn('adb', ['reverse', `tcp:${port}`, `tcp:${port}`]);
117
137
  };
118
-
119
- export const getEmulatorScreenshot = async (
120
- emulatorId: string,
121
- name: string = `${emulatorId}-${new Date()
122
- .toISOString()
123
- .replace(/:/g, '-')
124
- .replace(/\//g, '-')}.png`
125
- ): Promise<string> => {
126
- // Use screencap to save directly to device, then pull the file
127
- const devicePath = '/sdcard/screenshot.png';
128
-
129
- // Take screenshot and save to device
130
- await spawn('adb', ['-s', emulatorId, 'shell', 'screencap', '-p', devicePath]);
131
-
132
- // Pull the file from device to local
133
- await spawn('adb', ['-s', emulatorId, 'pull', devicePath, name]);
134
-
135
- // Clean up the file on device
136
- await spawn('adb', ['-s', emulatorId, 'shell', 'rm', devicePath]);
137
-
138
- return name;
139
- };
@@ -1,5 +1,6 @@
1
1
  import { spawn, spawnAndForget } from '@react-native-harness/tools';
2
2
 
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
4
  export const listDevices = async (): Promise<any> => {
4
5
  const { stdout } = await spawn('xcrun', [
5
6
  'simctl',
@@ -13,6 +14,7 @@ export const listDevices = async (): Promise<any> => {
13
14
  export const getDeviceByName = async (
14
15
  simulatorName: string,
15
16
  systemVersion: string
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
18
  ): Promise<any | null> => {
17
19
  const devices = await listDevices();
18
20
  const expectedRuntimeId = `com.apple.CoreSimulator.SimRuntime.iOS-${systemVersion.replace(
@@ -27,6 +29,7 @@ export const getDeviceByName = async (
27
29
  }
28
30
 
29
31
  const runtimeDevices = devices.devices[runtime];
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
33
  const device = runtimeDevices.find((d: any) => d.name === simulatorName);
31
34
 
32
35
  if (device) {
@@ -1,6 +1,9 @@
1
1
  import { spawn, spawnAndForget } from '@react-native-harness/tools';
2
2
 
3
- export const listDevices = async (): Promise<any> => {
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ type Device = any;
5
+
6
+ export const listDevices = async (): Promise<{ devices: Device[] }> => {
4
7
  const { stdout } = await spawn('xcrun', [
5
8
  'simctl',
6
9
  'list',
@@ -12,7 +15,7 @@ export const listDevices = async (): Promise<any> => {
12
15
 
13
16
  export const getDeviceByName = async (
14
17
  simulatorName: string
15
- ): Promise<any | null> => {
18
+ ): Promise<Device | null> => {
16
19
  const devices = await listDevices();
17
20
 
18
21
  for (const runtime in devices.devices) {
@@ -1,4 +1,4 @@
1
- import { spawn } from '@react-native-harness/tools';
1
+ import { spawn, SubprocessError } from '@react-native-harness/tools';
2
2
 
3
3
  export type IOSSimulatorStatus = 'stopped' | 'loading' | 'running';
4
4
 
@@ -25,6 +25,7 @@ export const getSimulatorDeviceId = async (
25
25
  return null;
26
26
  }
27
27
 
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
29
  const device = runtime.find((d: any) => d.name === simulatorName);
29
30
 
30
31
  if (device) {
@@ -57,6 +58,7 @@ export const getAvailableSimulators = async (): Promise<
57
58
  for (const runtime in devices.devices) {
58
59
  if (runtime.includes('iOS')) {
59
60
  const runtimeDevices = devices.devices[runtime];
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
62
  runtimeDevices.forEach((device: any) => {
61
63
  if (device.isAvailable) {
62
64
  simulators.push({
@@ -90,6 +92,7 @@ export const getSimulatorStatus = async (
90
92
  for (const runtime in devices.devices) {
91
93
  if (runtime.includes('iOS')) {
92
94
  const runtimeDevices = devices.devices[runtime];
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
96
  const device = runtimeDevices.find((d: any) => d.udid === udid);
94
97
 
95
98
  if (device) {
@@ -114,9 +117,10 @@ export const getSimulatorStatus = async (
114
117
  export const runSimulator = async (udid: string): Promise<void> => {
115
118
  try {
116
119
  await spawn('xcrun', ['simctl', 'boot', udid]);
117
- } catch (bootError: any) {
120
+ } catch (bootError) {
118
121
  // Ignore if simulator is already booted
119
122
  if (
123
+ bootError instanceof SubprocessError &&
120
124
  !bootError.stderr?.includes(
121
125
  'Unable to boot device in current state: Booted'
122
126
  )
@@ -153,9 +157,10 @@ export const stopSimulator = async (udid: string): Promise<void> => {
153
157
  const stopSimulatorById = async (udid: string): Promise<void> => {
154
158
  try {
155
159
  await spawn('xcrun', ['simctl', 'shutdown', udid]);
156
- } catch (shutdownError: any) {
160
+ } catch (shutdownError) {
157
161
  // Ignore if simulator is already shut down
158
162
  if (
163
+ shutdownError instanceof SubprocessError &&
159
164
  !shutdownError.stderr?.includes(
160
165
  'Unable to shutdown device in current state: Shutdown'
161
166
  )
@@ -20,7 +20,7 @@ export const getPlatformAdapter = async (
20
20
 
21
21
  try {
22
22
  return platformAdapters[platformName as keyof typeof platformAdapters];
23
- } catch (error) {
23
+ } catch {
24
24
  throw new Error(`Platform adapter for ${platformName} not found`);
25
25
  }
26
26
  };
@@ -152,7 +152,7 @@ export const uninstallApp = async (
152
152
  export const startPortForwarding = async (
153
153
  deviceId: string,
154
154
  port: number,
155
- forward: boolean = true
155
+ forward = true
156
156
  ): Promise<void> => {
157
157
  await spawn('kepler', [
158
158
  'device',
@@ -172,7 +172,7 @@ export const startPortForwarding = async (
172
172
  export const stopPortForwarding = async (
173
173
  deviceId: string,
174
174
  port: number,
175
- forward: boolean = true
175
+ forward = true
176
176
  ): Promise<void> => {
177
177
  await spawn('kepler', [
178
178
  'device',
package/src/process.ts CHANGED
@@ -25,7 +25,7 @@ export const killWithAwait = (child: ChildProcess): Promise<void> => {
25
25
 
26
26
  try {
27
27
  child.kill();
28
- } catch (error) {
28
+ } catch {
29
29
  clearTimeout(timeout);
30
30
  resolve();
31
31
  }
package/src/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import net from 'node:net';
2
+
1
3
  export function assert(condition: boolean, message: string): asserts condition {
2
4
  if (!condition) {
3
5
  throw new AssertionError(message);
@@ -10,3 +12,18 @@ export class AssertionError extends Error {
10
12
  this.name = 'AssertionError';
11
13
  }
12
14
  }
15
+
16
+ export const isPortAvailable = (port: number): Promise<boolean> => {
17
+ return new Promise((resolve) => {
18
+ const server = net.createServer();
19
+ server.once('error', () => {
20
+ server.close();
21
+ resolve(false);
22
+ });
23
+ server.once('listening', () => {
24
+ server.close();
25
+ resolve(true);
26
+ });
27
+ server.listen(port);
28
+ });
29
+ };
package/tsconfig.json CHANGED
@@ -3,6 +3,9 @@
3
3
  "files": [],
4
4
  "include": [],
5
5
  "references": [
6
+ {
7
+ "path": "../tools"
8
+ },
6
9
  {
7
10
  "path": "../config"
8
11
  },
@@ -1,228 +0,0 @@
1
- import {
2
- getBridgeServer,
3
- type BridgeServer,
4
- } from '@react-native-harness/bridge/server';
5
- import { TestExecutionOptions } from '@react-native-harness/bridge';
6
- import {
7
- Config,
8
- getConfig,
9
- TestRunnerConfig,
10
- } from '@react-native-harness/config';
11
- import { getPlatformAdapter } from '../platforms/platform-registry.js';
12
- import { intro, logger, outro, spinner } from '@react-native-harness/tools';
13
- import { type Environment } from '../platforms/platform-adapter.js';
14
- import { BridgeTimeoutError } from '../errors/errors.js';
15
- import { assert } from '../utils.js';
16
- import {
17
- EnvironmentInitializationError,
18
- NoRunnerSpecifiedError,
19
- RpcClientError,
20
- RunnerNotFoundError,
21
- } from '../errors/errors.js';
22
- import { TestSuiteResult } from '@react-native-harness/bridge';
23
- import {
24
- discoverTestFiles,
25
- type TestFilterOptions,
26
- } from '../discovery/index.js';
27
-
28
- type TestRunContext = {
29
- config: Config;
30
- runner: TestRunnerConfig;
31
- bridge?: BridgeServer;
32
- environment?: Environment;
33
- testFiles?: string[];
34
- results?: TestSuiteResult[];
35
- projectRoot: string;
36
- };
37
-
38
- const setupEnvironment = async (context: TestRunContext): Promise<void> => {
39
- const startSpinner = spinner();
40
- const platform = context.runner.platform;
41
-
42
- startSpinner.start(`Starting "${context.runner.name}" (${platform}) runner`);
43
-
44
- const platformAdapter = await getPlatformAdapter(platform);
45
- const serverBridge = await getBridgeServer({
46
- port: 3001,
47
- });
48
-
49
- context.bridge = serverBridge;
50
-
51
- const readyPromise = new Promise<void>((resolve, reject) => {
52
- const timeout = setTimeout(() => {
53
- reject(
54
- new BridgeTimeoutError(
55
- context.config.bridgeTimeout,
56
- context.runner.name,
57
- platform
58
- )
59
- );
60
- }, context.config.bridgeTimeout);
61
-
62
- serverBridge.once('ready', () => {
63
- clearTimeout(timeout);
64
- resolve();
65
- });
66
- });
67
-
68
- context.environment = await platformAdapter.getEnvironment(context.runner);
69
-
70
- logger.debug('Waiting for bridge to be ready');
71
- await readyPromise;
72
- logger.debug('Bridge is ready');
73
-
74
- if (!context.environment) {
75
- throw new EnvironmentInitializationError(
76
- 'Failed to initialize environment',
77
- context.runner.name,
78
- platform,
79
- 'Platform adapter returned null environment'
80
- );
81
- }
82
-
83
- startSpinner.stop(`"${context.runner.name}" (${platform}) runner started`);
84
- };
85
-
86
- const findTestFiles = async (
87
- context: TestRunContext,
88
- options: TestFilterOptions = {}
89
- ): Promise<void> => {
90
- const discoverSpinner = spinner();
91
- discoverSpinner.start('Discovering tests');
92
-
93
- context.testFiles = await discoverTestFiles(
94
- context.projectRoot,
95
- context.config.include,
96
- options
97
- );
98
-
99
- discoverSpinner.stop(`Found ${context.testFiles.length} test files`);
100
- };
101
-
102
- const runTests = async (
103
- context: TestRunContext,
104
- options: TestFilterOptions = {}
105
- ): Promise<void> => {
106
- const { bridge, environment, testFiles } = context;
107
- assert(bridge != null, 'Bridge not initialized');
108
- assert(environment != null, 'Environment not initialized');
109
- assert(testFiles != null, 'Test files not initialized');
110
-
111
- let runSpinner = spinner();
112
- runSpinner.start('Running tests');
113
-
114
- let shouldRestart = false;
115
-
116
- for (const testFile of testFiles) {
117
- if (shouldRestart) {
118
- runSpinner = spinner();
119
- runSpinner.start(`Restarting environment for next test file`);
120
-
121
- await new Promise((resolve) => {
122
- bridge.once('ready', resolve);
123
- environment.restart();
124
- });
125
- }
126
-
127
- runSpinner.message(`Running tests in ${testFile}`);
128
- const client = bridge.rpc.clients.at(-1);
129
- if (!client) {
130
- throw new RpcClientError(
131
- 'No RPC client available',
132
- 3001,
133
- 'No clients connected'
134
- );
135
- }
136
-
137
- // Pass only testNamePattern to runtime (file filtering already done)
138
- const executionOptions: TestExecutionOptions = {
139
- testNamePattern: options.testNamePattern,
140
- };
141
-
142
- const result = await client.runTests(testFile, executionOptions);
143
- context.results = [...(context.results ?? []), ...result.suites];
144
- shouldRestart = true;
145
- runSpinner.stop(`Test file ${testFile} completed`);
146
- }
147
-
148
- runSpinner.stop('Tests completed');
149
- };
150
-
151
- const cleanUp = async (context: TestRunContext): Promise<void> => {
152
- if (context.bridge) {
153
- context.bridge.dispose();
154
- }
155
- if (context.environment) {
156
- await context.environment.dispose();
157
- }
158
- };
159
-
160
- const hasFailedTests = (results: TestSuiteResult[]): boolean => {
161
- for (const suite of results) {
162
- // Check if the suite itself failed
163
- if (suite.status === 'failed') {
164
- return true;
165
- }
166
-
167
- // Check individual tests in the suite
168
- for (const test of suite.tests) {
169
- if (test.status === 'failed') {
170
- return true;
171
- }
172
- }
173
-
174
- // Recursively check nested suites
175
- if (suite.suites && hasFailedTests(suite.suites)) {
176
- return true;
177
- }
178
- }
179
-
180
- return false;
181
- };
182
-
183
- export const testCommand = async (
184
- runnerName?: string,
185
- options: TestFilterOptions = {}
186
- ): Promise<void> => {
187
- intro('React Native Test Harness');
188
-
189
- const { config, projectRoot } = await getConfig(process.cwd());
190
- const selectedRunnerName = runnerName ?? config.defaultRunner;
191
-
192
- if (!selectedRunnerName) {
193
- throw new NoRunnerSpecifiedError(config.runners);
194
- }
195
-
196
- const runner = config.runners.find((r) => r.name === selectedRunnerName);
197
-
198
- if (!runner) {
199
- throw new RunnerNotFoundError(selectedRunnerName, config.runners);
200
- }
201
-
202
- const context: TestRunContext = {
203
- config,
204
- runner,
205
- testFiles: [],
206
- results: [],
207
- projectRoot,
208
- };
209
-
210
- try {
211
- await setupEnvironment(context);
212
- await findTestFiles(context, options);
213
- await runTests(context, options);
214
-
215
- assert(context.results != null, 'Results not initialized');
216
- config.reporter?.report(context.results);
217
- } finally {
218
- await cleanUp(context);
219
- }
220
-
221
- // Check if any tests failed and exit with appropriate code
222
- if (hasFailedTests(context.results)) {
223
- outro('Test run completed with failures');
224
- process.exit(1);
225
- } else {
226
- outro('Test run completed successfully');
227
- }
228
- };
@@ -1,2 +0,0 @@
1
- export { discoverTestFiles } from './testDiscovery.js';
2
- export type { TestFilterOptions } from './testDiscovery.js';
@@ -1,50 +0,0 @@
1
- import { Glob } from 'glob';
2
-
3
- export type TestFilterOptions = {
4
- testNamePattern?: string;
5
- testPathPattern?: string;
6
- testPathIgnorePatterns?: string[];
7
- testMatch?: string[];
8
- };
9
-
10
- /**
11
- * Discovers test files based on patterns and filtering options
12
- */
13
- export const discoverTestFiles = async (
14
- projectRoot: string,
15
- configInclude: string | string[],
16
- options: TestFilterOptions = {}
17
- ): Promise<string[]> => {
18
- // Priority: testMatch > configInclude
19
- const patterns = options.testMatch || configInclude;
20
- const patternArray = Array.isArray(patterns) ? patterns : [patterns];
21
-
22
- // Glob discovery
23
- const allFiles: string[] = [];
24
- for (const pattern of patternArray) {
25
- const glob = new Glob(pattern, { cwd: projectRoot, nodir: true });
26
- const files = await glob.walk();
27
- allFiles.push(...files);
28
- }
29
-
30
- // Remove duplicates
31
- let uniqueFiles = [...new Set(allFiles)];
32
-
33
- // Apply testPathPattern filtering
34
- if (options.testPathPattern) {
35
- const regex = new RegExp(options.testPathPattern);
36
- uniqueFiles = uniqueFiles.filter((file) => regex.test(file));
37
- }
38
-
39
- // Apply testPathIgnorePatterns filtering
40
- if (options.testPathIgnorePatterns?.length) {
41
- const ignoreRegexes = options.testPathIgnorePatterns.map(
42
- (p) => new RegExp(p)
43
- );
44
- uniqueFiles = uniqueFiles.filter(
45
- (file) => !ignoreRegexes.some((regex) => regex.test(file))
46
- );
47
- }
48
-
49
- return uniqueFiles;
50
- };