react-native-harness 0.0.0 → 1.0.0-alpha.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.
Files changed (103) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +7 -0
  3. package/bin.js +3 -0
  4. package/dist/bundlers/metro.d.ts +5 -0
  5. package/dist/bundlers/metro.d.ts.map +1 -0
  6. package/dist/bundlers/metro.js +61 -0
  7. package/dist/bundlers/webpack.d.ts +2 -0
  8. package/dist/bundlers/webpack.d.ts.map +1 -0
  9. package/dist/bundlers/webpack.js +49 -0
  10. package/dist/commands/test.d.ts +2 -0
  11. package/dist/commands/test.d.ts.map +1 -0
  12. package/dist/commands/test.js +111 -0
  13. package/dist/errors/appNotInstalledError.d.ts +7 -0
  14. package/dist/errors/appNotInstalledError.d.ts.map +1 -0
  15. package/dist/errors/appNotInstalledError.js +12 -0
  16. package/dist/errors/bridgeTimeoutError.d.ts +7 -0
  17. package/dist/errors/bridgeTimeoutError.d.ts.map +1 -0
  18. package/dist/errors/bridgeTimeoutError.js +12 -0
  19. package/dist/errors/errorHandler.d.ts +2 -0
  20. package/dist/errors/errorHandler.d.ts.map +1 -0
  21. package/dist/errors/errorHandler.js +138 -0
  22. package/dist/errors/errors.d.ts +41 -0
  23. package/dist/errors/errors.d.ts.map +1 -0
  24. package/dist/errors/errors.js +83 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +37 -0
  28. package/dist/platforms/android/build.d.ts +5 -0
  29. package/dist/platforms/android/build.d.ts.map +1 -0
  30. package/dist/platforms/android/build.js +29 -0
  31. package/dist/platforms/android/device.d.ts +5 -0
  32. package/dist/platforms/android/device.d.ts.map +1 -0
  33. package/dist/platforms/android/device.js +36 -0
  34. package/dist/platforms/android/emulator.d.ts +11 -0
  35. package/dist/platforms/android/emulator.d.ts.map +1 -0
  36. package/dist/platforms/android/emulator.js +110 -0
  37. package/dist/platforms/android/index.d.ts +4 -0
  38. package/dist/platforms/android/index.d.ts.map +1 -0
  39. package/dist/platforms/android/index.js +56 -0
  40. package/dist/platforms/ios/build.d.ts +7 -0
  41. package/dist/platforms/ios/build.d.ts.map +1 -0
  42. package/dist/platforms/ios/build.js +44 -0
  43. package/dist/platforms/ios/device.d.ts +7 -0
  44. package/dist/platforms/ios/device.d.ts.map +1 -0
  45. package/dist/platforms/ios/device.js +51 -0
  46. package/dist/platforms/ios/index.d.ts +4 -0
  47. package/dist/platforms/ios/index.d.ts.map +1 -0
  48. package/dist/platforms/ios/index.js +43 -0
  49. package/dist/platforms/ios/simulator.d.ts +11 -0
  50. package/dist/platforms/ios/simulator.d.ts.map +1 -0
  51. package/dist/platforms/ios/simulator.js +124 -0
  52. package/dist/platforms/platform-adapter.d.ts +10 -0
  53. package/dist/platforms/platform-adapter.d.ts.map +1 -0
  54. package/dist/platforms/platform-adapter.js +1 -0
  55. package/dist/platforms/platform-registry.d.ts +3 -0
  56. package/dist/platforms/platform-registry.d.ts.map +1 -0
  57. package/dist/platforms/platform-registry.js +19 -0
  58. package/dist/platforms/web/index.d.ts +4 -0
  59. package/dist/platforms/web/index.d.ts.map +1 -0
  60. package/dist/platforms/web/index.js +9 -0
  61. package/dist/process.d.ts +3 -0
  62. package/dist/process.d.ts.map +1 -0
  63. package/dist/process.js +28 -0
  64. package/dist/reporters/default-reporter.d.ts +3 -0
  65. package/dist/reporters/default-reporter.d.ts.map +1 -0
  66. package/dist/reporters/default-reporter.js +116 -0
  67. package/dist/reporters/junit-reporter.d.ts +3 -0
  68. package/dist/reporters/junit-reporter.d.ts.map +1 -0
  69. package/dist/reporters/junit-reporter.js +119 -0
  70. package/dist/reporters/live-reporter.d.ts +20 -0
  71. package/dist/reporters/live-reporter.d.ts.map +1 -0
  72. package/dist/reporters/live-reporter.js +176 -0
  73. package/dist/src/reporters/default-reporter.js +135 -0
  74. package/dist/test-reporter-demo.js +95 -0
  75. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  76. package/dist/utils.d.ts +5 -0
  77. package/dist/utils.d.ts.map +1 -0
  78. package/dist/utils.js +11 -0
  79. package/eslint.config.mjs +19 -0
  80. package/package.json +16 -8
  81. package/src/bundlers/metro.ts +89 -0
  82. package/src/commands/test.ts +189 -0
  83. package/src/errors/errorHandler.ts +184 -0
  84. package/src/errors/errors.ts +109 -0
  85. package/src/index.ts +52 -0
  86. package/src/platforms/android/build.ts +48 -0
  87. package/src/platforms/android/device.ts +48 -0
  88. package/src/platforms/android/emulator.ts +139 -0
  89. package/src/platforms/android/index.ts +87 -0
  90. package/src/platforms/ios/build.ts +68 -0
  91. package/src/platforms/ios/device.ts +76 -0
  92. package/src/platforms/ios/index.ts +66 -0
  93. package/src/platforms/ios/simulator.ts +166 -0
  94. package/src/platforms/platform-adapter.ts +11 -0
  95. package/src/platforms/platform-registry.ts +24 -0
  96. package/src/platforms/web/index.ts +16 -0
  97. package/src/process.ts +33 -0
  98. package/src/reporters/default-reporter.ts +149 -0
  99. package/src/reporters/junit-reporter.ts +179 -0
  100. package/src/utils.ts +12 -0
  101. package/tsconfig.json +16 -0
  102. package/tsconfig.lib.json +33 -0
  103. package/tsconfig.tsbuildinfo +1 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Callstack
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # cli
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build cli` to build the library.
package/bin.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('./dist/index.js');
@@ -0,0 +1,5 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ export declare const runMetro: (isExpo?: boolean) => Promise<ChildProcess>;
3
+ export declare const waitForMetro: (port?: number, maxRetries?: number, retryDelay?: number) => Promise<void>;
4
+ export declare const reloadApp: () => Promise<void>;
5
+ //# sourceMappingURL=metro.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metro.d.ts","sourceRoot":"","sources":["../../src/bundlers/metro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWvD,eAAO,MAAM,QAAQ,GAAU,gBAAc,KAAG,OAAO,CAAC,YAAY,CAuCnE,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,aAAW,EACX,mBAAe,EACf,mBAAiB,KAChB,OAAO,CAAC,IAAI,CA4Bd,CAAC;AAEF,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,IAAI,CAE9C,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { getReactNativeCliPath, getExpoCliPath, getTimeoutSignal, spawn, SubprocessError, } from '@react-native-harness/tools';
2
+ const METRO_PORT = 8081;
3
+ export const runMetro = async (isExpo = false) => {
4
+ const metro = spawn('node', [
5
+ isExpo ? getExpoCliPath() : getReactNativeCliPath(),
6
+ 'start',
7
+ '--port',
8
+ METRO_PORT.toString(),
9
+ ], {
10
+ env: {
11
+ ...process.env,
12
+ RN_HARNESS: 'true',
13
+ ...(isExpo && { EXPO_NO_METRO_WORKSPACE_ROOT: 'true' }),
14
+ },
15
+ });
16
+ // Forward metro output to CLI output
17
+ metro.nodeChildProcess.then((childProcess) => {
18
+ if (childProcess.stdout) {
19
+ childProcess.stdout.pipe(process.stdout);
20
+ }
21
+ if (childProcess.stderr) {
22
+ childProcess.stderr.pipe(process.stderr);
23
+ }
24
+ });
25
+ metro.catch((error) => {
26
+ // This process is going to be killed by us, so we don't need to throw an error
27
+ if (error instanceof SubprocessError && error.signalName === 'SIGTERM') {
28
+ return;
29
+ }
30
+ throw error;
31
+ });
32
+ await waitForMetro();
33
+ return metro.nodeChildProcess;
34
+ };
35
+ export const waitForMetro = async (port = 8081, maxRetries = 20, retryDelay = 1000) => {
36
+ let attempts = 0;
37
+ while (attempts < maxRetries) {
38
+ attempts++;
39
+ try {
40
+ const response = await fetch(`http://localhost:${port}/status`, {
41
+ signal: getTimeoutSignal(100),
42
+ });
43
+ if (response.ok) {
44
+ const body = await response.text();
45
+ if (body === 'packager-status:running') {
46
+ return;
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ // Errors are expected here, we're just waiting for the process to be ready
52
+ }
53
+ if (attempts < maxRetries) {
54
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
55
+ }
56
+ }
57
+ throw new Error(`Metro bundler is not ready after ${maxRetries} attempts`);
58
+ };
59
+ export const reloadApp = async () => {
60
+ await fetch(`http://localhost:${METRO_PORT}/reload`);
61
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webpack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webpack.d.ts","sourceRoot":"","sources":["../../src/bundlers/webpack.ts"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ // import { ChildProcess, spawn } from 'node:child_process';
2
+ export {};
3
+ // export const runWebpack = async (configPath: string): Promise<ChildProcess> => {
4
+ // const webpack = await new Promise<ChildProcess>((resolve) => {
5
+ // const webpackProcess = spawn(
6
+ // 'webpack',
7
+ // [
8
+ // 'serve',
9
+ // '--config',
10
+ // configPath,
11
+ // '--mode',
12
+ // 'development',
13
+ // '--hot',
14
+ // '--port',
15
+ // '8081',
16
+ // ],
17
+ // {
18
+ // stdio: 'ignore',
19
+ // env: {
20
+ // ...process.env,
21
+ // RN_HARNESS: 'true',
22
+ // },
23
+ // }
24
+ // );
25
+ // resolve(webpackProcess);
26
+ // });
27
+ // await waitForWebpack(8081);
28
+ // return webpack;
29
+ // };
30
+ // export const waitForWebpack = async (
31
+ // port: number = 8081,
32
+ // maxRetries: number = 10,
33
+ // retryDelay: number = 2000
34
+ // ): Promise<void> => {
35
+ // let attempts = 0;
36
+ // while (attempts < maxRetries) {
37
+ // attempts++;
38
+ // try {
39
+ // const response = await fetch(`http://localhost:${port}`);
40
+ // if (response.ok) {
41
+ // return;
42
+ // }
43
+ // } catch {}
44
+ // if (attempts < maxRetries) {
45
+ // await new Promise((resolve) => setTimeout(resolve, retryDelay));
46
+ // }
47
+ // }
48
+ // throw new Error(`Metro bundler is not ready after ${maxRetries} attempts`);
49
+ // };
@@ -0,0 +1,2 @@
1
+ export declare const testCommand: (runnerName?: string, pattern?: string) => Promise<void>;
2
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/commands/test.ts"],"names":[],"mappings":"AAqJA,eAAO,MAAM,WAAW,GACtB,aAAa,MAAM,EACnB,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAoCd,CAAC"}
@@ -0,0 +1,111 @@
1
+ import { getBridgeServer, } from '@react-native-harness/bridge/server';
2
+ import { getConfig, } from '@react-native-harness/config';
3
+ import { getPlatformAdapter } from '../platforms/platform-registry.js';
4
+ import { Glob } from 'glob';
5
+ import { defaultReporter } from '../reporters/default-reporter.js';
6
+ import { intro, logger, outro, spinner, progress, } from '@react-native-harness/tools';
7
+ import { BridgeTimeoutError } from '../errors/errors.js';
8
+ import { assert } from '../utils.js';
9
+ import { EnvironmentInitializationError, NoRunnerSpecifiedError, RpcClientError, RunnerNotFoundError, } from '../errors/errors.js';
10
+ const setupEnvironment = async (context) => {
11
+ const startSpinner = spinner();
12
+ const platform = context.runner.platform;
13
+ startSpinner.start(`Starting "${context.runner.name}" (${platform}) runner`);
14
+ const platformAdapter = await getPlatformAdapter(platform);
15
+ const serverBridge = await getBridgeServer({
16
+ port: 3001,
17
+ });
18
+ context.bridge = serverBridge;
19
+ const readyPromise = new Promise((resolve, reject) => {
20
+ const timeout = setTimeout(() => {
21
+ reject(new BridgeTimeoutError(context.config.bridgeTimeout, context.runner.name, platform));
22
+ }, context.config.bridgeTimeout);
23
+ serverBridge.once('ready', () => {
24
+ clearTimeout(timeout);
25
+ resolve();
26
+ });
27
+ });
28
+ context.environment = await platformAdapter.getEnvironment(context.runner);
29
+ logger.debug('Waiting for bridge to be ready');
30
+ await readyPromise;
31
+ logger.debug('Bridge is ready');
32
+ if (!context.environment) {
33
+ throw new EnvironmentInitializationError('Failed to initialize environment', context.runner.name, platform, 'Platform adapter returned null environment');
34
+ }
35
+ startSpinner.stop(`"${context.runner.name}" (${platform}) runner started`);
36
+ };
37
+ const findTestFiles = async (context, pattern) => {
38
+ const discoverSpinner = spinner();
39
+ discoverSpinner.start('Discovering tests');
40
+ const globPattern = pattern || context.config.include;
41
+ const glob = new Glob(globPattern, {
42
+ cwd: process.cwd(),
43
+ });
44
+ context.testFiles = await glob.walk();
45
+ discoverSpinner.stop(`Found ${context.testFiles.length} test files`);
46
+ };
47
+ const runTests = async (context) => {
48
+ const { bridge, environment, testFiles } = context;
49
+ assert(bridge != null, 'Bridge not initialized');
50
+ assert(environment != null, 'Environment not initialized');
51
+ assert(testFiles != null, 'Test files not initialized');
52
+ let runSpinner = progress({ style: 'block' });
53
+ runSpinner.start('Running tests');
54
+ let shouldRestart = false;
55
+ for (const testFile of testFiles) {
56
+ if (shouldRestart) {
57
+ runSpinner = progress({ style: 'block' });
58
+ runSpinner.message(`Restarting environment for next test file`);
59
+ await new Promise((resolve) => {
60
+ bridge.once('ready', resolve);
61
+ environment.restart();
62
+ });
63
+ }
64
+ runSpinner.message(`Running tests in ${testFile}`);
65
+ const client = bridge.rpc.clients.at(-1);
66
+ if (!client) {
67
+ throw new RpcClientError('No RPC client available', 3001, 'No clients connected');
68
+ }
69
+ const result = await client.runTests(testFile);
70
+ context.results = [...(context.results ?? []), ...result.suites];
71
+ shouldRestart = true;
72
+ }
73
+ };
74
+ const cleanUp = async (context) => {
75
+ if (context.bridge) {
76
+ context.bridge.ws.close();
77
+ }
78
+ if (context.environment) {
79
+ await context.environment.dispose();
80
+ }
81
+ };
82
+ export const testCommand = async (runnerName, pattern) => {
83
+ intro('React Native Test Harness');
84
+ const config = await getConfig(process.cwd());
85
+ config.reporter = defaultReporter;
86
+ const selectedRunnerName = runnerName ?? config.defaultRunner;
87
+ if (!selectedRunnerName) {
88
+ throw new NoRunnerSpecifiedError(config.runners);
89
+ }
90
+ const runner = config.runners.find((r) => r.name === selectedRunnerName);
91
+ if (!runner) {
92
+ throw new RunnerNotFoundError(selectedRunnerName, config.runners);
93
+ }
94
+ const context = {
95
+ config,
96
+ runner,
97
+ testFiles: [],
98
+ results: [],
99
+ };
100
+ try {
101
+ await setupEnvironment(context);
102
+ await findTestFiles(context, pattern);
103
+ await runTests(context);
104
+ assert(context.results != null, 'Results not initialized');
105
+ config.reporter?.report(context.results);
106
+ outro('Test run completed successfully');
107
+ }
108
+ finally {
109
+ await cleanUp(context);
110
+ }
111
+ };
@@ -0,0 +1,7 @@
1
+ export declare class AppNotInstalledError extends Error {
2
+ readonly deviceName: string;
3
+ readonly bundleId: string;
4
+ readonly platform: 'ios' | 'android';
5
+ constructor(deviceName: string, bundleId: string, platform: 'ios' | 'android');
6
+ }
7
+ //# sourceMappingURL=appNotInstalledError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appNotInstalledError.d.ts","sourceRoot":"","sources":["../../src/errors/appNotInstalledError.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;aAEvB,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,MAAM;aAChB,QAAQ,EAAE,KAAK,GAAG,SAAS;gBAF3B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,KAAK,GAAG,SAAS;CAKlD"}
@@ -0,0 +1,12 @@
1
+ export class AppNotInstalledError extends Error {
2
+ deviceName;
3
+ bundleId;
4
+ platform;
5
+ constructor(deviceName, bundleId, platform) {
6
+ super(`App "${bundleId}" is not installed on ${platform === 'ios' ? 'simulator' : 'emulator'} "${deviceName}"`);
7
+ this.deviceName = deviceName;
8
+ this.bundleId = bundleId;
9
+ this.platform = platform;
10
+ this.name = 'AppNotInstalledError';
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ export declare class BridgeTimeoutError extends Error {
2
+ readonly timeout: number;
3
+ readonly runnerName: string;
4
+ readonly platform: string;
5
+ constructor(timeout: number, runnerName: string, platform: string);
6
+ }
7
+ //# sourceMappingURL=bridgeTimeoutError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridgeTimeoutError.d.ts","sourceRoot":"","sources":["../../src/errors/bridgeTimeoutError.ts"],"names":[],"mappings":"AAAA,qBAAa,kBAAmB,SAAQ,KAAK;aAErB,OAAO,EAAE,MAAM;aACf,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,MAAM;gBAFhB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM;CAKvC"}
@@ -0,0 +1,12 @@
1
+ export class BridgeTimeoutError extends Error {
2
+ timeout;
3
+ runnerName;
4
+ platform;
5
+ constructor(timeout, runnerName, platform) {
6
+ super(`Bridge connection timed out after ${timeout}ms while waiting for "${runnerName}" (${platform}) runner to be ready`);
7
+ this.timeout = timeout;
8
+ this.runnerName = runnerName;
9
+ this.platform = platform;
10
+ this.name = 'BridgeTimeoutError';
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ export declare const handleError: (error: unknown) => void;
2
+ //# sourceMappingURL=errorHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/errors/errorHandler.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,IAuK5C,CAAC"}
@@ -0,0 +1,138 @@
1
+ import { ConfigLoadError, ConfigNotFoundError, ConfigValidationError, } from '@react-native-harness/config';
2
+ import { AssertionError } from '../utils.js';
3
+ import { NoRunnerSpecifiedError, RunnerNotFoundError, EnvironmentInitializationError, TestExecutionError, RpcClientError, AppNotInstalledError, BridgeTimeoutError, } from './errors.js';
4
+ export const handleError = (error) => {
5
+ if (error instanceof AssertionError) {
6
+ console.error(`\n❌ Assertion Error`);
7
+ console.error(`\nError: ${error.message}`);
8
+ console.error(`\nPlease check your configuration and try again.`);
9
+ }
10
+ else if (error instanceof ConfigValidationError) {
11
+ console.error(`\n❌ Configuration Error`);
12
+ console.error(`\nFile: ${error.filePath}`);
13
+ console.error(`\nValidation errors:`);
14
+ error.validationErrors.forEach((err) => {
15
+ console.error(` • ${err}`);
16
+ });
17
+ console.error(`\nPlease fix the configuration errors and try again.`);
18
+ }
19
+ else if (error instanceof ConfigNotFoundError) {
20
+ console.error(`\n❌ Configuration Not Found`);
21
+ console.error(`\nCould not find 'rn-harness.config' in '${error.searchPath}' or any parent directories.`);
22
+ console.error(`\nSupported file extensions: .js, .mjs, .cjs, .json`);
23
+ console.error(`\nPlease create a configuration file or run from a directory that contains one.`);
24
+ }
25
+ else if (error instanceof ConfigLoadError) {
26
+ console.error(`\n❌ Configuration Load Error`);
27
+ console.error(`\nFile: ${error.filePath}`);
28
+ console.error(`Error: ${error.message}`);
29
+ if (error.cause) {
30
+ console.error(`\nCause: ${error.cause.message}`);
31
+ }
32
+ console.error(`\nPlease check your configuration file syntax and try again.`);
33
+ }
34
+ else if (error instanceof NoRunnerSpecifiedError) {
35
+ console.error('\n❌ No runner specified');
36
+ console.error('\nPlease specify a runner name or set a defaultRunner in your config.');
37
+ console.error('\nUsage: react-native-harness test [runner-name] [pattern]');
38
+ console.error('\nAvailable runners:');
39
+ error.availableRunners.forEach((r) => {
40
+ console.error(` • ${r.name} (${r.platform})`);
41
+ });
42
+ console.error('\nTo set a default runner, add "defaultRunner" to your config:');
43
+ console.error(' { "defaultRunner": "your-runner-name" }');
44
+ }
45
+ else if (error instanceof RunnerNotFoundError) {
46
+ console.error(`\n❌ Runner "${error.runnerName}" not found`);
47
+ console.error('\nAvailable runners:');
48
+ error.availableRunners.forEach((r) => {
49
+ console.error(` • ${r.name} (${r.platform})`);
50
+ });
51
+ console.error('\nTo add a new runner, update your rn-harness.config file.');
52
+ }
53
+ else if (error instanceof EnvironmentInitializationError) {
54
+ console.error(`\n❌ Environment Initialization Error`);
55
+ console.error(`\nRunner: ${error.runnerName} (${error.platform})`);
56
+ console.error(`\nError: ${error.message}`);
57
+ if (error.details) {
58
+ console.error(`\nDetails: ${error.details}`);
59
+ }
60
+ console.error(`\nTroubleshooting steps:`);
61
+ console.error(` • Verify that ${error.platform} development environment is properly set up`);
62
+ console.error(` • Check that the app is built and ready for testing`);
63
+ console.error(` • Ensure all required dependencies are installed`);
64
+ if (error.platform === 'ios') {
65
+ console.error(` • Verify Xcode and iOS Simulator are working correctly`);
66
+ }
67
+ else if (error.platform === 'android') {
68
+ console.error(` • Verify Android SDK and emulator are working correctly`);
69
+ }
70
+ console.error(`\nPlease check your environment configuration and try again.`);
71
+ }
72
+ else if (error instanceof TestExecutionError) {
73
+ console.error(`\n❌ Test Execution Error`);
74
+ console.error(`\nFile: ${error.testFile}`);
75
+ if (error.testSuite) {
76
+ console.error(`\nSuite: ${error.testSuite}`);
77
+ }
78
+ if (error.testName) {
79
+ console.error(`\nTest: ${error.testName}`);
80
+ }
81
+ console.error(`\nError: ${error.message}`);
82
+ console.error(`\nTroubleshooting steps:`);
83
+ console.error(` • Check the test file syntax and logic`);
84
+ console.error(` • Verify all test dependencies are available`);
85
+ console.error(` • Ensure the app is in the expected state for the test`);
86
+ console.error(` • Check device/emulator logs for additional error details`);
87
+ console.error(`\nPlease check your test file and try again.`);
88
+ }
89
+ else if (error instanceof RpcClientError) {
90
+ console.error(`\n❌ RPC Client Error`);
91
+ console.error(`\nError: ${error.message}`);
92
+ if (error.bridgePort) {
93
+ console.error(`\nBridge Port: ${error.bridgePort}`);
94
+ }
95
+ if (error.connectionStatus) {
96
+ console.error(`\nConnection Status: ${error.connectionStatus}`);
97
+ }
98
+ console.error(`\nTroubleshooting steps:`);
99
+ console.error(` • Verify the React Native app is running and connected`);
100
+ console.error(` • Check that the bridge port is not blocked by firewall`);
101
+ console.error(` • Ensure the app has the React Native Harness runtime integrated`);
102
+ console.error(` • Try restarting the app and test harness`);
103
+ console.error(`\nPlease check your bridge connection and try again.`);
104
+ }
105
+ else if (error instanceof AppNotInstalledError) {
106
+ console.error(`\n❌ App Not Installed`);
107
+ console.error(`\nThe app "${error.bundleId}" is not installed on ${error.platform === 'ios' ? 'simulator' : 'emulator'} "${error.deviceName}".`);
108
+ console.error(`\nTo resolve this issue:`);
109
+ if (error.platform === 'ios') {
110
+ console.error(` • Build and install the app: npx react-native run-ios --simulator="${error.deviceName}"`);
111
+ console.error(` • Or install from Xcode: Open ios/*.xcworkspace and run the project`);
112
+ }
113
+ else {
114
+ console.error(` • Build and install the app: npx react-native run-android`);
115
+ console.error(` • Or build manually: ./gradlew assembleDebug && adb install android/app/build/outputs/apk/debug/app-debug.apk`);
116
+ }
117
+ console.error(`\nPlease install the app and try running the tests again.`);
118
+ }
119
+ else if (error instanceof BridgeTimeoutError) {
120
+ console.error(`\n❌ Bridge Connection Timeout`);
121
+ console.error(`\nThe bridge connection timed out after ${error.timeout}ms while waiting for the "${error.runnerName}" (${error.platform}) runner to be ready.`);
122
+ console.error(`\nThis usually indicates that:`);
123
+ console.error(` • The React Native app failed to load or connect to the bridge`);
124
+ console.error(` • The app crashed during startup`);
125
+ console.error(` • Network connectivity issues between the app and the test harness`);
126
+ console.error(` • The app is taking longer than expected to initialize`);
127
+ console.error(`\nTo resolve this issue:`);
128
+ console.error(` • Check that the app is properly installed and can start normally`);
129
+ console.error(` • Verify that the app has the React Native Harness runtime integrated`);
130
+ console.error(` • Check device/emulator logs for any startup errors`);
131
+ console.error(` • Ensure the test harness bridge port (3001) is not blocked`);
132
+ console.error(`\nIf the app needs more time to start, consider increasing the timeout in the configuration.`);
133
+ }
134
+ else {
135
+ console.error(`\n❌ Unexpected Error`);
136
+ console.error(error);
137
+ }
138
+ };
@@ -0,0 +1,41 @@
1
+ import { TestRunnerConfig } from '@react-native-harness/config';
2
+ export declare class NoRunnerSpecifiedError extends Error {
3
+ constructor(availableRunners: TestRunnerConfig[]);
4
+ availableRunners: TestRunnerConfig[];
5
+ }
6
+ export declare class RunnerNotFoundError extends Error {
7
+ constructor(runnerName: string, availableRunners: TestRunnerConfig[]);
8
+ runnerName: string;
9
+ availableRunners: TestRunnerConfig[];
10
+ }
11
+ export declare class EnvironmentInitializationError extends Error {
12
+ constructor(message: string, runnerName: string, platform: string, details?: string);
13
+ runnerName: string;
14
+ platform: string;
15
+ details?: string;
16
+ }
17
+ export declare class TestExecutionError extends Error {
18
+ constructor(testFile: string, error: unknown, testSuite?: string, testName?: string);
19
+ testFile: string;
20
+ testSuite?: string;
21
+ testName?: string;
22
+ originalError: unknown;
23
+ }
24
+ export declare class RpcClientError extends Error {
25
+ constructor(message: string, bridgePort?: number, connectionStatus?: string);
26
+ bridgePort?: number;
27
+ connectionStatus?: string;
28
+ }
29
+ export declare class BridgeTimeoutError extends Error {
30
+ readonly timeout: number;
31
+ readonly runnerName: string;
32
+ readonly platform: string;
33
+ constructor(timeout: number, runnerName: string, platform: string);
34
+ }
35
+ export declare class AppNotInstalledError extends Error {
36
+ readonly deviceName: string;
37
+ readonly bundleId: string;
38
+ readonly platform: 'ios' | 'android';
39
+ constructor(deviceName: string, bundleId: string, platform: 'ios' | 'android');
40
+ }
41
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,gBAAgB,EAAE,gBAAgB,EAAE;IAKhD,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;CACtC;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,gBAAgB,EAAE;IAMpE,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;CACtC;AAED,qBAAa,8BAA+B,SAAQ,KAAK;gBAErD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM;IAUlB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAEzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM;IAenB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM;IAS3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,OAAO,EAAE,MAAM;aACf,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,MAAM;gBAFhB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM;CAOnC;AAED,qBAAa,oBAAqB,SAAQ,KAAK;aAE3B,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,MAAM;aAChB,QAAQ,EAAE,KAAK,GAAG,SAAS;gBAF3B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,KAAK,GAAG,SAAS;CAS9C"}
@@ -0,0 +1,83 @@
1
+ export class NoRunnerSpecifiedError extends Error {
2
+ constructor(availableRunners) {
3
+ super('No runner specified');
4
+ this.name = 'NoRunnerSpecifiedError';
5
+ this.availableRunners = availableRunners;
6
+ }
7
+ availableRunners;
8
+ }
9
+ export class RunnerNotFoundError extends Error {
10
+ constructor(runnerName, availableRunners) {
11
+ super(`Runner "${runnerName}" not found`);
12
+ this.name = 'RunnerNotFoundError';
13
+ this.runnerName = runnerName;
14
+ this.availableRunners = availableRunners;
15
+ }
16
+ runnerName;
17
+ availableRunners;
18
+ }
19
+ export class EnvironmentInitializationError extends Error {
20
+ constructor(message, runnerName, platform, details) {
21
+ super(`Failed to initialize environment for "${runnerName}" (${platform}): ${message}`);
22
+ this.name = 'EnvironmentInitializationError';
23
+ this.runnerName = runnerName;
24
+ this.platform = platform;
25
+ this.details = details;
26
+ }
27
+ runnerName;
28
+ platform;
29
+ details;
30
+ }
31
+ export class TestExecutionError extends Error {
32
+ constructor(testFile, error, testSuite, testName) {
33
+ const errorMessage = error instanceof Error ? error.message : String(error);
34
+ const suiteInfo = testSuite ? ` in suite "${testSuite}"` : '';
35
+ const testInfo = testName ? ` test "${testName}"` : '';
36
+ super(`Test execution failed for ${testFile}${suiteInfo}${testInfo}: ${errorMessage}`);
37
+ this.name = 'TestExecutionError';
38
+ this.testFile = testFile;
39
+ this.testSuite = testSuite;
40
+ this.testName = testName;
41
+ this.originalError = error;
42
+ }
43
+ testFile;
44
+ testSuite;
45
+ testName;
46
+ originalError;
47
+ }
48
+ export class RpcClientError extends Error {
49
+ constructor(message, bridgePort, connectionStatus) {
50
+ const portInfo = bridgePort ? ` (port ${bridgePort})` : '';
51
+ const statusInfo = connectionStatus ? ` - Status: ${connectionStatus}` : '';
52
+ super(`RPC client error${portInfo}: ${message}${statusInfo}`);
53
+ this.name = 'RpcClientError';
54
+ this.bridgePort = bridgePort;
55
+ this.connectionStatus = connectionStatus;
56
+ }
57
+ bridgePort;
58
+ connectionStatus;
59
+ }
60
+ export class BridgeTimeoutError extends Error {
61
+ timeout;
62
+ runnerName;
63
+ platform;
64
+ constructor(timeout, runnerName, platform) {
65
+ super(`Bridge connection timed out after ${timeout}ms while waiting for "${runnerName}" (${platform}) runner to be ready`);
66
+ this.timeout = timeout;
67
+ this.runnerName = runnerName;
68
+ this.platform = platform;
69
+ this.name = 'BridgeTimeoutError';
70
+ }
71
+ }
72
+ export class AppNotInstalledError extends Error {
73
+ deviceName;
74
+ bundleId;
75
+ platform;
76
+ constructor(deviceName, bundleId, platform) {
77
+ super(`App "${bundleId}" is not installed on ${platform === 'ios' ? 'simulator' : 'emulator'} "${deviceName}"`);
78
+ this.deviceName = deviceName;
79
+ this.bundleId = bundleId;
80
+ this.platform = platform;
81
+ this.name = 'AppNotInstalledError';
82
+ }
83
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readFileSync } from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+ import { testCommand } from './commands/test.js';
7
+ import { handleError } from './errors/errorHandler.js';
8
+ import { logger } from '@react-native-harness/tools';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const packageJsonPath = join(__dirname, '../package.json');
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
13
+ const program = new Command();
14
+ logger.setVerbose(true);
15
+ program
16
+ .name('react-native-harness')
17
+ .description('React Native Test Harness - A comprehensive testing framework for React Native applications')
18
+ .version(packageJson.version);
19
+ program
20
+ .command('test')
21
+ .description('Run tests using the specified runner')
22
+ .argument('[runner]', 'test runner name (uses defaultRunner from config if not specified)')
23
+ .argument('[pattern]', 'glob pattern to match test files (uses config.include if not specified)')
24
+ .action(async (runner, pattern) => {
25
+ try {
26
+ await testCommand(runner, pattern);
27
+ }
28
+ catch (error) {
29
+ handleError(error);
30
+ process.exit(1);
31
+ }
32
+ });
33
+ process.on('uncaughtException', (error) => {
34
+ handleError(error);
35
+ process.exit(1);
36
+ });
37
+ program.parse();