@react-native-harness/jest 1.0.0-alpha.20 → 1.0.0-alpha.21

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.
@@ -0,0 +1,16 @@
1
+ import { HarnessPlatformRunner } from '@react-native-harness/platforms';
2
+ import { BridgeServer } from '@react-native-harness/bridge/server';
3
+ export type CrashMonitor = {
4
+ startMonitoring(testFilePath: string): Promise<never>;
5
+ stopMonitoring(): void;
6
+ markIntentionalRestart(): void;
7
+ clearIntentionalRestart(): void;
8
+ dispose(): void;
9
+ };
10
+ export type CrashMonitorOptions = {
11
+ interval: number;
12
+ platformRunner: HarnessPlatformRunner;
13
+ bridgeServer: BridgeServer;
14
+ };
15
+ export declare const createCrashMonitor: ({ interval, platformRunner, bridgeServer, }: CrashMonitorOptions) => CrashMonitor;
16
+ //# sourceMappingURL=crash-monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAInE,MAAM,MAAM,YAAY,GAAG;IACzB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACtD,cAAc,IAAI,IAAI,CAAC;IACvB,sBAAsB,IAAI,IAAI,CAAC;IAC/B,uBAAuB,IAAI,IAAI,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,qBAAqB,CAAC;IACtC,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,6CAIhC,mBAAmB,KAAG,YAqFxB,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { NativeCrashError } from './errors.js';
2
+ import { logger } from '@react-native-harness/tools';
3
+ export const createCrashMonitor = ({ interval, platformRunner, bridgeServer, }) => {
4
+ let pollingInterval = null;
5
+ let isIntentionalRestart = false;
6
+ let currentTestFilePath = null;
7
+ let rejectFn = null;
8
+ const handleDisconnect = () => {
9
+ // Verify if it's actually a crash by checking if app is still running
10
+ if (!isIntentionalRestart && currentTestFilePath) {
11
+ // Capture the value to avoid it being null when setTimeout callback runs
12
+ const testFilePath = currentTestFilePath;
13
+ logger.debug('Bridge disconnected, checking if app crashed');
14
+ // Use a slight delay to allow the OS to clean up the process
15
+ setTimeout(async () => {
16
+ const isRunning = await platformRunner.isAppRunning();
17
+ if (!isRunning && !isIntentionalRestart && rejectFn) {
18
+ logger.debug(`Native crash detected during: ${testFilePath}`);
19
+ rejectFn(new NativeCrashError(testFilePath));
20
+ }
21
+ }, 100);
22
+ }
23
+ };
24
+ const startMonitoring = (testFilePath) => {
25
+ currentTestFilePath = testFilePath;
26
+ return new Promise((_, reject) => {
27
+ rejectFn = reject;
28
+ // Listen for bridge disconnect as early indicator
29
+ bridgeServer.on('disconnect', handleDisconnect);
30
+ // Poll for app running status
31
+ pollingInterval = setInterval(async () => {
32
+ // Skip check during intentional restarts
33
+ if (isIntentionalRestart) {
34
+ return;
35
+ }
36
+ try {
37
+ const isRunning = await platformRunner.isAppRunning();
38
+ if (!isRunning && currentTestFilePath) {
39
+ logger.debug(`Native crash detected during: ${currentTestFilePath}`);
40
+ stopMonitoring();
41
+ reject(new NativeCrashError(currentTestFilePath));
42
+ }
43
+ }
44
+ catch (error) {
45
+ logger.error('Error checking app status:', error);
46
+ }
47
+ }, interval);
48
+ });
49
+ };
50
+ const stopMonitoring = () => {
51
+ if (pollingInterval) {
52
+ clearInterval(pollingInterval);
53
+ pollingInterval = null;
54
+ }
55
+ bridgeServer.off('disconnect', handleDisconnect);
56
+ currentTestFilePath = null;
57
+ rejectFn = null;
58
+ };
59
+ const markIntentionalRestart = () => {
60
+ isIntentionalRestart = true;
61
+ };
62
+ const clearIntentionalRestart = () => {
63
+ isIntentionalRestart = false;
64
+ };
65
+ const dispose = () => {
66
+ stopMonitoring();
67
+ };
68
+ return {
69
+ startMonitoring,
70
+ stopMonitoring,
71
+ markIntentionalRestart,
72
+ clearIntentionalRestart,
73
+ dispose,
74
+ };
75
+ };
package/dist/errors.d.ts CHANGED
@@ -9,4 +9,12 @@ export declare class RunnerNotFoundError extends HarnessError {
9
9
  export declare class InitializationTimeoutError extends HarnessError {
10
10
  constructor();
11
11
  }
12
+ export declare class MaxAppRestartsError extends HarnessError {
13
+ constructor(attempts: number);
14
+ }
15
+ export declare class NativeCrashError extends HarnessError {
16
+ readonly testFilePath: string;
17
+ readonly lastKnownTest?: string | undefined;
18
+ constructor(testFilePath: string, lastKnownTest?: string | undefined);
19
+ }
12
20
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,mBAAoB,SAAQ,YAAY;gBACvC,QAAQ,EAAE,MAAM;CAO7B;AAED,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,aAAa,CAAC,EAAE,MAAM;gBADtB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,YAAA;CAKzC"}
package/dist/errors.js CHANGED
@@ -19,3 +19,20 @@ export class InitializationTimeoutError extends HarnessError {
19
19
  this.name = 'InitializationTimeoutError';
20
20
  }
21
21
  }
