@react-native-harness/jest 1.0.0-canary.1766225407244 → 1.0.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 (59) hide show
  1. package/dist/__tests__/client-log-handler.test.d.ts +2 -0
  2. package/dist/__tests__/client-log-handler.test.d.ts.map +1 -0
  3. package/dist/__tests__/client-log-handler.test.js +262 -0
  4. package/dist/cli-args.test.d.ts +2 -0
  5. package/dist/cli-args.test.d.ts.map +1 -0
  6. package/dist/cli-args.test.js +1 -0
  7. package/dist/client-log-handler.d.ts +23 -0
  8. package/dist/client-log-handler.d.ts.map +1 -0
  9. package/dist/client-log-handler.js +84 -0
  10. package/dist/crash-monitor.d.ts +16 -0
  11. package/dist/crash-monitor.d.ts.map +1 -0
  12. package/dist/crash-monitor.js +75 -0
  13. package/dist/errors.d.ts +8 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +17 -0
  16. package/dist/event-mapper.d.ts +16 -0
  17. package/dist/event-mapper.d.ts.map +1 -0
  18. package/dist/event-mapper.js +120 -0
  19. package/dist/global-setup.d.ts +3 -0
  20. package/dist/global-setup.d.ts.map +1 -0
  21. package/dist/global-setup.js +60 -0
  22. package/dist/global-teardown.d.ts +3 -0
  23. package/dist/global-teardown.d.ts.map +1 -0
  24. package/dist/global-teardown.js +11 -0
  25. package/dist/harness-reporter.d.ts +8 -0
  26. package/dist/harness-reporter.d.ts.map +1 -0
  27. package/dist/harness-reporter.js +12 -0
  28. package/dist/harness.d.ts +17 -3
  29. package/dist/harness.d.ts.map +1 -1
  30. package/dist/harness.js +114 -19
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +39 -8
  33. package/dist/jest-options.d.ts +2 -0
  34. package/dist/jest-options.d.ts.map +1 -0
  35. package/dist/jest-options.js +1 -0
  36. package/dist/logs.d.ts +1 -0
  37. package/dist/logs.d.ts.map +1 -1
  38. package/dist/logs.js +3 -0
  39. package/dist/run.d.ts.map +1 -1
  40. package/dist/run.js +1 -0
  41. package/dist/setup.d.ts.map +1 -1
  42. package/dist/setup.js +6 -0
  43. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  44. package/dist/types.d.ts +17 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +1 -0
  47. package/eslint.config.mjs +5 -2
  48. package/package.json +6 -7
  49. package/src/__tests__/client-log-handler.test.ts +328 -0
  50. package/src/client-log-handler.ts +101 -0
  51. package/src/crash-monitor.ts +109 -0
  52. package/src/errors.ts +20 -0
  53. package/src/harness.ts +173 -29
  54. package/src/index.ts +46 -10
  55. package/src/logs.ts +6 -0
  56. package/src/run.ts +1 -0
  57. package/src/setup.ts +8 -0
  58. package/tsconfig.json +3 -3
  59. package/vite.config.ts +18 -0
package/src/harness.ts CHANGED
@@ -1,17 +1,128 @@
1
- import { getBridgeServer } from '@react-native-harness/bridge/server';
2
- import { BridgeClientFunctions } from '@react-native-harness/bridge';
3
- import { HarnessPlatform } from '@react-native-harness/platforms';
4
- import { getMetroInstance } from '@react-native-harness/bundler-metro';
5
- import { InitializationTimeoutError } from './errors.js';
1
+ import {
2
+ getBridgeServer,
3
+ BridgeServer,
4
+ } from '@react-native-harness/bridge/server';
5
+ import {
6
+ HarnessContext,
7
+ TestExecutionOptions,
8
+ TestSuiteResult,
9
+ } from '@react-native-harness/bridge';
10
+ import {
11
+ HarnessPlatform,
12
+ HarnessPlatformRunner,
13
+ } from '@react-native-harness/platforms';
14
+ import {
15
+ getMetroInstance,
16
+ prewarmMetroBundle,
17
+ Reporter,
18
+ ReportableEvent,
19
+ } from '@react-native-harness/bundler-metro';
20
+ import { InitializationTimeoutError, MaxAppRestartsError } from './errors.js';
6
21
  import { Config as HarnessConfig } from '@react-native-harness/config';
