@react-native-harness/cli 1.1.0 → 1.2.0

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 (116) hide show
  1. package/dist/__tests__/platform-commands.test.d.ts +2 -0
  2. package/dist/__tests__/platform-commands.test.d.ts.map +1 -0
  3. package/dist/__tests__/platform-commands.test.js +207 -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 +71 -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 +3 -0
  11. package/dist/commands/test.d.ts.map +1 -0
  12. package/dist/commands/test.js +140 -0
  13. package/dist/discovery/index.d.ts +3 -0
  14. package/dist/discovery/index.d.ts.map +1 -0
  15. package/dist/discovery/index.js +1 -0
  16. package/dist/discovery/testDiscovery.d.ts +11 -0
  17. package/dist/discovery/testDiscovery.d.ts.map +1 -0
  18. package/dist/discovery/testDiscovery.js +29 -0
  19. package/dist/errors/appNotInstalledError.d.ts +7 -0
  20. package/dist/errors/appNotInstalledError.d.ts.map +1 -0
  21. package/dist/errors/appNotInstalledError.js +12 -0
  22. package/dist/errors/bridgeTimeoutError.d.ts +7 -0
  23. package/dist/errors/bridgeTimeoutError.d.ts.map +1 -0
  24. package/dist/errors/bridgeTimeoutError.js +12 -0
  25. package/dist/errors/errorHandler.d.ts +2 -0
  26. package/dist/errors/errorHandler.d.ts.map +1 -0
  27. package/dist/errors/errorHandler.js +152 -0
  28. package/dist/errors/errors.d.ts +45 -0
  29. package/dist/errors/errors.d.ts.map +1 -0
  30. package/dist/errors/errors.js +89 -0
  31. package/dist/index.js +113 -6
  32. package/dist/jest.d.ts +2 -0
  33. package/dist/jest.d.ts.map +1 -0
  34. package/dist/jest.js +7 -0
  35. package/dist/platform-commands.d.ts +18 -0
  36. package/dist/platform-commands.d.ts.map +1 -0
  37. package/dist/platform-commands.js +84 -0
  38. package/dist/platforms/android/build.d.ts +5 -0
  39. package/dist/platforms/android/build.d.ts.map +1 -0
  40. package/dist/platforms/android/build.js +29 -0
  41. package/dist/platforms/android/device.d.ts +5 -0
  42. package/dist/platforms/android/device.d.ts.map +1 -0
  43. package/dist/platforms/android/device.js +36 -0
  44. package/dist/platforms/android/emulator.d.ts +10 -0
  45. package/dist/platforms/android/emulator.d.ts.map +1 -0
  46. package/dist/platforms/android/emulator.js +116 -0
  47. package/dist/platforms/android/index.d.ts +4 -0
  48. package/dist/platforms/android/index.d.ts.map +1 -0
  49. package/dist/platforms/android/index.js +56 -0
  50. package/dist/platforms/ios/build.d.ts +7 -0
  51. package/dist/platforms/ios/build.d.ts.map +1 -0
  52. package/dist/platforms/ios/build.js +48 -0
  53. package/dist/platforms/ios/device.d.ts +11 -0
  54. package/dist/platforms/ios/device.d.ts.map +1 -0
  55. package/dist/platforms/ios/device.js +51 -0
  56. package/dist/platforms/ios/index.d.ts +4 -0
  57. package/dist/platforms/ios/index.d.ts.map +1 -0
  58. package/dist/platforms/ios/index.js +43 -0
  59. package/dist/platforms/ios/simulator.d.ts +11 -0
  60. package/dist/platforms/ios/simulator.d.ts.map +1 -0
  61. package/dist/platforms/ios/simulator.js +129 -0
  62. package/dist/platforms/platform-adapter.d.ts +10 -0
  63. package/dist/platforms/platform-adapter.d.ts.map +1 -0
  64. package/dist/platforms/platform-adapter.js +1 -0
  65. package/dist/platforms/platform-registry.d.ts +3 -0
  66. package/dist/platforms/platform-registry.d.ts.map +1 -0
  67. package/dist/platforms/platform-registry.js +21 -0
  68. package/dist/platforms/vega/build.d.ts +23 -0
  69. package/dist/platforms/vega/build.d.ts.map +1 -0
  70. package/dist/platforms/vega/build.js +55 -0
  71. package/dist/platforms/vega/device.d.ts +57 -0
  72. package/dist/platforms/vega/device.d.ts.map +1 -0
  73. package/dist/platforms/vega/device.js +206 -0
  74. package/dist/platforms/vega/index.d.ts +4 -0
  75. package/dist/platforms/vega/index.d.ts.map +1 -0
  76. package/dist/platforms/vega/index.js +75 -0
  77. package/dist/platforms/web/index.d.ts +4 -0
  78. package/dist/platforms/web/index.d.ts.map +1 -0
  79. package/dist/platforms/web/index.js +9 -0
  80. package/dist/process.d.ts +3 -0
  81. package/dist/process.d.ts.map +1 -0
  82. package/dist/process.js +28 -0
  83. package/dist/reporters/default-reporter.d.ts +3 -0
  84. package/dist/reporters/default-reporter.d.ts.map +1 -0
  85. package/dist/reporters/default-reporter.js +116 -0
  86. package/dist/reporters/junit-reporter.d.ts +3 -0
  87. package/dist/reporters/junit-reporter.d.ts.map +1 -0
  88. package/dist/reporters/junit-reporter.js +119 -0
  89. package/dist/reporters/live-reporter.d.ts +20 -0
  90. package/dist/reporters/live-reporter.d.ts.map +1 -0
  91. package/dist/reporters/live-reporter.js +176 -0
  92. package/dist/src/reporters/default-reporter.js +135 -0
  93. package/dist/test-reporter-demo.js +95 -0
  94. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  95. package/dist/utils/status-formatter.d.ts +27 -0
  96. package/dist/utils/status-formatter.d.ts.map +1 -0
  97. package/dist/utils/status-formatter.js +54 -0
  98. package/dist/utils.d.ts +6 -0
  99. package/dist/utils.d.ts.map +1 -0
  100. package/dist/utils.js +26 -0
  101. package/dist/wizard/bundleId.js +1 -1
  102. package/dist/wizard/jestIntegration.d.ts +2 -0
  103. package/dist/wizard/jestIntegration.d.ts.map +1 -0
  104. package/dist/wizard/jestIntegration.js +15 -0
  105. package/dist/wizard/packageManager.d.ts +2 -0
  106. package/dist/wizard/packageManager.d.ts.map +1 -0
  107. package/dist/wizard/packageManager.js +10 -0
  108. package/eslint.config.mjs +1 -1
  109. package/package.json +8 -8
  110. package/skills/core.md +92 -0
  111. package/skills/mocking.md +87 -0
  112. package/skills/ui.md +59 -0
  113. package/src/__tests__/platform-commands.test.ts +232 -0
  114. package/src/index.ts +152 -5
  115. package/src/platform-commands.ts +148 -0
  116. package/src/wizard/bundleId.ts +1 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=platform-commands.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-commands.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/platform-commands.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,207 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { discoverPlatformCommands, runPlatformCommand, } from '../platform-commands.js';