22
+ export class MaxAppRestartsError extends HarnessError {
23
+ constructor(attempts) {
24
+ super(`App failed to start after ${attempts} attempts. ` +
25
+ `No bundling activity detected within timeout period.`);
26
+ this.name = 'MaxAppRestartsError';
27
+ }
28
+ }
29
+ export class NativeCrashError extends HarnessError {
30
+ testFilePath;
31
+ lastKnownTest;
32
+ constructor(testFilePath, lastKnownTest) {
33
+ super('The native app crashed during test execution.');
34
+ this.testFilePath = testFilePath;
35
+ this.lastKnownTest = lastKnownTest;
36
+ this.name = 'NativeCrashError';
37
+ }
38
+ }
package/dist/harness.d.ts CHANGED
@@ -1,10 +1,22 @@
1
+ import { BridgeServer } from '@react-native-harness/bridge/server';
1
2
  import { BridgeClientFunctions } from '@react-native-harness/bridge';
2
- import { HarnessPlatform } from '@react-native-harness/platforms';
3
+ import { HarnessPlatform, HarnessPlatformRunner } from '@react-native-harness/platforms';
4
+ import { Reporter } from '@react-native-harness/bundler-metro';
3
5
  import { Config as HarnessConfig } from '@react-native-harness/config';
6
+ import { CrashMonitor } from './crash-monitor.js';
4
7
  export type Harness = {
5
8
  runTests: BridgeClientFunctions['runTests'];
6
9
  restart: () => Promise<void>;
7
10
  dispose: () => Promise<void>;
11
+ crashMonitor: CrashMonitor;
8
12
  };
13
+ export declare const waitForAppReady: (options: {
14
+ metroEvents: Reporter;
15
+ serverBridge: BridgeServer;
16
+ platformInstance: HarnessPlatformRunner;
17
+ bundleStartTimeout: number;
18
+ maxRestarts: number;
19
+ signal: AbortSignal;
20
+ }) => Promise<void>;
9
21
  export declare const getHarness: (config: HarnessConfig, platform: HarnessPlatform, projectRoot: string) => Promise<Harness>;
10
22
  //# sourceMappingURL=harness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAGlE,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAKvE,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AA6EF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAkBjB,CAAC"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EACb,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EACL,eAAe,EACf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,QAAQ,EAET,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAsB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEtE,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,SAAS;IAC7C,WAAW,EAAE,QAAQ,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;CACrB,KAAG,OAAO,CAAC,IAAI,CAgFf,CAAC;AA+EF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAkBjB,CAAC"}
