@react-native-harness/cli 1.0.0-alpha.9 → 1.0.0-canary.1761729829908
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/README.md +23 -4
- package/dist/bundlers/metro.d.ts.map +1 -1
- package/dist/bundlers/metro.js +15 -5
- package/dist/commands/test.d.ts +2 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +23 -19
- package/dist/discovery/index.d.ts +3 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +1 -0
- package/dist/discovery/testDiscovery.d.ts +11 -0
- package/dist/discovery/testDiscovery.d.ts.map +1 -0
- package/dist/discovery/testDiscovery.js +29 -0
- package/dist/errors/errorHandler.d.ts +1 -1
- package/dist/errors/errorHandler.d.ts.map +1 -1
- package/dist/errors/errorHandler.js +103 -101
- package/dist/errors/errors.d.ts +6 -7
- package/dist/errors/errors.d.ts.map +1 -1
- package/dist/errors/errors.js +8 -12
- package/dist/external.d.ts +2 -0
- package/dist/external.d.ts.map +1 -0
- package/dist/external.js +1 -0
- package/dist/index.js +58 -33
- package/dist/jest.d.ts +2 -0
- package/dist/jest.d.ts.map +1 -0
- package/dist/jest.js +7 -0
- package/dist/platforms/android/emulator.d.ts +0 -1
- package/dist/platforms/android/emulator.d.ts.map +1 -1
- package/dist/platforms/android/emulator.js +26 -20
- package/dist/platforms/android/index.js +1 -1
- package/dist/platforms/ios/build.d.ts.map +1 -1
- package/dist/platforms/ios/build.js +5 -1
- package/dist/platforms/ios/device.d.ts +6 -2
- package/dist/platforms/ios/device.d.ts.map +1 -1
- package/dist/platforms/ios/simulator.d.ts.map +1 -1
- package/dist/platforms/ios/simulator.js +8 -3
- package/dist/platforms/platform-registry.d.ts.map +1 -1
- package/dist/platforms/platform-registry.js +3 -1
- package/dist/platforms/vega/build.d.ts +23 -0
- package/dist/platforms/vega/build.d.ts.map +1 -0
- package/dist/platforms/vega/build.js +55 -0
- package/dist/platforms/vega/device.d.ts +57 -0
- package/dist/platforms/vega/device.d.ts.map +1 -0
- package/dist/platforms/vega/device.js +206 -0
- package/dist/platforms/vega/index.d.ts +4 -0
- package/dist/platforms/vega/index.d.ts.map +1 -0
- package/dist/platforms/vega/index.js +75 -0
- package/dist/process.js +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/utils/status-formatter.d.ts +27 -0
- package/dist/utils/status-formatter.d.ts.map +1 -0
- package/dist/utils/status-formatter.js +54 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +15 -0
- package/eslint.config.mjs +1 -0
- package/package.json +25 -9
- package/src/external.ts +0 -0
- package/src/index.ts +65 -44
- package/tsconfig.lib.json +1 -12
- package/src/bundlers/metro.ts +0 -89
- package/src/commands/test.ts +0 -218
- package/src/errors/errorHandler.ts +0 -196
- package/src/errors/errors.ts +0 -119
- package/src/platforms/android/build.ts +0 -49
- package/src/platforms/android/device.ts +0 -48
- package/src/platforms/android/emulator.ts +0 -139
- package/src/platforms/android/index.ts +0 -87
- package/src/platforms/ios/build.ts +0 -68
- package/src/platforms/ios/device.ts +0 -76
- package/src/platforms/ios/index.ts +0 -66
- package/src/platforms/ios/simulator.ts +0 -166
- package/src/platforms/platform-adapter.ts +0 -11
- package/src/platforms/platform-registry.ts +0 -24
- package/src/platforms/web/index.ts +0 -16
- package/src/process.ts +0 -33
- package/src/utils.ts +0 -12
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BridgeEvents } from '@react-native-harness/bridge';
|
|
2
|
+
/**
|
|
3
|
+
* Type-safe status line formatter for bridge events.
|
|
4
|
+
* This ensures every event type has a corresponding status message.
|
|
5
|
+
*
|
|
6
|
+
* When adding new events to BridgeEvents:
|
|
7
|
+
* 1. Add the event type to the appropriate events file in packages/bridge/src/shared/
|
|
8
|
+
* 2. Add a formatter function to the statusFormatter object below
|
|
9
|
+
* 3. TypeScript will enforce that all event types are covered
|
|
10
|
+
* 4. The formatter function receives the full event data for type safety
|
|
11
|
+
*/
|
|
12
|
+
export type StatusFormatter = {
|
|
13
|
+
[K in BridgeEvents['type']]: (event: Extract<BridgeEvents, {
|
|
14
|
+
type: K;
|
|
15
|
+
}>) => string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Maps every bridge event to a concise, informative status line.
|
|
19
|
+
* Each formatter receives the full event data for type safety.
|
|
20
|
+
*/
|
|
21
|
+
export declare const statusFormatter: StatusFormatter;
|
|
22
|
+
/**
|
|
23
|
+
* Formats a bridge event into a status line.
|
|
24
|
+
* This function is type-safe and ensures all event types are handled.
|
|
25
|
+
*/
|
|
26
|
+
export declare const formatEventStatus: (event: BridgeEvents) => string;
|
|
27
|
+
//# sourceMappingURL=status-formatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-formatter.d.ts","sourceRoot":"","sources":["../../src/utils/status-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG;KAC3B,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,GAAG,CAC3B,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KACtC,MAAM;CACZ,CAAC;AAcF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,eAkD7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,YAAY,KAAG,MAGvD,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const formatDuration = (duration) => {
|
|
2
|
+
if (duration < 1000) {
|
|
3
|
+
return `${duration}ms`;
|
|
4
|
+
}
|
|
5
|
+
return `${(duration / 1000).toFixed(2)}s`;
|
|
6
|
+
};
|
|
7
|
+
const formatFileName = (file) => {
|
|
8
|
+
// Extract just the filename from the path for cleaner display
|
|
9
|
+
return file.split('/').pop() || file;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Maps every bridge event to a concise, informative status line.
|
|
13
|
+
* Each formatter receives the full event data for type safety.
|
|
14
|
+
*/
|
|
15
|
+
export const statusFormatter = {
|
|
16
|
+
// Test Collection Events
|
|
17
|
+
'collection-started': (event) => `📋 Collecting tests from ${formatFileName(event.file)}`,
|
|
18
|
+
'collection-finished': (event) => `✅ Collected tests from ${formatFileName(event.file)} (${formatDuration(event.duration)})`,
|
|
19
|
+
// Test Runner Events
|
|
20
|
+
'file-started': (event) => `🧪 Running tests in ${formatFileName(event.file)}`,
|
|
21
|
+
'file-finished': (event) => `✅ Finished ${formatFileName(event.file)} (${formatDuration(event.duration)})`,
|
|
22
|
+
'suite-started': (event) => `📦 ${event.name}`,
|
|
23
|
+
'suite-finished': (event) => {
|
|
24
|
+
const icon = event.status === 'failed'
|
|
25
|
+
? '❌'
|
|
26
|
+
: event.status === 'skipped'
|
|
27
|
+
? '⏭️'
|
|
28
|
+
: '✅';
|
|
29
|
+
return `${icon} ${event.name} (${formatDuration(event.duration)})`;
|
|
30
|
+
},
|
|
31
|
+
'test-started': (event) => ` 🔄 ${event.name}`,
|
|
32
|
+
'test-finished': (event) => {
|
|
33
|
+
const icon = event.status === 'failed'
|
|
34
|
+
? '❌'
|
|
35
|
+
: event.status === 'skipped'
|
|
36
|
+
? '⏭️'
|
|
37
|
+
: event.status === 'todo'
|
|
38
|
+
? '📝'
|
|
39
|
+
: '✅';
|
|
40
|
+
return ` ${icon} ${event.name} (${formatDuration(event.duration)})`;
|
|
41
|
+
},
|
|
42
|
+
// Bundler Events
|
|
43
|
+
'module-bundling-started': (event) => `📦 Bundling ${formatFileName(event.file)}`,
|
|
44
|
+
'module-bundling-finished': (event) => `✅ Bundled ${formatFileName(event.file)} (${formatDuration(event.duration)})`,
|
|
45
|
+
'module-bundling-failed': (event) => `❌ Failed to bundle ${formatFileName(event.file)} (${formatDuration(event.duration)})`,
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Formats a bridge event into a status line.
|
|
49
|
+
* This function is type-safe and ensures all event types are handled.
|
|
50
|
+
*/
|
|
51
|
+
export const formatEventStatus = (event) => {
|
|
52
|
+
// TypeScript ensures this covers all event types
|
|
53
|
+
return statusFormatter[event.type](event);
|
|
54
|
+
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export declare function assert(condition: boolean, message: string): asserts con
|
|
|
2
2
|
export declare class AssertionError extends Error {
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
5
|
+
export declare const isPortAvailable: (port: number) => Promise<boolean>;
|
|
5
6
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,wBAAgB,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAI7E;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,OAAO,CAAC,OAAO,CAa7D,CAAC"}
|
package/dist/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
1
2
|
export function assert(condition, message) {
|
|
2
3
|
if (!condition) {
|
|
3
4
|
throw new AssertionError(message);
|
|
@@ -9,3 +10,17 @@ export class AssertionError extends Error {
|
|
|
9
10
|
this.name = 'AssertionError';
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
export const isPortAvailable = (port) => {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const server = net.createServer();
|
|
16
|
+
server.once('error', () => {
|
|
17
|
+
server.close();
|
|
18
|
+
resolve(false);
|
|
19
|
+
});
|
|
20
|
+
server.once('listening', () => {
|
|
21
|
+
server.close();
|
|
22
|
+
resolve(true);
|
|
23
|
+
});
|
|
24
|
+
server.listen(port);
|
|
25
|
+
});
|
|
26
|
+
};
|
package/eslint.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/cli",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-canary.1761729829908",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"development": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./external": {
|
|
17
|
+
"development": "./src/external.ts",
|
|
18
|
+
"types": "./dist/external.d.ts",
|
|
19
|
+
"import": "./dist/external.js",
|
|
20
|
+
"default": "./dist/external.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
8
23
|
"dependencies": {
|
|
9
|
-
"commander": "^14.0.0",
|
|
10
|
-
"glob": "^11.0.0",
|
|
11
|
-
"playwright": "^1.53.2",
|
|
12
24
|
"tslib": "^2.3.0",
|
|
13
|
-
"@react-native-harness/bridge": "1.0.0-
|
|
14
|
-
"@react-native-harness/
|
|
15
|
-
"@react-native-harness/config": "1.0.0-alpha.9"
|
|
25
|
+
"@react-native-harness/bridge": "1.0.0-canary.1761729829908",
|
|
26
|
+
"@react-native-harness/config": "1.0.0-canary.1761729829908"
|
|
16
27
|
},
|
|
17
28
|
"devDependencies": {
|
|
18
|
-
"@types/node": "18.16.9"
|
|
19
|
-
|
|
29
|
+
"@types/node": "18.16.9",
|
|
30
|
+
"jest-cli": "^30.2.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"jest-cli": "*"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
20
36
|
}
|
package/src/external.ts
ADDED
|
File without changes
|
package/src/index.ts
CHANGED
|
@@ -1,50 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, join } from 'path';
|
|
5
|
-
import { testCommand } from './commands/test.js';
|
|
6
|
-
import { handleError } from './errors/errorHandler.js';
|
|
7
|
-
import { logger } from '@react-native-harness/tools';
|
|
1
|
+
import { run, yargsOptions } from 'jest-cli';
|
|
2
|
+
import { getConfig } from '@react-native-harness/config';
|
|
8
3
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
4
|
+
const checkForOldConfig = async () => {
|
|
5
|
+
try {
|
|
6
|
+
const { config } = await getConfig(process.cwd());
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.command('test')
|
|
27
|
-
.description('Run tests using the specified runner')
|
|
28
|
-
.argument(
|
|
29
|
-
'[runner]',
|
|
30
|
-
'test runner name (uses defaultRunner from config if not specified)'
|
|
31
|
-
)
|
|
32
|
-
.argument(
|
|
33
|
-
'[pattern]',
|
|
34
|
-
'glob pattern to match test files (uses config.include if not specified)'
|
|
35
|
-
)
|
|
36
|
-
.action(async (runner, pattern) => {
|
|
37
|
-
try {
|
|
38
|
-
await testCommand(runner, pattern);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
handleError(error);
|
|
8
|
+
if (config.include) {
|
|
9
|
+
console.error('\n❌ Migration Required\n');
|
|
10
|
+
console.error('React Native Harness has migrated to the Jest CLI.');
|
|
11
|
+
console.error(
|
|
12
|
+
'The "include" property in your rn-harness.config file is no longer supported.\n'
|
|
13
|
+
);
|
|
14
|
+
console.error(
|
|
15
|
+
'Please follow the migration guide to update your configuration:'
|
|
16
|
+
);
|
|
17
|
+
console.error(
|
|
18
|
+
'https://react-native-harness.dev/docs/guides/migration-guide\n'
|
|
19
|
+
);
|
|
41
20
|
process.exit(1);
|
|
42
21
|
}
|
|
43
|
-
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Swallow the error - if we can't load the config, let Jest CLI handle it
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const patchYargsOptions = () => {
|
|
28
|
+
yargsOptions.harnessRunner = {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Specify which Harness runner to use',
|
|
31
|
+
requiresArg: true,
|
|
32
|
+
};
|
|
44
33
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
// Remove all options that are not supported by Harness
|
|
35
|
+
delete yargsOptions.runner;
|
|
36
|
+
delete yargsOptions.testRunner;
|
|
37
|
+
delete yargsOptions.testEnvironment;
|
|
38
|
+
delete yargsOptions.testEnvironmentOptions;
|
|
39
|
+
delete yargsOptions.transform;
|
|
40
|
+
delete yargsOptions.transformIgnorePatterns;
|
|
41
|
+
delete yargsOptions.updateSnapshot;
|
|
42
|
+
delete yargsOptions.workerThreads;
|
|
43
|
+
delete yargsOptions.snapshotSerializers;
|
|
44
|
+
delete yargsOptions.shard;
|
|
45
|
+
delete yargsOptions.runInBand;
|
|
46
|
+
delete yargsOptions.resolver;
|
|
47
|
+
delete yargsOptions.resetMocks;
|
|
48
|
+
delete yargsOptions.resetModules;
|
|
49
|
+
delete yargsOptions.restoreMocks;
|
|
50
|
+
delete yargsOptions.preset;
|
|
51
|
+
delete yargsOptions.prettierPath;
|
|
52
|
+
delete yargsOptions.maxWorkers;
|
|
53
|
+
delete yargsOptions.moduleDirectories;
|
|
54
|
+
delete yargsOptions.moduleFileExtensions;
|
|
55
|
+
delete yargsOptions.moduleNameMapper;
|
|
56
|
+
delete yargsOptions.modulePathIgnorePatterns;
|
|
57
|
+
delete yargsOptions.modulePaths;
|
|
58
|
+
delete yargsOptions.maxConcurrency;
|
|
59
|
+
delete yargsOptions.injectGlobals;
|
|
60
|
+
delete yargsOptions.globalSetup;
|
|
61
|
+
delete yargsOptions.globalTeardown;
|
|
62
|
+
delete yargsOptions.clearMocks;
|
|
63
|
+
delete yargsOptions.globals;
|
|
64
|
+
delete yargsOptions.haste;
|
|
65
|
+
delete yargsOptions.automock;
|
|
66
|
+
delete yargsOptions.coverageProvider;
|
|
67
|
+
delete yargsOptions.logHeapUsage;
|
|
68
|
+
};
|
|
49
69
|
|
|
50
|
-
|
|
70
|
+
patchYargsOptions();
|
|
71
|
+
checkForOldConfig().then(() => run());
|
package/tsconfig.lib.json
CHANGED
|
@@ -10,19 +10,8 @@
|
|
|
10
10
|
"types": ["node"],
|
|
11
11
|
"lib": ["DOM"]
|
|
12
12
|
},
|
|
13
|
-
"include": [
|
|
14
|
-
"src/**/*.ts",
|
|
15
|
-
"../tools/src/abort.ts",
|
|
16
|
-
"../tools/src/color.ts",
|
|
17
|
-
"../tools/src/isInteractive.ts",
|
|
18
|
-
"../tools/src/logger.ts",
|
|
19
|
-
"../tools/src/prompts.ts",
|
|
20
|
-
"../tools/src/spawn.ts"
|
|
21
|
-
],
|
|
13
|
+
"include": ["src/**/*.ts"],
|
|
22
14
|
"references": [
|
|
23
|
-
{
|
|
24
|
-
"path": "../tools/tsconfig.lib.json"
|
|
25
|
-
},
|
|
26
15
|
{
|
|
27
16
|
"path": "../config/tsconfig.lib.json"
|
|
28
17
|
},
|
package/src/bundlers/metro.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { type ChildProcess } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
getReactNativeCliPath,
|
|
4
|
-
getExpoCliPath,
|
|
5
|
-
getTimeoutSignal,
|
|
6
|
-
spawn,
|
|
7
|
-
SubprocessError,
|
|
8
|
-
} from '@react-native-harness/tools';
|
|
9
|
-
|
|
10
|
-
const METRO_PORT = 8081;
|
|
11
|
-
|
|
12
|
-
export const runMetro = async (isExpo = false): Promise<ChildProcess> => {
|
|
13
|
-
const metro = spawn(
|
|
14
|
-
'node',
|
|
15
|
-
[
|
|
16
|
-
isExpo ? getExpoCliPath() : getReactNativeCliPath(),
|
|
17
|
-
'start',
|
|
18
|
-
'--port',
|
|
19
|
-
METRO_PORT.toString(),
|
|
20
|
-
],
|
|
21
|
-
{
|
|
22
|
-
env: {
|
|
23
|
-
...process.env,
|
|
24
|
-
RN_HARNESS: 'true',
|
|
25
|
-
...(isExpo && { EXPO_NO_METRO_WORKSPACE_ROOT: 'true' }),
|
|
26
|
-
},
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// Forward metro output to CLI output
|
|
31
|
-
metro.nodeChildProcess.then((childProcess) => {
|
|
32
|
-
if (childProcess.stdout) {
|
|
33
|
-
childProcess.stdout.pipe(process.stdout);
|
|
34
|
-
}
|
|
35
|
-
if (childProcess.stderr) {
|
|
36
|
-
childProcess.stderr.pipe(process.stderr);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
metro.catch((error) => {
|
|
41
|
-
// This process is going to be killed by us, so we don't need to throw an error
|
|
42
|
-
if (error instanceof SubprocessError && error.signalName === 'SIGTERM') {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
throw error;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await waitForMetro();
|
|
50
|
-
return metro.nodeChildProcess;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const waitForMetro = async (
|
|
54
|
-
port = 8081,
|
|
55
|
-
maxRetries = 20,
|
|
56
|
-
retryDelay = 1000
|
|
57
|
-
): Promise<void> => {
|
|
58
|
-
let attempts = 0;
|
|
59
|
-
|
|
60
|
-
while (attempts < maxRetries) {
|
|
61
|
-
attempts++;
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(`http://localhost:${port}/status`, {
|
|
65
|
-
signal: getTimeoutSignal(100),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (response.ok) {
|
|
69
|
-
const body = await response.text();
|
|
70
|
-
|
|
71
|
-
if (body === 'packager-status:running') {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
// Errors are expected here, we're just waiting for the process to be ready
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (attempts < maxRetries) {
|
|
80
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
throw new Error(`Metro bundler is not ready after ${maxRetries} attempts`);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export const reloadApp = async (): Promise<void> => {
|
|
88
|
-
await fetch(`http://localhost:${METRO_PORT}/reload`);
|
|
89
|
-
};
|
package/src/commands/test.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getBridgeServer,
|
|
3
|
-
type BridgeServer,
|
|
4
|
-
} from '@react-native-harness/bridge/server';
|
|
5
|
-
import {
|
|
6
|
-
Config,
|
|
7
|
-
getConfig,
|
|
8
|
-
TestRunnerConfig,
|
|
9
|
-
} from '@react-native-harness/config';
|
|
10
|
-
import { getPlatformAdapter } from '../platforms/platform-registry.js';
|
|
11
|
-
import { Glob } from 'glob';
|
|
12
|
-
import {
|
|
13
|
-
intro,
|
|
14
|
-
logger,
|
|
15
|
-
outro,
|
|
16
|
-
spinner,
|
|
17
|
-
progress,
|
|
18
|
-
} from '@react-native-harness/tools';
|
|
19
|
-
import { type Environment } from '../platforms/platform-adapter.js';
|
|
20
|
-
import { BridgeTimeoutError } from '../errors/errors.js';
|
|
21
|
-
import { assert } from '../utils.js';
|
|
22
|
-
import {
|
|
23
|
-
EnvironmentInitializationError,
|
|
24
|
-
NoRunnerSpecifiedError,
|
|
25
|
-
RpcClientError,
|
|
26
|
-
RunnerNotFoundError,
|
|
27
|
-
} from '../errors/errors.js';
|
|
28
|
-
import { TestSuiteResult } from '@react-native-harness/bridge';
|
|
29
|
-
|
|
30
|
-
type TestRunContext = {
|
|
31
|
-
config: Config;
|
|
32
|
-
runner: TestRunnerConfig;
|
|
33
|
-
bridge?: BridgeServer;
|
|
34
|
-
environment?: Environment;
|
|
35
|
-
testFiles?: string[];
|
|
36
|
-
results?: TestSuiteResult[];
|
|
37
|
-
projectRoot: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const setupEnvironment = async (context: TestRunContext): Promise<void> => {
|
|
41
|
-
const startSpinner = spinner();
|
|
42
|
-
const platform = context.runner.platform;
|
|
43
|
-
|
|
44
|
-
startSpinner.start(`Starting "${context.runner.name}" (${platform}) runner`);
|
|
45
|
-
|
|
46
|
-
const platformAdapter = await getPlatformAdapter(platform);
|
|
47
|
-
const serverBridge = await getBridgeServer({
|
|
48
|
-
port: 3001,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
context.bridge = serverBridge;
|
|
52
|
-
|
|
53
|
-
const readyPromise = new Promise<void>((resolve, reject) => {
|
|
54
|
-
const timeout = setTimeout(() => {
|
|
55
|
-
reject(
|
|
56
|
-
new BridgeTimeoutError(
|
|
57
|
-
context.config.bridgeTimeout,
|
|
58
|
-
context.runner.name,
|
|
59
|
-
platform
|
|
60
|
-
)
|
|
61
|
-
);
|
|
62
|
-
}, context.config.bridgeTimeout);
|
|
63
|
-
|
|
64
|
-
serverBridge.once('ready', () => {
|
|
65
|
-
clearTimeout(timeout);
|
|
66
|
-
resolve();
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
context.environment = await platformAdapter.getEnvironment(context.runner);
|
|
71
|
-
|
|
72
|
-
logger.debug('Waiting for bridge to be ready');
|
|
73
|
-
await readyPromise;
|
|
74
|
-
logger.debug('Bridge is ready');
|
|
75
|
-
|
|
76
|
-
if (!context.environment) {
|
|
77
|
-
throw new EnvironmentInitializationError(
|
|
78
|
-
'Failed to initialize environment',
|
|
79
|
-
context.runner.name,
|
|
80
|
-
platform,
|
|
81
|
-
'Platform adapter returned null environment'
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
startSpinner.stop(`"${context.runner.name}" (${platform}) runner started`);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const findTestFiles = async (
|
|
89
|
-
context: TestRunContext,
|
|
90
|
-
pattern?: string
|
|
91
|
-
): Promise<void> => {
|
|
92
|
-
const discoverSpinner = spinner();
|
|
93
|
-
discoverSpinner.start('Discovering tests');
|
|
94
|
-
|
|
95
|
-
const globPattern = pattern || context.config.include;
|
|
96
|
-
const glob = new Glob(globPattern, {
|
|
97
|
-
cwd: context.projectRoot,
|
|
98
|
-
});
|
|
99
|
-
context.testFiles = await glob.walk();
|
|
100
|
-
discoverSpinner.stop(`Found ${context.testFiles.length} test files`);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const runTests = async (context: TestRunContext): Promise<void> => {
|
|
104
|
-
const { bridge, environment, testFiles } = context;
|
|
105
|
-
assert(bridge != null, 'Bridge not initialized');
|
|
106
|
-
assert(environment != null, 'Environment not initialized');
|
|
107
|
-
assert(testFiles != null, 'Test files not initialized');
|
|
108
|
-
|
|
109
|
-
let runSpinner = progress({ style: 'block' });
|
|
110
|
-
runSpinner.start('Running tests');
|
|
111
|
-
|
|
112
|
-
let shouldRestart = false;
|
|
113
|
-
|
|
114
|
-
for (const testFile of testFiles) {
|
|
115
|
-
if (shouldRestart) {
|
|
116
|
-
runSpinner = progress({ style: 'block' });
|
|
117
|
-
runSpinner.message(`Restarting environment for next test file`);
|
|
118
|
-
|
|
119
|
-
await new Promise((resolve) => {
|
|
120
|
-
bridge.once('ready', resolve);
|
|
121
|
-
environment.restart();
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
runSpinner.message(`Running tests in ${testFile}`);
|
|
126
|
-
const client = bridge.rpc.clients.at(-1);
|
|
127
|
-
if (!client) {
|
|
128
|
-
throw new RpcClientError(
|
|
129
|
-
'No RPC client available',
|
|
130
|
-
3001,
|
|
131
|
-
'No clients connected'
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const result = await client.runTests(testFile);
|
|
136
|
-
context.results = [...(context.results ?? []), ...result.suites];
|
|
137
|
-
shouldRestart = true;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const cleanUp = async (context: TestRunContext): Promise<void> => {
|
|
142
|
-
if (context.bridge) {
|
|
143
|
-
context.bridge.ws.close();
|
|
144
|
-
}
|
|
145
|
-
if (context.environment) {
|
|
146
|
-
await context.environment.dispose();
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const hasFailedTests = (results: TestSuiteResult[]): boolean => {
|
|
151
|
-
for (const suite of results) {
|
|
152
|
-
// Check if the suite itself failed
|
|
153
|
-
if (suite.status === 'failed') {
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Check individual tests in the suite
|
|
158
|
-
for (const test of suite.tests) {
|
|
159
|
-
if (test.status === 'failed') {
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Recursively check nested suites
|
|
165
|
-
if (suite.suites && hasFailedTests(suite.suites)) {
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return false;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
export const testCommand = async (
|
|
174
|
-
runnerName?: string,
|
|
175
|
-
pattern?: string
|
|
176
|
-
): Promise<void> => {
|
|
177
|
-
intro('React Native Test Harness');
|
|
178
|
-
|
|
179
|
-
const { config, projectRoot } = await getConfig(process.cwd());
|
|
180
|
-
const selectedRunnerName = runnerName ?? config.defaultRunner;
|
|
181
|
-
|
|
182
|
-
if (!selectedRunnerName) {
|
|
183
|
-
throw new NoRunnerSpecifiedError(config.runners);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const runner = config.runners.find((r) => r.name === selectedRunnerName);
|
|
187
|
-
|
|
188
|
-
if (!runner) {
|
|
189
|
-
throw new RunnerNotFoundError(selectedRunnerName, config.runners);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const context: TestRunContext = {
|
|
193
|
-
config,
|
|
194
|
-
runner,
|
|
195
|
-
testFiles: [],
|
|
196
|
-
results: [],
|
|
197
|
-
projectRoot,
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
await setupEnvironment(context);
|
|
202
|
-
await findTestFiles(context, pattern);
|
|
203
|
-
await runTests(context);
|
|
204
|
-
|
|
205
|
-
assert(context.results != null, 'Results not initialized');
|
|
206
|
-
config.reporter?.report(context.results);
|
|
207
|
-
} finally {
|
|
208
|
-
await cleanUp(context);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Check if any tests failed and exit with appropriate code
|
|
212
|
-
if (hasFailedTests(context.results)) {
|
|
213
|
-
outro('Test run completed with failures');
|
|
214
|
-
process.exit(1);
|
|
215
|
-
} else {
|
|
216
|
-
outro('Test run completed successfully');
|
|
217
|
-
}
|
|
218
|
-
};
|