Skip to content

Commit 3595b82

Browse files
committed
feat: improve error handling
1 parent 0614426 commit 3595b82

9 files changed

Lines changed: 163 additions & 149 deletions

File tree

packages/cli/src/errors/errorHandler.ts

Lines changed: 113 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -16,211 +16,203 @@ import {
1616
MetroPortUnavailableError,
1717
} from './errors.js';
1818

19-
export const handleError = (error: unknown): void => {
19+
export const formatError = (error: unknown): string => {
20+
const lines: string[] = [];
21+
2022
if (error instanceof AssertionError) {
21-
console.error(`\n❌ Assertion Error`);
22-
console.error(`\nError: ${error.message}`);
23-
console.error(`\nPlease check your configuration and try again.`);
23+
lines.push(`\n❌ Assertion Error`);
24+
lines.push(`\nError: ${error.message}`);
25+
lines.push(`\nPlease check your configuration and try again.`);
2426
} else if (error instanceof ConfigValidationError) {
25-
console.error(`\n❌ Configuration Error`);
26-
console.error(`\nFile: ${error.filePath}`);
27-
console.error(`\nValidation errors:`);
27+
lines.push(`\n❌ Configuration Error`);
28+
lines.push(`\nFile: ${error.filePath}`);
29+
lines.push(`\nValidation errors:`);
2830
error.validationErrors.forEach((err) => {
29-
console.error(` • ${err}`);
31+
lines.push(` • ${err}`);
3032
});
31-
console.error(`\nPlease fix the configuration errors and try again.`);
33+
lines.push(`\nPlease fix the configuration errors and try again.`);
3234
} else if (error instanceof ConfigNotFoundError) {
33-
console.error(`\n❌ Configuration Not Found`);
34-
console.error(
35+
lines.push(`\n❌ Configuration Not Found`);
36+
lines.push(
3537
`\nCould not find 'rn-harness.config' in '${error.searchPath}' or any parent directories.`
3638
);
37-
console.error(`\nSupported file extensions: .js, .mjs, .cjs, .json`);
38-
console.error(
39+
lines.push(`\nSupported file extensions: .js, .mjs, .cjs, .json`);
40+
lines.push(
3941
`\nPlease create a configuration file or run from a directory that contains one.`
4042
);
4143
} else if (error instanceof ConfigLoadError) {
42-
console.error(`\n❌ Configuration Load Error`);
43-
console.error(`\nFile: ${error.filePath}`);
44-
console.error(`Error: ${error.message}`);
44+
lines.push(`\n❌ Configuration Load Error`);
45+
lines.push(`\nFile: ${error.filePath}`);
46+
lines.push(`Error: ${error.message}`);
4547
if (error.cause) {
46-
console.error(`\nCause: ${error.cause.message}`);
48+
lines.push(`\nCause: ${error.cause.message}`);
4749
}
48-
console.error(
49-
`\nPlease check your configuration file syntax and try again.`
50-
);
50+
lines.push(`\nPlease check your configuration file syntax and try again.`);
5151
} else if (error instanceof NoRunnerSpecifiedError) {
52-
console.error('\n❌ No runner specified');
53-
console.error(
52+
lines.push('\n❌ No runner specified');
53+
lines.push(
5454
'\nPlease specify a runner name or set a defaultRunner in your config.'
5555
);
56-
console.error('\nUsage: react-native-harness test [runner-name] [pattern]');
57-
console.error('\nAvailable runners:');
56+
lines.push('\nUsage: react-native-harness test [runner-name] [pattern]');
57+
lines.push('\nAvailable runners:');
5858
error.availableRunners.forEach((r) => {
59-
console.error(` • ${r.name} (${r.platform})`);
59+
lines.push(` • ${r.name} (${r.platform})`);
6060
});
61-
console.error(
61+
lines.push(
6262
'\nTo set a default runner, add "defaultRunner" to your config:'
6363
);
64-
console.error(' { "defaultRunner": "your-runner-name" }');
64+
lines.push(' { "defaultRunner": "your-runner-name" }');
6565
} else if (error instanceof RunnerNotFoundError) {
66-
console.error(`\n❌ Runner "${error.runnerName}" not found`);
67-
console.error('\nAvailable runners:');
66+
lines.push(`\n❌ Runner "${error.runnerName}" not found`);
67+
lines.push('\nAvailable runners:');
6868
error.availableRunners.forEach((r) => {
69-
console.error(` • ${r.name} (${r.platform})`);
69+
lines.push(` • ${r.name} (${r.platform})`);
7070
});
71-
console.error('\nTo add a new runner, update your rn-harness.config file.');
71+
lines.push('\nTo add a new runner, update your rn-harness.config file.');
7272
} else if (error instanceof EnvironmentInitializationError) {
73-
console.error(`\n❌ Environment Initialization Error`);
74-
console.error(`\nRunner: ${error.runnerName} (${error.platform})`);
75-
console.error(`\nError: ${error.message}`);
73+
lines.push(`\n❌ Environment Initialization Error`);
74+
lines.push(`\nRunner: ${error.runnerName} (${error.platform})`);
75+
lines.push(`\nError: ${error.message}`);
7676
if (error.details) {
77-
console.error(`\nDetails: ${error.details}`);
77+
lines.push(`\nDetails: ${error.details}`);
7878
}
79-
console.error(`\nTroubleshooting steps:`);
80-
console.error(
79+
lines.push(`\nTroubleshooting steps:`);
80+
lines.push(
8181
` • Verify that ${error.platform} development environment is properly set up`
8282
);
83-
console.error(` • Check that the app is built and ready for testing`);
84-
console.error(` • Ensure all required dependencies are installed`);
83+
lines.push(` • Check that the app is built and ready for testing`);
84+
lines.push(` • Ensure all required dependencies are installed`);
8585
if (error.platform === 'ios') {
86-
console.error(` • Verify Xcode and iOS Simulator are working correctly`);
86+
lines.push(` • Verify Xcode and iOS Simulator are working correctly`);
8787
} else if (error.platform === 'android') {
88-
console.error(
89-
` • Verify Android SDK and emulator are working correctly`
90-
);
88+
lines.push(` • Verify Android SDK and emulator are working correctly`);
9189
}
92-
console.error(
93-
`\nPlease check your environment configuration and try again.`
94-
);
90+
lines.push(`\nPlease check your environment configuration and try again.`);
9591
} else if (error instanceof TestExecutionError) {
96-
console.error(`\n❌ Test Execution Error`);
97-
console.error(`\nFile: ${error.testFile}`);
92+
lines.push(`\n❌ Test Execution Error`);
93+
lines.push(`\nFile: ${error.testFile}`);
9894
if (error.testSuite) {
99-
console.error(`\nSuite: ${error.testSuite}`);
95+
lines.push(`\nSuite: ${error.testSuite}`);
10096
}
10197
if (error.testName) {
102-
console.error(`\nTest: ${error.testName}`);
98+
lines.push(`\nTest: ${error.testName}`);
10399
}
104-
console.error(`\nError: ${error.message}`);
105-
console.error(`\nTroubleshooting steps:`);
106-
console.error(` • Check the test file syntax and logic`);
107-
console.error(` • Verify all test dependencies are available`);
108-
console.error(` • Ensure the app is in the expected state for the test`);
109-
console.error(
110-
` • Check device/emulator logs for additional error details`
111-
);
112-
console.error(`\nPlease check your test file and try again.`);
100+
lines.push(`\nError: ${error.message}`);
101+
lines.push(`\nTroubleshooting steps:`);
102+
lines.push(` • Check the test file syntax and logic`);
103+
lines.push(` • Verify all test dependencies are available`);
104+
lines.push(` • Ensure the app is in the expected state for the test`);
105+
lines.push(` • Check device/emulator logs for additional error details`);
106+
lines.push(`\nPlease check your test file and try again.`);
113107
} else if (error instanceof RpcClientError) {
114-
console.error(`\n❌ RPC Client Error`);
115-
console.error(`\nError: ${error.message}`);
108+
lines.push(`\n❌ RPC Client Error`);
109+
lines.push(`\nError: ${error.message}`);
116110
if (error.bridgePort) {
117-
console.error(`\nBridge Port: ${error.bridgePort}`);
111+
lines.push(`\nBridge Port: ${error.bridgePort}`);
118112
}
119113
if (error.connectionStatus) {
120-
console.error(`\nConnection Status: ${error.connectionStatus}`);
114+
lines.push(`\nConnection Status: ${error.connectionStatus}`);
121115
}
122-
console.error(`\nTroubleshooting steps:`);
123-
console.error(` • Verify the React Native app is running and connected`);
124-
console.error(` • Check that the bridge port is not blocked by firewall`);
125-
console.error(
116+
lines.push(`\nTroubleshooting steps:`);
117+
lines.push(` • Verify the React Native app is running and connected`);
118+
lines.push(` • Check that the bridge port is not blocked by firewall`);
119+
lines.push(
126120
` • Ensure the app has the React Native Harness runtime integrated`
127121
);
128-
console.error(` • Try restarting the app and test harness`);
129-
console.error(`\nPlease check your bridge connection and try again.`);
122+
lines.push(` • Try restarting the app and test harness`);
123+
lines.push(`\nPlease check your bridge connection and try again.`);
130124
} else if (error instanceof AppNotInstalledError) {
131-
console.error(`\n❌ App Not Installed`);
125+
lines.push(`\n❌ App Not Installed`);
132126
const deviceType =
133127
error.platform === 'ios'
134128
? 'simulator'
135129
: error.platform === 'android'
136130
? 'emulator'
137131
: 'virtual device';
138-
console.error(
132+
lines.push(
139133
`\nThe app "${error.bundleId}" is not installed on ${deviceType} "${error.deviceName}".`
140134
);
141-
console.error(`\nTo resolve this issue:`);
135+
lines.push(`\nTo resolve this issue:`);
142136
if (error.platform === 'ios') {
143-
console.error(
137+
lines.push(
144138
` • Build and install the app: npx react-native run-ios --simulator="${error.deviceName}"`
145139
);
146-
console.error(
140+
lines.push(
147141
` • Or install from Xcode: Open ios/*.xcworkspace and run the project`
148142
);
149143
} else if (error.platform === 'android') {
150-
console.error(
151-
` • Build and install the app: npx react-native run-android`
152-
);
153-
console.error(
144+
lines.push(` • Build and install the app: npx react-native run-android`);
145+
lines.push(
154146
` • Or build manually: ./gradlew assembleDebug && adb install android/app/build/outputs/apk/debug/app-debug.apk`
155147
);
156148
} else if (error.platform === 'vega') {
157-
console.error(` • Build the Vega app: npm run build:app`);
158-
console.error(
149+
lines.push(` • Build the Vega app: npm run build:app`);
150+
lines.push(
159151
` • Install the app: kepler device install-app -p <path-to-vpkg> --device "${error.deviceName}"`
160152
);
161-
console.error(
153+
lines.push(
162154
` • Or use the combined command: kepler run-kepler <path-to-vpkg> "${error.bundleId}" -d "${error.deviceName}"`
163155
);
164156
}
165-
console.error(`\nPlease install the app and try running the tests again.`);
157+
lines.push(`\nPlease install the app and try running the tests again.`);
166158
} else if (error instanceof BundlingFailedError) {
167-
console.error(`\n❌ Test File Bundling Error`);
168-
console.error(`\nFile: ${error.modulePath}`);
169-
console.error(`\nError: ${error.reason}`);
170-
console.error(`\nTroubleshooting steps:`);
171-
console.error(` • Check the test file syntax and imports`);
172-
console.error(` • Verify all imported modules exist and are accessible`);
173-
console.error(` • Ensure the Metro bundler configuration is correct`);
174-
console.error(` • Check for any circular dependencies in the test file`);
175-
console.error(` • Verify that all required packages are installed`);
176-
console.error(`\nPlease fix the bundling issues and try again.`);
159+
lines.push(`\n❌ Test File Bundling Error`);
160+
lines.push(`\nFile: ${error.modulePath}`);
161+
lines.push(`\nError: ${error.reason}`);
162+
lines.push(`\nTroubleshooting steps:`);
163+
lines.push(` • Check the test file syntax and imports`);
164+
lines.push(` • Verify all imported modules exist and are accessible`);
165+
lines.push(` • Ensure the Metro bundler configuration is correct`);
166+
lines.push(` • Check for any circular dependencies in the test file`);
167+
lines.push(` • Verify that all required packages are installed`);
168+
lines.push(`\nPlease fix the bundling issues and try again.`);
177169
} else if (error instanceof BridgeTimeoutError) {
178-
console.error(`\n❌ Bridge Connection Timeout`);
179-
console.error(
170+
lines.push(`\n❌ Bridge Connection Timeout`);
171+
lines.push(
180172
`\nThe bridge connection timed out after ${error.timeout}ms while waiting for the "${error.runnerName}" (${error.platform}) runner to be ready.`
181173
);
182-
console.error(`\nThis usually indicates that:`);
183-
console.error(
174+
lines.push(`\nThis usually indicates that:`);
175+
lines.push(
184176
` • The React Native app failed to load or connect to the bridge`
185177
);
186-
console.error(` • The app crashed during startup`);
187-
console.error(
178+
lines.push(` • The app crashed during startup`);
179+
lines.push(
188180
` • Network connectivity issues between the app and the test harness`
189181
);
190-
console.error(` • The app is taking longer than expected to initialize`);
191-
console.error(`\nTo resolve this issue:`);
192-
console.error(
182+
lines.push(` • The app is taking longer than expected to initialize`);
183+
lines.push(`\nTo resolve this issue:`);
184+
lines.push(
193185
` • Check that the app is properly installed and can start normally`
194186
);
195-
console.error(
187+
lines.push(
196188
` • Verify that the app has the React Native Harness runtime integrated`
197189
);
198-
console.error(` • Check device/emulator logs for any startup errors`);
199-
console.error(
200-
` • Ensure the test harness bridge port (3001) is not blocked`
201-
);
202-
console.error(
190+
lines.push(` • Check device/emulator logs for any startup errors`);
191+
lines.push(` • Ensure the test harness bridge port (3001) is not blocked`);
192+
lines.push(
203193
`\nIf the app needs more time to start, consider increasing the timeout in the configuration.`
204194
);
205195
} else if (error instanceof MetroPortUnavailableError) {
206-
console.error(`\n❌ Metro Port Unavailable`);
207-
console.error(`\nPort ${error.port} is already in use or unavailable.`);
208-
console.error(`\nThis usually indicates that:`);
209-
console.error(` • Another Metro bundler instance is already running`);
210-
console.error(` • Another application is using port ${error.port}`);
211-
console.error(` • The port is blocked by a firewall or security software`);
212-
console.error(`\nTo resolve this issue:`);
213-
console.error(` • Stop any running Metro bundler instances`);
214-
console.error(
196+
lines.push(`\n❌ Metro Port Unavailable`);
197+
lines.push(`\nPort ${error.port} is already in use or unavailable.`);
198+
lines.push(`\nThis usually indicates that:`);
199+
lines.push(` • Another Metro bundler instance is already running`);
200+
lines.push(` • Another application is using port ${error.port}`);
201+
lines.push(` • The port is blocked by a firewall or security software`);
202+
lines.push(`\nTo resolve this issue:`);
203+
lines.push(` • Stop any running Metro bundler instances`);
204+
lines.push(
215205
` • Check for other applications using port ${error.port}: lsof -i :${error.port}`
216206
);
217-
console.error(` • Kill the process using the port: kill -9 <PID>`);
218-
console.error(
207+
lines.push(` • Kill the process using the port: kill -9 <PID>`);
208+
lines.push(
219209
` • Or use a different port by updating your Metro configuration`
220210
);
221-
console.error(`\nPlease free up the port and try again.`);
211+
lines.push(`\nPlease free up the port and try again.`);
222212
} else {
223-
console.error(`\n❌ Unexpected Error`);
224-
console.error(error);
213+
// Re-throw the error to be handled by the caller
214+
throw error;
225215
}
216+
217+
return lines.join('');
226218
};

packages/cli/src/external.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ export const getHarness = async (
4242
bridge: serverBridge,
4343
};
4444
};
45+
46+
export { formatError } from './errors/errorHandler.js';
47+
export * from './errors/errors.js';

packages/jest/src/cli-args.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => {
1010
.option('harnessRunner', {
1111
type: 'string',
1212
description: 'Specify which Harness runner to use',
13-
coerce: (value: string) => {
14-
if (!value || value.trim().length === 0) {
15-
throw new Error('harnessRunner must be a non-empty string');
16-
}
17-
return value.trim();
18-
},
1913
})
2014
.strict(false)
2115
.help(false)

packages/jest/src/index.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import type {
1111
import pLimit from 'p-limit';
1212
import { runHarnessTestFile } from './run.js';
1313
import { Config as HarnessConfig } from '@react-native-harness/config';
14-
import type { Harness } from '@react-native-harness/cli/external';
14+
import { formatError, type Harness } from '@react-native-harness/cli/external';
15+
import { setup } from './setup.js';
16+
import { teardown } from './teardown.js';
1517

1618
class CancelRun extends Error {
1719
constructor(message?: string) {
@@ -41,18 +43,29 @@ export default class JestHarness implements CallbackTestRunnerInterface {
4143
throw new Error('Parallel test running is not supported');
4244
}
4345

44-
const harness = global.HARNESS;
45-
const harnessConfig = global.HARNESS_CONFIG;
46+
try {
47+
// This is necessary as Harness may throw and we want to catch it and display a helpful error message.
48+
await setup(this.#globalConfig);
4649

47-
return await this._createInBandTestRun(
48-
tests,
49-
watcher,
50-
harness,
51-
harnessConfig,
52-
onStart,
53-
onResult,
54-
onFailure
55-
);
50+
const harness = global.HARNESS;
51+
const harnessConfig = global.HARNESS_CONFIG;
52+
53+
return await this._createInBandTestRun(
54+
tests,
55+
watcher,
56+
harness,
57+
harnessConfig,
58+
onStart,
59+
onResult,
60+
onFailure
61+
);
62+
} catch (error) {
63+
// Jest will print strings as they are, without processing them further.
64+
throw formatError(error);
65+
} finally {
66+
// This is necessary as Harness may throw and we want to catch it and display a helpful error message.
67+
await teardown(this.#globalConfig);
68+
}
5669
}
5770

5871
async _createInBandTestRun(

0 commit comments

Comments
 (0)