7
- import pRetry from 'p-retry';
22
+ import { createCrashMonitor, CrashMonitor } from './crash-monitor.js';
23
+ import { createClientLogListener } from './client-log-handler.js';
24
+ import { logMetroPrewarmCompleted } from './logs.js';
8
25
 
9
- const BRIDGE_READY_TIMEOUT = 10000;
26
+ export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
10
27
 
11
28
  export type Harness = {
12
- runTests: BridgeClientFunctions['runTests'];
29
+ context: HarnessContext;
30
+ runTests: (
31
+ path: string,
32
+ options: HarnessRunTestsOptions
33
+ ) => Promise<TestSuiteResult>;
13
34
  restart: () => Promise<void>;
14
35
  dispose: () => Promise<void>;
36
+ crashMonitor: CrashMonitor;
37
+ };
38
+
39
+ export const waitForAppReady = async (options: {
40
+ metroEvents: Reporter;
41
+ serverBridge: BridgeServer;
42
+ platformInstance: HarnessPlatformRunner;
43
+ bundleStartTimeout: number;
44
+ maxRestarts: number;
45
+ signal: AbortSignal;
46
+ }): Promise<void> => {
47
+ const {
48
+ metroEvents,
49
+ serverBridge,
50
+ platformInstance,
51
+ bundleStartTimeout,
52
+ maxRestarts,
53
+ signal,
54
+ } = options;
55
+
56
+ let restartCount = 0;
57
+ let isBundling = false;
58
+ let bundleTimeoutId: NodeJS.Timeout | null = null;
59
+
60
+ const clearBundleTimeout = () => {
61
+ if (bundleTimeoutId) {
62
+ clearTimeout(bundleTimeoutId);
63
+ bundleTimeoutId = null;
64
+ }
65
+ };
66
+
67
+ return new Promise<void>((resolve, reject) => {
68
+ // Handle abort signal
69
+ signal.addEventListener('abort', () => {
70
+ clearBundleTimeout();
71
+ reject(new DOMException('The operation was aborted', 'AbortError'));
72
+ });
73
+
74
+ // Start/restart the bundle timeout
75
+ const startBundleTimeout = () => {
76
+ clearBundleTimeout();
77
+ bundleTimeoutId = setTimeout(() => {
78
+ if (isBundling) return; // Don't restart while bundling
79
+
80
+ if (restartCount >= maxRestarts) {
81
+ cleanup();
82
+ reject(new MaxAppRestartsError(restartCount + 1));
83
+ return;
84
+ }
85
+
86
+ restartCount++;
87
+ platformInstance.restartApp().catch(reject);
88
+ startBundleTimeout(); // Reset timer for next attempt
89
+ }, bundleStartTimeout);
90
+ };
91
+
92
+ // Metro event listener
93
+ const onMetroEvent = (event: ReportableEvent) => {
94
+ if (event.type === 'bundle_build_started') {
95
+ isBundling = true;
96
+ clearBundleTimeout(); // Cancel restart timer while bundling
97
+ } else if (
98
+ event.type === 'bundle_build_done' ||
99
+ event.type === 'bundle_build_failed'
100
+ ) {
101
+ isBundling = false;
102
+ startBundleTimeout(); // Reset timer after bundle completes
103
+ }
104
+ };
105
+
106
+ // Bridge ready listener
107
+ const onReady = () => {
108
+ cleanup();
109
+ resolve();
110
+ };
111
+
112
+ const cleanup = () => {
113
+ clearBundleTimeout();
114
+ metroEvents.removeListener(onMetroEvent);
115
+ serverBridge.off('ready', onReady);
116
+ };
117
+
118
+ // Setup listeners
119
+ metroEvents.addListener(onMetroEvent);
120
+ serverBridge.once('ready', onReady);
121
+
122
+ // Start the app and timeout
123
+ platformInstance.restartApp().catch(reject);
124
+ startBundleTimeout();
125
+ });
15
126
  };
16
127
 