package/dist/harness.js CHANGED
@@ -1,8 +1,70 @@
1
- import { getBridgeServer } from '@react-native-harness/bridge/server';
2
- import { getMetroInstance } from '@react-native-harness/bundler-metro';
3
- import { InitializationTimeoutError } from './errors.js';
4
- import pRetry from 'p-retry';
5
- const BRIDGE_READY_TIMEOUT = 10000;
1
+ import { getBridgeServer, } from '@react-native-harness/bridge/server';
2
+ import { getMetroInstance, } from '@react-native-harness/bundler-metro';
3
+ import { InitializationTimeoutError, MaxAppRestartsError } from './errors.js';
4
+ import { createCrashMonitor } from './crash-monitor.js';
5
+ export const waitForAppReady = async (options) => {
6
+ const { metroEvents, serverBridge, platformInstance, bundleStartTimeout, maxRestarts, signal, } = options;
7
+ let restartCount = 0;
8
+ let isBundling = false;
9
+ let bundleTimeoutId = null;
10
+ const clearBundleTimeout = () => {
11
+ if (bundleTimeoutId) {
12
+ clearTimeout(bundleTimeoutId);
13
+ bundleTimeoutId = null;
14
+ }
15
+ };
16
+ return new Promise((resolve, reject) => {
17
+ // Handle abort signal
18
+ signal.addEventListener('abort', () => {
19
+ clearBundleTimeout();
20
+ reject(new DOMException('The operation was aborted', 'AbortError'));
21
+ });
22
+ // Start/restart the bundle timeout
23
+ const startBundleTimeout = () => {
24
+ clearBundleTimeout();
25
+ bundleTimeoutId = setTimeout(() => {
26
+ if (isBundling)
27
+ return; // Don't restart while bundling
28
+ if (restartCount >= maxRestarts) {
29
+ cleanup();
30
+ reject(new MaxAppRestartsError(restartCount + 1));
31
+ return;
32
+ }
33
+ restartCount++;
34
+ platformInstance.restartApp().catch(reject);
35
+ startBundleTimeout(); // Reset timer for next attempt
36
+ }, bundleStartTimeout);
37
+ };
38
+ // Metro event listener
39
+ const onMetroEvent = (event) => {
40
+ if (event.type === 'bundle_build_started') {
41
+ isBundling = true;
42
+ clearBundleTimeout(); // Cancel restart timer while bundling
43
+ }
44
+ else if (event.type === 'bundle_build_done' ||
45
+ event.type === 'bundle_build_failed') {
46
+ isBundling = false;
47
+ startBundleTimeout(); // Reset timer after bundle completes
48
+ }
49
+ };
50
+ // Bridge ready listener
51
+ const onReady = () => {
52
+ cleanup();
53
+ resolve();
54
+ };
55
+ const cleanup = () => {
56
+ clearBundleTimeout();
57
+ metroEvents.removeListener(onMetroEvent);
58
+ serverBridge.off('ready', onReady);
59
+ };
60
+ // Setup listeners
61
+ metroEvents.addListener(onMetroEvent);
62
+ serverBridge.once('ready', onReady);
63
+ // Start the app and timeout
64
+ platformInstance.restartApp().catch(reject);
65
+ startBundleTimeout();
66
+ });
67
+ };
6
68
  const getHarnessInternal = async (config, platform, projectRoot, signal) => {
7
69
  const [metroInstance, platformInstance, serverBridge] = await Promise.all([
8
70
  getMetroInstance({ projectRoot, harnessConfig: config }, signal),
@@ -19,21 +81,22 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
19
81
  metroInstance.dispose(),
20
82
  ]);
21
83
  };
84
+ const crashMonitor = createCrashMonitor({
85
+ interval: config.crashDetectionInterval,
86
+ platformRunner: platformInstance,
87
+ bridgeServer: serverBridge,
88
+ });
22
89
  if (signal.aborted) {
23
90
  await dispose();
24
91
  throw new DOMException('The operation was aborted', 'AbortError');
25
92
  }