3
+ const createCommandModuleUrl = (body) => `data:text/javascript,${encodeURIComponent(body)}`;
4
+ const globalState = globalThis;
5
+ describe('platform CLI command discovery', () => {
6
+ afterEach(() => {
7
+ delete globalState.__platformCommandCall;
8
+ });
9
+ it('runs a discovered platform command', async () => {
10
+ const moduleUrl = createCommandModuleUrl(`
11
+ export const commands = [{
12
+ name: 'xctest',
13
+ async run(args, context) {
14
+ globalThis.__platformCommandCall = { args, context };
15
+ }
16
+ }];
17
+ `);
18
+ const loadConfig = vi.fn(async () => ({
19
+ projectRoot: '/tmp/project',
20
+ config: {
21
+ entryPoint: 'index.js',
22
+ appRegistryComponentName: 'App',
23
+ runners: [
24
+ {
25
+ name: 'ios',
26
+ config: {},
27
+ runner: '/virtual/runner.js',
28
+ cli: moduleUrl,
29
+ platformId: 'ios',
30
+ },
31
+ ],
32
+ plugins: [],
33
+ metroPort: 8081,
34
+ webSocketPort: undefined,
35
+ bridgeTimeout: 60000,
36
+ platformReadyTimeout: 300000,
37
+ bundleStartTimeout: 60000,
38
+ maxAppRestarts: 2,
39
+ resetEnvironmentBetweenTestFiles: true,
40
+ unstable__skipAlreadyIncludedModules: false,
41
+ unstable__enableMetroCache: false,
42
+ permissions: false,
43
+ detectNativeCrashes: true,
44
+ crashDetectionInterval: 500,
45
+ disableViewFlattening: false,
46
+ forwardClientLogs: false,
47
+ },
48
+ }));
49
+ expect(await runPlatformCommand({
50
+ argv: ['xctest', 'build', '--destination', 'simulator'],
51
+ cwd: '/tmp/project',
52
+ loadConfig,
53
+ })).toBe(true);
54
+ expect(globalState.__platformCommandCall).toEqual({
55
+ args: ['build', '--destination', 'simulator'],
56
+ context: {
57
+ cwd: '/tmp/project',
58
+ projectRoot: '/tmp/project',
59
+ },
60
+ });
61
+ });
62
+ it('deduplicates platform CLI modules across runners', async () => {
63
+ const moduleUrl = createCommandModuleUrl(`
64
+ export const commands = [{
65
+ name: 'xctest',
66
+ async run() {}
67
+ }];
68
+ `);
69
+ const loadConfig = vi.fn(async () => ({
70
+ projectRoot: '/tmp/project',
71
+ config: {
72
+ entryPoint: 'index.js',
73
+ appRegistryComponentName: 'App',
74
+ runners: [
75
+ {
76
+ name: 'ios-sim',
77
+ config: {},
78
+ runner: '/virtual/ios-sim-runner.js',
79
+ cli: moduleUrl,
80
+ platformId: 'ios',
81
+ },
82
+ {
83
+ name: 'ios-device',
84
+ config: {},
85
+ runner: '/virtual/ios-device-runner.js',
86
+ cli: moduleUrl,
87
+ platformId: 'ios',
88
+ },
89
+ ],
90
+ plugins: [],
91
+ metroPort: 8081,
92
+ webSocketPort: undefined,
93
+ bridgeTimeout: 60000,
94
+ platformReadyTimeout: 300000,
95
+ bundleStartTimeout: 60000,
96
+ maxAppRestarts: 2,
97
+ resetEnvironmentBetweenTestFiles: true,
98
+ unstable__skipAlreadyIncludedModules: false,
99
+ unstable__enableMetroCache: false,
100
+ permissions: false,
101
+ detectNativeCrashes: true,
102
+ crashDetectionInterval: 500,
103
+ disableViewFlattening: false,
104
+ forwardClientLogs: false,
105
+ },
106
+ }));
107
+ const discoveredCommands = await discoverPlatformCommands({
108
+ cwd: '/tmp/project',
109
+ loadConfig,
110
+ });
111
+ expect(discoveredCommands?.commands).toHaveLength(1);
112
+ });
113
+ it('returns false when no platform command matches', async () => {
114
+ const loadConfig = vi.fn(async () => ({
115
+ projectRoot: '/tmp/project',
116
+ config: {
117
+ entryPoint: 'index.js',
118
+ appRegistryComponentName: 'App',
119
+ runners: [
120
+ {
121
+ name: 'android',
122
+ config: {},
123
+ runner: '/virtual/android-runner.js',
124
+ platformId: 'android',
125
+ },
126
+ ],
127
+ plugins: [],
128
+ metroPort: 8081,
129
+ webSocketPort: undefined,
130
+ bridgeTimeout: 60000,
131
+ platformReadyTimeout: 300000,
132
+ bundleStartTimeout: 60000,
133
+ maxAppRestarts: 2,
134
+ resetEnvironmentBetweenTestFiles: true,
135
+ unstable__skipAlreadyIncludedModules: false,
136
+ unstable__enableMetroCache: false,
137
+ permissions: false,
138
+ detectNativeCrashes: true,
139
+ crashDetectionInterval: 500,
140
+ disableViewFlattening: false,
141
+ forwardClientLogs: false,
142
+ },
143
+ }));
144
+ await expect(runPlatformCommand({
145
+ argv: ['xctest', 'build'],
146
+ cwd: '/tmp/project',
147
+ loadConfig,
148
+ })).resolves.toBe(false);
149
+ });
150
+ it('throws when two platform modules define the same command', async () => {
151
+ const firstModuleUrl = createCommandModuleUrl(`
152
+ export const commands = [{
153
+ name: 'xctest',
154
+ async run() {}
155
+ }];
156
+ `);
157
+ const secondModuleUrl = createCommandModuleUrl(`
158
+ // second module
159
+ export const commands = [{
160
+ name: 'xctest',
161
+ async run() {}
162
+ }];
163
+ `);
164
+ const loadConfig = vi.fn(async () => ({
165
+ projectRoot: '/tmp/project',
166
+ config: {
167
+ entryPoint: 'index.js',
168
+ appRegistryComponentName: 'App',
169
+ runners: [
170
+ {
171
+ name: 'ios',
172
+ config: {},
173
+ runner: '/virtual/ios-runner.js',
174
+ cli: firstModuleUrl,
175
+ platformId: 'ios',
176
+ },
177
+ {
178
+ name: 'android',
179
+ config: {},
180
+ runner: '/virtual/android-runner.js',
181
+ cli: secondModuleUrl,
182
+ platformId: 'android',
183
+ },
184
+ ],
185
+ plugins: [],
186
+ metroPort: 8081,
187
+ webSocketPort: undefined,
188
+ bridgeTimeout: 60000,
189
+ platformReadyTimeout: 300000,
190
+ bundleStartTimeout: 60000,
191
+ maxAppRestarts: 2,
192
+ resetEnvironmentBetweenTestFiles: true,
193
+ unstable__skipAlreadyIncludedModules: false,
194
+ unstable__enableMetroCache: false,
195
+ permissions: false,
196
+ detectNativeCrashes: true,
197
+ crashDetectionInterval: 500,
198
+ disableViewFlattening: false,
199
+ forwardClientLogs: false,
200
+ },
201
+ }));
202
+ await expect(discoverPlatformCommands({
203
+ cwd: '/tmp/project',
204
+ loadConfig,
205
+ })).rejects.toThrow("Duplicate platform CLI command 'xctest'");
206
+ });
207
+ });
@@ -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;AAcvD,eAAO,MAAM,QAAQ,GAAU,gBAAc,KAAG,OAAO,CAAC,YAAY,CAiDnE,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,71 @@
1
+ import { getReactNativeCliPath, getExpoCliPath, getTimeoutSignal, spawn, logger, SubprocessError, } from '@react-native-harness/tools';
2
+ import { isPortAvailable } from '../utils.js';
3
+ import { MetroPortUnavailableError } from '../errors/errors.js';
4
+ const METRO_PORT = 8081;
5
+ export const runMetro = async (isExpo = false) => {
6
+ const metro = spawn('node', [
7
+ isExpo ? getExpoCliPath() : getReactNativeCliPath(),
8
+ 'start',
9
+ '--port',
10
+ METRO_PORT.toString(),
11
+ ], {
12
+ env: {
13
+ ...process.env,
14
+ RN_HARNESS: 'true',
15
+ ...(isExpo && { EXPO_NO_METRO_WORKSPACE_ROOT: 'true' }),
16
+ },
17
+ });
18
+ // Forward metro output to logger
19
+ metro.nodeChildProcess.then((childProcess) => {
20
+ if (childProcess.stdout) {
21
+ childProcess.stdout.on('data', (data) => {
22
+ logger.debug(data.toString().trim());
23
+ });
24
+ }
25
+ if (childProcess.stderr) {
26
+ childProcess.stderr.on('data', (data) => {
27
+ logger.debug(data.toString().trim());
28
+ });
29
+ }
30
+ });
31
+ metro.catch((error) => {
32
+ // This process is going to be killed by us, so we don't need to throw an error
33
+ if (error instanceof SubprocessError && error.signalName === 'SIGTERM') {
34
+ return;
35
+ }
36
+ logger.error('Metro crashed unexpectedly', error);
37
+ });
38
+ const isDefaultPortAvailable = await isPortAvailable(METRO_PORT);
39
+ if (!isDefaultPortAvailable) {
40
+ throw new MetroPortUnavailableError(METRO_PORT);
41
+ }
42
+ await waitForMetro();
43
+ return metro.nodeChildProcess;
44
+ };
45
+ export const waitForMetro = async (port = 8081, maxRetries = 20, retryDelay = 1000) => {
46
+ let attempts = 0;
47
+ while (attempts < maxRetries) {
48
+ attempts++;
49
+ try {
50
+ const response = await fetch(`http://localhost:${port}/status`, {
51
+ signal: getTimeoutSignal(100),
52
+ });
53
+ if (response.ok) {
54
+ const body = await response.text();
55
+ if (body === 'packager-status:running') {
56
+ return;
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Errors are expected here, we're just waiting for the process to be ready
62
+ }
63
+ if (attempts < maxRetries) {
64
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
65
+ }
66
+ }
67
+ throw new Error(`Metro bundler is not ready after ${maxRetries} attempts`);
68
+ };
69
+ export const reloadApp = async () => {
70
+ await fetch(`http://localhost:${METRO_PORT}/reload`);
71
+ };
@@ -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,3 @@
1
+ import { type TestFilterOptions } from '../discovery/index.js';
2
+ export declare const testCommand: (runnerName?: string, options?: TestFilterOptions) => Promise<void>;
3
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/commands/test.ts"],"names":[],"mappings":"AAsBA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAiK/B,eAAO,MAAM,WAAW,GACtB,aAAa,MAAM,EACnB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,IAAI,CA0Cd,CAAC"}
@@ -0,0 +1,140 @@
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 { intro, logger, outro, spinner } from '@react-native-harness/tools';
5
+ import { BridgeTimeoutError } from '../errors/errors.js';
6
+ import { assert } from '../utils.js';
7
+ import { EnvironmentInitializationError, NoRunnerSpecifiedError, RpcClientError, RunnerNotFoundError, } from '../errors/errors.js';
8
+ import { discoverTestFiles, } from '../discovery/index.js';
9
+ const setupEnvironment = async (context) => {
10
+ const startSpinner = spinner();
11
+ const platform = context.runner.platform;
12
+ startSpinner.start(`Starting "${context.runner.name}" (${platform}) runner`);
13
+ const platformAdapter = await getPlatformAdapter(platform);
14
+ const serverBridge = await getBridgeServer({
15
+ port: 3001,
16
+ });
17
+ context.bridge = serverBridge;
18
+ const readyPromise = new Promise((resolve, reject) => {
19
+ const timeout = setTimeout(() => {
20
+ reject(new BridgeTimeoutError(context.config.bridgeTimeout, context.runner.name, platform));
21
+ }, context.config.bridgeTimeout);
22
+ serverBridge.once('ready', () => {
23
+ clearTimeout(timeout);
24
+ resolve();
25
+ });
26
+ });
27
+ context.environment = await platformAdapter.getEnvironment(context.runner);
28
+ logger.debug('Waiting for bridge to be ready');
29
+ await readyPromise;
30
+ logger.debug('Bridge is ready');
31
+ if (!context.environment) {
32
+ throw new EnvironmentInitializationError('Failed to initialize environment', context.runner.name, platform, 'Platform adapter returned null environment');
33
+ }
34
+ startSpinner.stop(`"${context.runner.name}" (${platform}) runner started`);
35
+ };
36
+ const findTestFiles = async (context, options = {}) => {
37
+ const discoverSpinner = spinner();
38
+ discoverSpinner.start('Discovering tests');
39
+ context.testFiles = await discoverTestFiles(context.projectRoot, context.config.include, options);
40
+ discoverSpinner.stop(`Found ${context.testFiles.length} test files`);
41
+ };
42
+ const runTests = async (context, options = {}) => {
43
+ const { bridge, environment, testFiles, config } = context;
44
+ assert(bridge != null, 'Bridge not initialized');
45
+ assert(environment != null, 'Environment not initialized');
46
+ assert(testFiles != null, 'Test files not initialized');
47
+ let runSpinner = spinner();
48
+ runSpinner.start('Running tests');
49
+ let shouldRestart = false;
50
+ for (const testFile of testFiles) {
51
+ if (shouldRestart) {
52
+ runSpinner = spinner();
53
+ runSpinner.start(`Restarting environment for next test file`);
54
+ await new Promise((resolve) => {
55
+ bridge.once('ready', resolve);
56
+ environment.restart();
57
+ });
58
+ }
59
+ runSpinner.message(`Running tests in ${testFile}`);
60
+ const client = bridge.rpc.clients.at(-1);
61
+ if (!client) {
62
+ throw new RpcClientError('No RPC client available', 3001, 'No clients connected');
63
+ }
64
+ // Pass only testNamePattern to runtime (file filtering already done)
65
+ const executionOptions = {
66
+ testNamePattern: options.testNamePattern,
67
+ };
68
+ const result = await client.runTests(testFile, executionOptions);
69
+ context.results = [...(context.results ?? []), ...result.suites];
70
+ if (config.resetEnvironmentBetweenTestFiles) {
71
+ shouldRestart = true;
72
+ }
73
+ runSpinner.stop(`Test file ${testFile} completed`);
74
+ }
75
+ runSpinner.stop('Tests completed');
76
+ };
77
+ const cleanUp = async (context) => {
78
+ if (context.bridge) {
79
+ context.bridge.dispose();
80
+ }
81
+ if (context.environment) {
82
+ await context.environment.dispose();
83
+ }
84
+ };
85
+ const hasFailedTests = (results) => {
86
+ for (const suite of results) {
87
+ // Check if the suite itself failed
88
+ if (suite.status === 'failed') {
89
+ return true;
90
+ }
91
+ // Check individual tests in the suite
92
+ for (const test of suite.tests) {
93
+ if (test.status === 'failed') {
94
+ return true;
95
+ }
96
+ }
97
+ // Recursively check nested suites
98
+ if (suite.suites && hasFailedTests(suite.suites)) {
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ };
104
+ export const testCommand = async (runnerName, options = {}) => {
105
+ intro('React Native Test Harness');
106
+ const { config, projectRoot } = await getConfig(process.cwd());
107
+ const selectedRunnerName = runnerName ?? config.defaultRunner;
108
+ if (!selectedRunnerName) {
109
+ throw new NoRunnerSpecifiedError(config.runners);
110
+ }
111
+ const runner = config.runners.find((r) => r.name === selectedRunnerName);
112
+ if (!runner) {
113
+ throw new RunnerNotFoundError(selectedRunnerName, config.runners);
114
+ }
115
+ const context = {
116
+ config,
117
+ runner,
118
+ testFiles: [],
119
+ results: [],
120
+ projectRoot,
121
+ };
122
+ try {
123
+ await setupEnvironment(context);
124
+ await findTestFiles(context, options);
125
+ await runTests(context, options);
126
+ assert(context.results != null, 'Results not initialized');
127
+ config.reporter?.report(context.results);
128
+ }
129
+ finally {
130
+ await cleanUp(context);
131
+ }
132
+ // Check if any tests failed and exit with appropriate code
133
+ if (hasFailedTests(context.results)) {
134
+ outro('Test run completed with failures');
135
+ process.exit(1);
136
+ }
137
+ else {
138
+ outro('Test run completed successfully');
139
+ }
140
+ };
@@ -0,0 +1,3 @@
1
+ export { discoverTestFiles } from './testDiscovery.js';
2
+ export type { TestFilterOptions } from './testDiscovery.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/discovery/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1 @@
1
+ export { discoverTestFiles } from './testDiscovery.js';
@@ -0,0 +1,11 @@
1
+ export type TestFilterOptions = {
2
+ testNamePattern?: string;
3
+ testPathPattern?: string;
4
+ testPathIgnorePatterns?: string[];
5
+ testMatch?: string[];
6
+ };
7
+ /**
8
+ * Discovers test files based on patterns and filtering options
9
+ */
10
+ export declare const discoverTestFiles: (projectRoot: string, configInclude: string | string[], options?: TestFilterOptions) => Promise<string[]>;
11
+ //# sourceMappingURL=testDiscovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testDiscovery.d.ts","sourceRoot":"","sources":["../../src/discovery/testDiscovery.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAC5B,aAAa,MAAM,EACnB,eAAe,MAAM,GAAG,MAAM,EAAE,EAChC,UAAS,iBAAsB,KAC9B,OAAO,CAAC,MAAM,EAAE,CAiClB,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { Glob } from 'glob';
2
+ /**
3
+ * Discovers test files based on patterns and filtering options
4
+ */
5
+ export const discoverTestFiles = async (projectRoot, configInclude, options = {}) => {
6
+ // Priority: testMatch > configInclude
7
+ const patterns = options.testMatch || configInclude;
8
+ const patternArray = Array.isArray(patterns) ? patterns : [patterns];
9
+ // Glob discovery
10
+ const allFiles = [];
11
+ for (const pattern of patternArray) {
12
+ const glob = new Glob(pattern, { cwd: projectRoot, nodir: true });
13
+ const files = await glob.walk();
14
+ allFiles.push(...files);
15
+ }
16
+ // Remove duplicates
17
+ let uniqueFiles = [...new Set(allFiles)];
18
+ // Apply testPathPattern filtering
19
+ if (options.testPathPattern) {
20
+ const regex = new RegExp(options.testPathPattern);
21
+ uniqueFiles = uniqueFiles.filter((file) => regex.test(file));
22
+ }
23
+ // Apply testPathIgnorePatterns filtering
24
+ if (options.testPathIgnorePatterns?.length) {
25
+ const ignoreRegexes = options.testPathIgnorePatterns.map((p) => new RegExp(p));
26
+ uniqueFiles = uniqueFiles.filter((file) => !ignoreRegexes.some((regex) => regex.test(file)));
27
+ }
28
+ return uniqueFiles;
29
+ };
@@ -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 formatError: (error: unknown) => string;
2
+ //# sourceMappingURL=errorHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/errors/errorHandler.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,MAqK5C,CAAC"}