17
128
  const getHarnessInternal = async (
@@ -20,16 +131,33 @@ const getHarnessInternal = async (
20
131
  projectRoot: string,
21
132
  signal: AbortSignal
22
133
  ): Promise<Harness> => {
134
+ const context: HarnessContext = {
135
+ platform,
136
+ };
137
+
23
138
  const [metroInstance, platformInstance, serverBridge] = await Promise.all([
24
139
  getMetroInstance({ projectRoot, harnessConfig: config }, signal),
25
- import(platform.runner).then((module) => module.default(platform.config)),
140
+ import(platform.runner).then((module) =>
141
+ module.default(platform.config, config)
142
+ ),
26
143
  getBridgeServer({
27
- port: 3001,
144
+ port: config.webSocketPort,
28
145
  timeout: config.bridgeTimeout,
146
+ context,
29
147
  }),
30
148
  ]);
31
149
 
150
+ // Forward client logs to console if enabled
151
+ const clientLogListener = createClientLogListener();
152
+
153
+ if (config.forwardClientLogs) {
154
+ metroInstance.events.addListener(clientLogListener);
155
+ }
156
+
32
157
  const dispose = async () => {
158
+ if (config.forwardClientLogs) {
159
+ metroInstance.events.removeListener(clientLogListener);
160
+ }
33
161
  await Promise.all([
34
162
  serverBridge.dispose(),
35
163
  platformInstance.dispose(),
@@ -37,6 +165,12 @@ const getHarnessInternal = async (
37
165
  ]);
38
166
  };
39
167
 
168
+ const crashMonitor = createCrashMonitor({
169
+ interval: config.crashDetectionInterval,
170
+ platformRunner: platformInstance,
171
+ bridgeServer: serverBridge,
172
+ });
173
+
40
174
  if (signal.aborted) {
41
175
  await dispose();
42
176
 
@@ -44,23 +178,24 @@ const getHarnessInternal = async (
44
178
  }
45
179
 
46
180
  try {
47
- await pRetry(
48
- () =>
49
- new Promise<void>((resolve, reject) => {
50
- signal.addEventListener('abort', () => {
51
- reject(new DOMException('The operation was aborted', 'AbortError'));
52
- });
53
-
54
- serverBridge.once('ready', () => resolve());
55
- platformInstance.restartApp().catch(reject);
56
- }),
57
- {
58
- minTimeout: BRIDGE_READY_TIMEOUT,
59
- maxTimeout: BRIDGE_READY_TIMEOUT,
60
- retries: Infinity,
61
- signal,
62
- }
63
- );
181
+ await prewarmMetroBundle({
182
+ projectRoot,
183
+ entryPoint: config.entryPoint,
184
+ platform: platform.platformId,
185
+ dev: true,
186
+ minify: false,
187
+ signal,
188
+ });
189
+ logMetroPrewarmCompleted(platform);
190
+
191
+ await waitForAppReady({
192
+ metroEvents: metroInstance.events,
193
+ serverBridge,
194
+ platformInstance: platformInstance as HarnessPlatformRunner,
195
+ bundleStartTimeout: config.bundleStartTimeout,
196
+ maxRestarts: config.maxAppRestarts,
197
+ signal,
198
+ });
64
199
  } catch (error) {
65
200
  await dispose();
66
201
  throw error;
@@ -68,11 +203,16 @@ const getHarnessInternal = async (
68
203
 
69
204
  const restart = () =>
70
205
  new Promise<void>((resolve, reject) => {
71
- serverBridge.once('ready', () => resolve());
206
+ crashMonitor.markIntentionalRestart();
207
+ serverBridge.once('ready', () => {
208
+ crashMonitor.clearIntentionalRestart();
209
+ resolve();
210
+ });
72
211
  platformInstance.restartApp().catch(reject);
73
212
  });
74
213
 
75
214
  return {
215
+ context,
76
216
  runTests: async (path, options) => {
77
217
  const client = serverBridge.rpc.clients.at(-1);
78
218
 
@@ -80,10 +220,14 @@ const getHarnessInternal = async (
80
220
  throw new Error('No client found');
81
221
  }
82
222
 
83
- return await client.runTests(path, options);
223
+ return await client.runTests(path, {
224
+ ...options,
225
+ runner: platform.runner,
226
+ });
84
227
  },
85
228
  restart,
86
229
  dispose,
230
+ crashMonitor,
87
231
  };
88
232
  };
89
233
 
package/src/index.ts CHANGED
@@ -16,7 +16,8 @@ import { setup } from './setup.js';
16
16
  import { teardown } from './teardown.js';
17
17
  import { HarnessError } from '@react-native-harness/tools';
18
18
  import { getErrorMessage } from './logs.js';
19
- import { DeviceNotRespondingError } from '@react-native-harness/bridge';
19
+ import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
20
+ import { NativeCrashError } from './errors.js';
20
21
 
21
22
  class CancelRun extends Error {
22
23
  constructor(message?: string) {
@@ -104,17 +105,52 @@ export default class JestHarness implements CallbackTestRunnerInterface {
104
105
  }
105
106
  isFirstTest = false;
106
107
 
107
- return onStart(test).then(() =>
108
- runHarnessTestFile({
109
- testPath: test.path,
110
- harness,
111
- globalConfig: this.#globalConfig,
112
- projectConfig: test.context.config,
113
- })
114
- );
108
+ return onStart(test).then(async () => {
109
+ if (!harnessConfig.detectNativeCrashes) {
110
+ return runHarnessTestFile({
111
+ testPath: test.path,
112
+ harness,
113
+ globalConfig: this.#globalConfig,
114
+ projectConfig: test.context.config,
115
+ });
116
+ }
117
+
118
+ // Start crash monitoring
119
+ const crashPromise = harness.crashMonitor.startMonitoring(
120
+ test.path
121
+ );
122
+
123
+ try {
124
+ const result = await Promise.race([
125
+ runHarnessTestFile({
126
+ testPath: test.path,
127
+ harness,
128
+ globalConfig: this.#globalConfig,
129
+ projectConfig: test.context.config,
130
+ }),
131
+ crashPromise,
132
+ ]);
133
+
134
+ return result;
135
+ } finally {
136
+ harness.crashMonitor.stopMonitoring();
137
+ }
138
+ });
115
139
  })
116
140
  .then((result) => onResult(test, result))
117
- .catch((err) => {
141
+ .catch(async (err) => {
142
+ if (err instanceof NativeCrashError) {
143
+ onFailure(test, {
144
+ message: err.message,
145
+ stack: '',
146
+ });
147
+
148
+ // Restart the app for the next test file
149
+ await harness.restart();
150
+
151
+ return;
152
+ }
153
+
118
154
  if (err instanceof DeviceNotRespondingError) {
119
155
  onFailure(test, {
120
156
  message: err.message,
package/src/logs.ts CHANGED
@@ -25,6 +25,12 @@ export const logTestEnvironmentReady = (runner: HarnessPlatform): void => {
25
25
  log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
26
26
  };
27
27
 
28
+ export const logMetroPrewarmCompleted = (runner: HarnessPlatform): void => {
29
+ log(
30
+ `${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n`
31
+ );
32
+ };
33
+
28
34
  export const getErrorMessage = (error: HarnessError): string => {
29
35
  return `${ERROR_TAG} ${error.message}\n`;
30
36
  };
package/src/run.ts CHANGED
@@ -88,6 +88,7 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
88
88
  testNamePattern: globalConfig.testNamePattern,
89
89
  setupFiles,
90
90
  setupFilesAfterEnv,
91
+ runner: harness.context.platform.runner,
91
92
  });
92
93
  const end = Date.now();
93
94
 
package/src/setup.ts CHANGED
@@ -68,6 +68,14 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
68
68
  // This is going to be used by @react-native-harness/babel-preset
69
69
  // to enable instrumentation of test files.
70
70
  process.env.RN_HARNESS_COLLECT_COVERAGE = 'true';
71
+
72
+ if (harnessConfig.coverage?.root) {
73
+ process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverage.root;
74
+ }
75
+ }
76
+
77
+ if (harnessConfig.disableViewFlattening) {
78
+ process.env.RN_HARNESS_VIEW_FLATTENING = 'false';
71
79
  }
72
80
 
73
81
  logTestRunHeader(selectedRunner);
package/tsconfig.json CHANGED
@@ -6,9 +6,6 @@
6
6
  {
7
7
  "path": "../cli"
8
8
  },
9
- {
10
- "path": "../bundler-metro"
11
- },
12
9
  {
13
10
  "path": "../tools"
14
11
  },
@@ -18,6 +15,9 @@
18
15
  {
19
16
  "path": "../config"
20
17
  },
18
+ {
19
+ "path": "../bundler-metro"
20
+ },
21
21
  {
22
22
  "path": "../bridge"
23
23
  },
package/vite.config.ts ADDED
@@ -0,0 +1,18 @@
1
+ /// <reference types='vitest' />
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig(() => ({
5
+ root: __dirname,
6
+ cacheDir: '../../node_modules/.vite/packages/jest',
7
+ test: {
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ reporters: ['default'],
13
+ coverage: {
14
+ reportsDirectory: './test-output/vitest/coverage',
15
+ provider: 'v8' as const,
16
+ },
17
+ },
18
+ }));