26
93
  try {
27
- await pRetry(() => new Promise((resolve, reject) => {
28
- signal.addEventListener('abort', () => {
29
- reject(new DOMException('The operation was aborted', 'AbortError'));
30
- });
31
- serverBridge.once('ready', () => resolve());
32
- platformInstance.restartApp().catch(reject);
33
- }), {
34
- minTimeout: BRIDGE_READY_TIMEOUT,
35
- maxTimeout: BRIDGE_READY_TIMEOUT,
36
- retries: Infinity,
94
+ await waitForAppReady({
95
+ metroEvents: metroInstance.events,
96
+ serverBridge,
97
+ platformInstance: platformInstance,
98
+ bundleStartTimeout: config.bundleStartTimeout,
99
+ maxRestarts: config.maxAppRestarts,
37
100
  signal,
38
101
  });
39
102
  }
@@ -42,7 +105,11 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
42
105
  throw error;
43
106
  }
44
107
  const restart = () => new Promise((resolve, reject) => {
45
- serverBridge.once('ready', () => resolve());
108
+ crashMonitor.markIntentionalRestart();
109
+ serverBridge.once('ready', () => {
110
+ crashMonitor.clearIntentionalRestart();
111
+ resolve();
112
+ });
46
113
  platformInstance.restartApp().catch(reject);
47
114
  });
48
115
  return {
@@ -55,6 +122,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
55
122
  },
56
123
  restart,
57
124
  dispose,
125
+ crashMonitor,
58
126
  };
59
127
  };
60
128
  export const getHarness = async (config, platform, projectRoot) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,MAAM,EACN,aAAa,EACb,WAAW,EACX,aAAa,EACb,IAAI,EACJ,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAc5C,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;gBAIb,YAAY,EAAE,MAAM,CAAC,YAAY;IAIvC,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;IAkCV,oBAAoB,CACxB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC;CA+CjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,MAAM,EACN,aAAa,EACb,WAAW,EACX,aAAa,EACb,IAAI,EACJ,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAe5C,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;gBAIb,YAAY,EAAE,MAAM,CAAC,YAAY;IAIvC,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;IAkCV,oBAAoB,CACxB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC;CAkFjB"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { teardown } from './teardown.js';
5
5
  import { HarnessError } from '@react-native-harness/tools';
6
6
  import { getErrorMessage } from './logs.js';
7
7
  import { DeviceNotRespondingError } from '@react-native-harness/bridge';
8
+ import { NativeCrashError } from './errors.js';
8
9
  class CancelRun extends Error {
9
10
  constructor(message) {
10
11
  super(message);
@@ -53,15 +54,45 @@ export default class JestHarness {
53
54
  await harness.restart();
54
55
  }
55
56
  isFirstTest = false;
56
- return onStart(test).then(() => runHarnessTestFile({
57
- testPath: test.path,
58
- harness,
59
- globalConfig: this.#globalConfig,
60
- projectConfig: test.context.config,
61
- }));
57
+ return onStart(test).then(async () => {
58
+ if (!harnessConfig.detectNativeCrashes) {
59
+ return runHarnessTestFile({
60
+ testPath: test.path,
61
+ harness,
62
+ globalConfig: this.#globalConfig,
63
+ projectConfig: test.context.config,
64
+ });
65
+ }
66
+ // Start crash monitoring
67
+ const crashPromise = harness.crashMonitor.startMonitoring(test.path);
68
+ try {
69
+ const result = await Promise.race([
70
+ runHarnessTestFile({
71
+ testPath: test.path,
72
+ harness,
73
+ globalConfig: this.#globalConfig,
74
+ projectConfig: test.context.config,
75
+ }),
76
+ crashPromise,
77
+ ]);
78
+ return result;
79
+ }
80
+ finally {
81
+ harness.crashMonitor.stopMonitoring();
82
+ }
83
+ });
62
84
  })
63
85
  .then((result) => onResult(test, result))
64
- .catch((err) => {
86
+ .catch(async (err) => {
87
+ if (err instanceof NativeCrashError) {
88
+ onFailure(test, {
89
+ message: err.message,
90
+ stack: '',
91
+ });
92
+ // Restart the app for the next test file
93
+ await harness.restart();
94
+ return;
95
+ }
65
96
  if (err instanceof DeviceNotRespondingError) {
66
97
  onFailure(test, {
67
98
  message: err.message,