@react-native-harness/jest 1.6.0 → 1.7.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 (82) hide show
  1. package/dist/cli-args.d.ts +3 -4
  2. package/dist/cli-args.d.ts.map +1 -1
  3. package/dist/cli-args.js +3 -8
  4. package/dist/diagnostics/index.d.ts +4 -1
  5. package/dist/diagnostics/index.d.ts.map +1 -1
  6. package/dist/diagnostics/index.js +2 -1
  7. package/dist/diagnostics/report.d.ts +81 -1
  8. package/dist/diagnostics/report.d.ts.map +1 -1
  9. package/dist/diagnostics/report.js +225 -3
  10. package/dist/diagnostics/telemetry.d.ts +42 -0
  11. package/dist/diagnostics/telemetry.d.ts.map +1 -0
  12. package/dist/diagnostics/telemetry.js +120 -0
  13. package/dist/errors.d.ts +8 -2
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +29 -3
  16. package/dist/execute-run.d.ts.map +1 -1
  17. package/dist/execute-run.js +73 -21
  18. package/dist/format-harness-error.d.ts.map +1 -1
  19. package/dist/format-harness-error.js +3 -0
  20. package/dist/harness-session.d.ts +8 -1
  21. package/dist/harness-session.d.ts.map +1 -1
  22. package/dist/harness-session.js +106 -22
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -7
  25. package/dist/jest-preset.d.ts +3 -0
  26. package/dist/jest-preset.d.ts.map +1 -1
  27. package/dist/jest-preset.js +18 -0
  28. package/dist/relative-path.d.ts +8 -0
  29. package/dist/relative-path.d.ts.map +1 -0
  30. package/dist/relative-path.js +8 -0
  31. package/dist/resource-lock.d.ts.map +1 -1
  32. package/dist/resource-lock.js +47 -27
  33. package/dist/run.d.ts.map +1 -1
  34. package/dist/run.js +4 -4
  35. package/dist/watch-session.d.ts +6 -0
  36. package/dist/watch-session.d.ts.map +1 -0
  37. package/dist/watch-session.js +37 -0
  38. package/package.json +8 -8
  39. package/src/cli-args.ts +5 -13
  40. package/src/diagnostics/index.ts +12 -1
  41. package/src/diagnostics/report.ts +326 -5
  42. package/src/diagnostics/telemetry.ts +186 -0
  43. package/src/errors.ts +42 -5
  44. package/src/execute-run.ts +93 -23
  45. package/src/format-harness-error.ts +5 -0
  46. package/src/harness-session.ts +139 -21
  47. package/src/index.ts +8 -7
  48. package/src/jest-preset.ts +18 -0
  49. package/src/relative-path.ts +10 -0
  50. package/src/resource-lock.ts +64 -33
  51. package/src/run.ts +4 -4
  52. package/src/watch-session.ts +65 -0
  53. package/dist/crash-supervisor.d.ts +0 -21
  54. package/dist/crash-supervisor.d.ts.map +0 -1
  55. package/dist/crash-supervisor.js +0 -210
  56. package/dist/event-mapper.d.ts +0 -16
  57. package/dist/event-mapper.d.ts.map +0 -1
  58. package/dist/event-mapper.js +0 -120
  59. package/dist/global-setup.d.ts +0 -3
  60. package/dist/global-setup.d.ts.map +0 -1
  61. package/dist/global-setup.js +0 -60
  62. package/dist/global-teardown.d.ts +0 -3
  63. package/dist/global-teardown.d.ts.map +0 -1
  64. package/dist/global-teardown.js +0 -11
  65. package/dist/harness-reporter.d.ts +0 -8
  66. package/dist/harness-reporter.d.ts.map +0 -1
  67. package/dist/harness-reporter.js +0 -12
  68. package/dist/harness.d.ts +0 -47
  69. package/dist/harness.d.ts.map +0 -1
  70. package/dist/harness.js +0 -619
  71. package/dist/jest-options.d.ts +0 -2
  72. package/dist/jest-options.d.ts.map +0 -1
  73. package/dist/jest-options.js +0 -1
  74. package/dist/setup.d.ts +0 -3
  75. package/dist/setup.d.ts.map +0 -1
  76. package/dist/setup.js +0 -80
  77. package/dist/teardown.d.ts +0 -3
  78. package/dist/teardown.d.ts.map +0 -1
  79. package/dist/teardown.js +0 -10
  80. package/dist/types.d.ts +0 -17
  81. package/dist/types.d.ts.map +0 -1
  82. package/dist/types.js +0 -1
package/dist/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- import { formatPath, HarnessError } from '@react-native-harness/tools';
1
+ import { formatHostPressure, formatPath, HarnessError, isHostStarved, } from '@react-native-harness/tools';
2
2
  export { StartupStallError, } from '@react-native-harness/bundler-metro';
3
3
  export class NoRunnerSpecifiedError extends HarnessError {
4
4
  constructor() {
@@ -20,13 +20,28 @@ export class InitializationTimeoutError extends HarnessError {
20
20
  this.name = 'InitializationTimeoutError';
21
21
  }
22
22
  }
23
+ const buildPlatformReadyTimeoutMessage = (timeout, hostPressure) => {
24
+ const prefix = `The platform did not become ready within ${timeout}ms.`;
25
+ if (hostPressure && isHostStarved(hostPressure)) {
26
+ return `${prefix} The host was starved for resources during startup (${formatHostPressure(hostPressure)}). A larger "platformReadyTimeout" is unlikely to help; reduce other load on the host or use a larger machine.`;
27
+ }
28
+ const suggestion = `Increase "platformReadyTimeout" if your device, simulator, or emulator needs more time to start.`;
29
+ return hostPressure
30
+ ? `${prefix} ${suggestion} Host during startup: ${formatHostPressure(hostPressure)}.`
31
+ : `${prefix} ${suggestion}`;
32
+ };
23
33
  export class PlatformReadyTimeoutError extends HarnessError {
24
34
  timeout;
25
- constructor(timeout) {
26
- super(`The platform did not become ready within ${timeout}ms. Increase "platformReadyTimeout" if your device, simulator, or emulator needs more time to start.`);
35
+ hostPressure;
36
+ constructor(timeout, hostPressure = null) {
37
+ super(buildPlatformReadyTimeoutMessage(timeout, hostPressure));
27
38
  this.timeout = timeout;
39
+ this.hostPressure = hostPressure;
28
40
  this.name = 'PlatformReadyTimeoutError';
29
41
  }
42
+ get hostStarved() {
43
+ return this.hostPressure !== null && isHostStarved(this.hostPressure);
44
+ }
30
45
  }
31
46
  export class MetroPortRangeExhaustedError extends HarnessError {
32
47
  initialPort;
@@ -128,3 +143,14 @@ export class RuntimeDisconnectError extends HarnessError {
128
143
  return this.details.phase;
129
144
  }
130
145
  }
146
+ // The runtime sends this name through the runTests RPC when its native host
147
+ // surface is replaced while a test file is still running.
148
+ export class HostSurfaceReplacedError extends HarnessError {
149
+ testFilePath;
150
+ constructor(testFilePath, cause) {
151
+ super(cause.message, { cause });
152
+ this.testFilePath = testFilePath;
153
+ this.name = 'HostSurfaceReplacedError';
154
+ this.stack = cause.stack;
155
+ }
156
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"execute-run.d.ts","sourceRoot":"","sources":["../src/execute-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,MAAM,EACN,UAAU,EACX,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AA8BjF,KAAK,aAAa,GAAG,CAAC,IAAI,SAAS,MAAM,UAAU,EACjD,SAAS,EAAE,IAAI,EACf,GAAG,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;AA+FnB,eAAO,MAAM,UAAU,GACrB,SAAS,cAAc,EACvB,OAAO,KAAK,CAAC,IAAI,CAAC,EAClB,SAAS,WAAW,EACpB,WAAW,aAAa,EACxB,cAAc,MAAM,CAAC,YAAY,KAChC,OAAO,CAAC,IAAI,CAsOd,CAAC"}
1
+ {"version":3,"file":"execute-run.d.ts","sourceRoot":"","sources":["../src/execute-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,MAAM,EACN,UAAU,EACX,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAsCjF,KAAK,aAAa,GAAG,CAAC,IAAI,SAAS,MAAM,UAAU,EACjD,SAAS,EAAE,IAAI,EACf,GAAG,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;AA8GnB,eAAO,MAAM,UAAU,GACrB,SAAS,cAAc,EACvB,OAAO,KAAK,CAAC,IAAI,CAAC,EAClB,SAAS,WAAW,EACpB,WAAW,aAAa,EACxB,cAAc,MAAM,CAAC,YAAY,KAChC,OAAO,CAAC,IAAI,CAsRd,CAAC"}
@@ -1,13 +1,13 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import path from 'node:path';
3
2
  import { runHarnessTestFile } from './run.js';
3
+ import { toRelativePosixPath } from './relative-path.js';
4
4
  import { resolveResetStrategyKind } from './environment-reset.js';
5
- import { NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
5
+ import { HostSurfaceReplacedError, NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
6
6
  import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
7
7
  import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './test-file-platform-filter.js';
8
8
  import { formatHarnessErrorMessage } from './format-harness-error.js';
9
9
  import { logger } from '@react-native-harness/tools';
10
- import { printSummary, writeTraceFile } from './diagnostics/index.js';
10
+ import { appendStepSummary, printSummary, summarizeRunKpis, writeSummaryFile, writeTraceFile, } from './diagnostics/index.js';
11
11
  import { toJestStatus } from './toJestStatus.js';
12
12
  const diagnosticsLogger = logger.child('diagnostics');
13
13
  const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
@@ -23,13 +23,22 @@ class CancelRun extends Error {
23
23
  this.name = 'CancelRun';
24
24
  }
25
25
  }
26
- const buildTestFailure = (err) => {
27
- if (err instanceof NativeCrashError ||
28
- err instanceof RuntimeDisconnectError ||
29
- err instanceof StartupStallError ||
30
- err instanceof AppBridgeDisconnectedError ||
31
- err instanceof DeviceNotRespondingError) {
32
- return { message: err.message, stack: '' };
26
+ const isRuntimeFailure = (err) => err instanceof NativeCrashError ||
27
+ err instanceof RuntimeDisconnectError ||
28
+ err instanceof StartupStallError ||
29
+ err instanceof AppBridgeDisconnectedError ||
30
+ err instanceof DeviceNotRespondingError;
31
+ const formatRunningTest = (test) => `Running test: "${[...test.ancestorTitles, test.name].join(' › ')}"`;
32
+ const buildTestFailure = (err, runningTest = null) => {
33
+ if (isRuntimeFailure(err)) {
34
+ if (!runningTest) {
35
+ return { message: err.message, stack: '' };
36
+ }
37
+ const [headline, ...details] = err.message.split('\n');
38
+ return {
39
+ message: [headline, formatRunningTest(runningTest), ...details].join('\n'),
40
+ stack: '',
41
+ };
33
42
  }
34
43
  return err;
35
44
  };
@@ -80,10 +89,19 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
80
89
  const startTime = Date.now();
81
90
  const watchMode = globalConfig.watch || globalConfig.watchAll;
82
91
  const rootDir = globalConfig.rootDir ?? process.cwd();
83
- const testFiles = tests.map((t) => path.relative(rootDir, t.path));
92
+ const testFiles = tests.map((t) => toRelativePosixPath(rootDir, t.path));
84
93
  const summary = createRunSummary();
85
94
  let caseEventChain = Promise.resolve();
95
+ // The device runs one test at a time, so a test that started and has not
96
+ // finished is the one the app was running if it crashes or disconnects.
97
+ let runningTest = null;
86
98
  const unsubscribe = session.onTestRunnerEvent((event) => {
99
+ if (event.type === 'test-started') {
100
+ runningTest = event;
101
+ }
102
+ else if (event.type === 'test-finished') {
103
+ runningTest = null;
104
+ }
87
105
  if (isHarnessCaseEvent(event)) {
88
106
  caseEventChain = caseEventChain.then(() => event.type === 'test-started'
89
107
  ? emitHarnessTestStarted(emitEvent, event)
@@ -106,6 +124,11 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
106
124
  return state;
107
125
  };
108
126
  updateRunState();
127
+ session.lifecycle.emit({
128
+ type: 'phase',
129
+ phase: 'run.start',
130
+ attrs: { runId },
131
+ });
109
132
  await session.callHook('run:started', {
110
133
  runId,
111
134
  startTime,
@@ -128,8 +151,9 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
128
151
  for (const test of tests) {
129
152
  if (watcher.isInterrupted())
130
153
  throw new CancelRun();
131
- const relativeTestPath = path.relative(rootDir, test.path);
154
+ const relativeTestPath = toRelativePosixPath(rootDir, test.path);
132
155
  const fileStartedAt = Date.now();
156
+ runningTest = null;
133
157
  let emittedTestFileFinished = false;
134
158
  const emitTestFileFinished = async (options) => {
135
159
  emittedTestFileFinished = true;
@@ -143,6 +167,11 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
143
167
  };
144
168
  await session.callHook('test-file:started', { runId, file: relativeTestPath });
145
169
  const fileSpan = diagnostics.start('run.file', { file: relativeTestPath });
170
+ session.lifecycle.emit({
171
+ type: 'phase',
172
+ phase: 'test-file.start',
173
+ attrs: { file: relativeTestPath },
174
+ });
146
175
  let fileStatus = 'failed';
147
176
  try {
148
177
  if (!shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)) {
@@ -216,7 +245,10 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
216
245
  shouldRestartAfterTimeout = false;
217
246
  }
218
247
  }
219
- catch (err) {
248
+ catch (error) {
249
+ const err = error instanceof Error && error.name === 'HostSurfaceReplacedError'
250
+ ? new HostSurfaceReplacedError(test.path, error)
251
+ : error;
220
252
  fileStatus = 'failed';
221
253
  if (!emittedTestFileFinished) {
222
254
  await emitTestFileFinished({
@@ -225,25 +257,31 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
225
257
  result: null,
226
258
  });
227
259
  }
228
- const isRuntimeFailure = err instanceof NativeCrashError ||
229
- err instanceof RuntimeDisconnectError ||
230
- err instanceof StartupStallError ||
231
- err instanceof AppBridgeDisconnectedError ||
232
- err instanceof DeviceNotRespondingError;
233
- if (isRuntimeFailure) {
260
+ const isFileRuntimeFailure = err instanceof HostSurfaceReplacedError || isRuntimeFailure(err);
261
+ if (isFileRuntimeFailure) {
234
262
  summary.failed += 1;
235
263
  updateRunState();
236
264
  }
237
265
  if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
238
266
  session.resetCrashState();
239
267
  }
240
- updateRunState({ error: isRuntimeFailure ? undefined : err });
268
+ updateRunState({ error: isFileRuntimeFailure ? undefined : err });
241
269
  await caseEventChain;
242
- await emitEvent('test-file-failure', test, buildTestFailure(err));
270
+ await emitEvent('test-file-failure', test, buildTestFailure(err, runningTest));
271
+ if (err instanceof HostSurfaceReplacedError) {
272
+ // Abandoned async test work may still be running in this process.
273
+ await session.restartApp(test.path);
274
+ isFirstTest = true;
275
+ }
243
276
  }
244
277
  }
245
278
  finally {
246
279
  fileSpan.end({ status: fileStatus });
280
+ session.lifecycle.emit({
281
+ type: 'phase',
282
+ phase: 'test-file.end',
283
+ attrs: { file: relativeTestPath, status: fileStatus },
284
+ });
247
285
  }
248
286
  }
249
287
  }
@@ -267,12 +305,26 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
267
305
  await caseEventChain;
268
306
  unsubscribe();
269
307
  runSpan.end({ runId, status: finalStatus });
308
+ // Idle time between runs (watch mode) is not part of any run's metrics.
309
+ session.lifecycle.emit({
310
+ type: 'phase',
311
+ phase: 'run.end',
312
+ attrs: { runId, status: finalStatus },
313
+ });
270
314
  if (session.diagnostics.enabled) {
271
315
  const entries = session.diagnostics.flush();
272
316
  try {
273
317
  printSummary(entries, (line) => diagnosticsLogger.info(line));
274
318
  const tracePath = await writeTraceFile(entries, { projectRoot: rootDir, runId });
275
319
  diagnosticsLogger.info(`wrote diagnostics trace to ${tracePath}`);
320
+ const kpis = summarizeRunKpis(entries);
321
+ const summaryPath = await writeSummaryFile(kpis, {
322
+ projectRoot: rootDir,
323
+ runId,
324
+ sessionId: session.sessionId,
325
+ });
326
+ diagnosticsLogger.info(`wrote diagnostics summary to ${summaryPath}`);
327
+ await appendStepSummary(kpis);
276
328
  }
277
329
  catch (reportError) {
278
330
  diagnosticsLogger.warn('failed to write diagnostics report: %s', reportError);
@@ -1 +1 @@
1
- {"version":3,"file":"format-harness-error.d.ts","sourceRoot":"","sources":["../src/format-harness-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA6CpE,eAAO,MAAM,yBAAyB,GACpC,OAAO,eAAe,GAAG,SAAS,EAClC,UAAS;IACP,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,KACL,MAAM,GAAG,SAsBX,CAAC"}
1
+ {"version":3,"file":"format-harness-error.d.ts","sourceRoot":"","sources":["../src/format-harness-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA6CpE,eAAO,MAAM,yBAAyB,GACpC,OAAO,eAAe,GAAG,SAAS,EAClC,UAAS;IACP,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,KACL,MAAM,GAAG,SA2BX,CAAC"}
@@ -30,6 +30,9 @@ export const formatHarnessErrorMessage = (error, options = {}) => {
30
30
  return undefined;
31
31
  }
32
32
  const parts = [error.message];
33
+ if (error.diff) {
34
+ parts.push(error.diff);
35
+ }
33
36
  const pendingPromiseDetails = error.diagnostics?.pendingPromises
34
37
  ? formatPendingPromises(error.diagnostics.pendingPromises, options.testStartedAt)
35
38
  : null;
@@ -2,12 +2,13 @@ import { type HarnessBridge, type AppConnection } from '@react-native-harness/br
2
2
  import { type HarnessContext, type TestRunnerEvents, type TestExecutionOptions, type TestSuiteResult } from '@react-native-harness/bridge';
3
3
  import { type HarnessPlatform } from '@react-native-harness/platforms';
4
4
  import { type HarnessPluginManager, type HarnessRunStatus, type HarnessRunSummary } from '@react-native-harness/plugins';
5
- import { type Diagnostics } from '@react-native-harness/tools';
5
+ import { type Diagnostics, type HostPressure } from '@react-native-harness/tools';
6
6
  import { type Config as HarnessConfig } from '@react-native-harness/config';
7
7
  import type { Config as JestConfig } from 'jest-runner';
8
8
  import { type CrashMonitor } from './crash-monitor.js';
9
9
  import { type ClientLogBuffer } from './client-log-handler.js';
10
10
  import { type ResourceLockManager } from './resource-lock.js';
11
+ import { type SessionTelemetry } from './diagnostics/index.js';
11
12
  type DisconnectObservableBridge = {
12
13
  readonly connection: AppConnection | null;
13
14
  on: (event: 'disconnected', listener: () => void) => void;
@@ -38,6 +39,10 @@ export type HarnessSession = {
38
39
  readonly config: HarnessConfig;
39
40
  readonly context: HarnessContext;
40
41
  readonly diagnostics: Diagnostics;
42
+ /** Identifies this session; names its incremental diagnostics log. */
43
+ readonly sessionId: string;
44
+ /** Run lifecycle phases, for telemetry probes (see `SessionTelemetry`). */
45
+ readonly lifecycle: SessionTelemetry['lifecycle'];
41
46
  onTestRunnerEvent: (listener: (event: TestRunnerEvents) => void) => () => void;
42
47
  runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
43
48
  ensureAppReady: (testFilePath: string) => Promise<void>;
@@ -54,6 +59,8 @@ export declare const withPlatformReadyTimeout: <T>(options: {
54
59
  timeout: number;
55
60
  signal: AbortSignal;
56
61
  work: (signal: AbortSignal) => Promise<T>;
62
+ /** Host pressure at the moment of the timeout, attached to the error. */
63
+ getHostPressure?: () => HostPressure | null;
57
64
  }) => Promise<T>;
58
65
  export declare const waitForStartupCrash: ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }: {
59
66
  crashMonitor: CrashMonitor;
@@ -1 +1 @@
1
- {"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,iCAAiC,CAAC;AASzC,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAaL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAIL,KAAK,MAAM,IAAI,aAAa,EAE7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAKxD,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQ3E,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AA0ChC,KAAK,0BAA0B,GAAG;IAChC,QAAQ,CAAC,UAAU,EAAE,aAAa,GAAG,IAAI,CAAC;IAC1C,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC1D,GAAG,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC5D,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAAU,oCAIpD;IACD,MAAM,EAAE,0BAA0B,CAAC;IACnC,UAAU,EAAE,aAAa,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB,KAAG,OAAO,CAAC,OAAO,CAuBlB,CAAC;AAKF,eAAO,MAAM,oBAAoB,GAAI,qBAGlC;IACD,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC;IAC1C,MAAM,EAAE,WAAW,CAAC;CACrB,KAAG,OAAO,CAAC,IAAI,CAWZ,CAAC;AAEL,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAClD,iBAAiB,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/E,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACzF,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,UAAU,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,gBAAgB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,eAAe,EAAE,MAAM,eAAe,CAAC;IACvC,QAAQ,EAAE,oBAAoB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC;IAC3E,WAAW,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,CAAC;AAEF,eAAO,MAAM,4BAA4B,GACvC,OAAO,eAAe,GAAG,IAAI,KAC5B,MAWF,CAAC;AAYF,eAAO,MAAM,wBAAwB,GAAU,CAAC,EAAE,SAAS;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1C,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,YAAY,GAAG,IAAI,CAAC;CAC7C,KAAG,OAAO,CAAC,CAAC,CAgBZ,CAAC;AA+CF,eAAO,MAAM,mBAAmB,GAAU,8DAKvC;IACD,YAAY,EAAE,YAAY,CAAC;IAC3B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;CACrB,mBAcA,CAAC;AA4HF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS;IAChD,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,4BAA4B,CAAC,CAAC;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,eAAe,CAAC;CAC3B,KAAG,IAUH,CAAC;AA8CF,eAAO,MAAM,oBAAoB,GAC/B,cAAc,UAAU,CAAC,YAAY,EACrC,kBAA8C;IAAE,WAAW,CAAC,EAAE,mBAAmB,CAAA;CAAO,KACvF,OAAO,CAAC,cAAc,CA8uBxB,CAAC"}
@@ -3,7 +3,7 @@ import { HARNESS_BRIDGE_PATH, } from '@react-native-harness/bridge';
3
3
  import { createHarnessCache } from '@react-native-harness/cache';
4
4
  import { getMetroInstance, waitForMetroBackedAppReady, } from '@react-native-harness/bundler-metro';
5
5
  import { createHarnessPluginManager, } from '@react-native-harness/plugins';
6
- import { createCrashArtifactWriter, createDiagnostics, delay, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } from '@react-native-harness/tools';
6
+ import { createCrashArtifactWriter, createDiagnostics, delay, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, checkHostHealth, formatGitHubWarning, formatSaturatedHostMessage, isCI, readProcessUsage, } from '@react-native-harness/tools';
7
7
  import { getConfig, isDiagnosticsEnabled, resolveMetroCacheEnabled, ConfigSchema, } from '@react-native-harness/config';
8
8
  import { preRunMessage } from 'jest-util';
9
9
  import path from 'node:path';
@@ -15,7 +15,7 @@ import { createHookQueue } from './hook-queue.js';
15
15
  import { createClientLogCollector, } from './client-log-handler.js';
16
16
  import { createActionHooksPlugin } from './action-hooks.js';
17
17
  import { createResourceLockManager, } from './resource-lock.js';
18
- import { observeBridgeEvents, observeMetroEvents } from './diagnostics/index.js';
18
+ import { observeBridgeEvents, observeMetroEvents, startSessionTelemetry, } from './diagnostics/index.js';
19
19
  import { resolveHarnessMetroPort } from './metro-port.js';
20
20
  import { getAdditionalCliArgs } from './cli-args.js';
21
21
  import { logMetroCacheReused, logMetroPortFallback, logNativeCoverageCollected, logRunnerStarting, logRunnerStillWaitingInQueue, logRunnerWaitingInQueue, logTestEnvironmentReady, logTestRunHeader, } from './logs.js';
@@ -25,6 +25,8 @@ const ignorePromiseRejection = () => undefined;
25
25
  const isBridgeDisconnectError = (error) => error instanceof Error && error.message === 'App bridge disconnected';
26
26
  const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
27
27
  const DISPOSE_HOOK_TIMEOUT_MS = 5_000;
28
+ // Upper bound on waiting for end-of-run telemetry before tearing resources down.
29
+ const DISPOSE_TELEMETRY_TIMEOUT_MS = 10_000;
28
30
  const waitForBoundedSettlement = async (promise, timeoutMs) => {
29
31
  const timeout = delay(timeoutMs);
30
32
  try {
@@ -99,7 +101,7 @@ export const withPlatformReadyTimeout = async (options) => {
99
101
  return await Promise.race([
100
102
  options.work(options.signal),
101
103
  abortWait.promise.catch(() => {
102
- throw new PlatformReadyTimeoutError(options.timeout);
104
+ throw new PlatformReadyTimeoutError(options.timeout, options.getHostPressure?.() ?? null);
103
105
  }),
104
106
  ]);
105
107
  }
@@ -107,6 +109,35 @@ export const withPlatformReadyTimeout = async (options) => {
107
109
  abortWait.cancel();
108
110
  }
109
111
  };
112
+ // A saturated host is exactly where `ps` is slow; it took over 10s on CI.
113
+ const HOST_HEALTH_PROCESS_LISTING_TIMEOUT_MS = 30_000;
114
+ // Tells, before anything is booted, whether the host is already saturated by
115
+ // something other than this run. Only a warning: a saturated runner can still
116
+ // pass, but a later startup timeout then has an explanation.
117
+ const reportSaturatedHost = async (diagnostics) => {
118
+ try {
119
+ const verdict = await checkHostHealth();
120
+ diagnostics.mark('telemetry.host_health', {
121
+ load1: verdict.load1,
122
+ cpuCount: verdict.cpuCount,
123
+ idlePct: verdict.idlePct ?? undefined,
124
+ saturated: verdict.saturated,
125
+ });
126
+ if (!verdict.saturated) {
127
+ return;
128
+ }
129
+ const processes = await readProcessUsage(AbortSignal.timeout(HOST_HEALTH_PROCESS_LISTING_TIMEOUT_MS)).catch(() => null);
130
+ const message = formatSaturatedHostMessage(verdict, processes?.topByCpu);
131
+ sessionLogger.warn(message);
132
+ const annotation = formatGitHubWarning('Saturated CI runner', message);
133
+ if (annotation) {
134
+ process.stdout.write(`${annotation}\n`);
135
+ }
136
+ }
137
+ catch (error) {
138
+ sessionLogger.debug('host health check failed: %s', error);
139
+ }
140
+ };
110
141
  export const waitForStartupCrash = async ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }) => {
111
142
  if (!detectNativeCrashes) {
112
143
  return await waitForAbort(signal).promise;
@@ -268,6 +299,23 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
268
299
  label: 'session.config.load',
269
300
  duration: Date.now() - setupStartedAt,
270
301
  });
302
+ // The one composition point for run telemetry. Everything below only emits
303
+ // lifecycle phases; probes translate them and polled data into diagnostics.
304
+ const telemetry = startSessionTelemetry({
305
+ diagnostics,
306
+ config: harnessConfig,
307
+ projectRoot,
308
+ });
309
+ telemetry.phase('session.created');
310
+ if (isCI()) {
311
+ void reportSaturatedHost(diagnostics);
312
+ }
313
+ // Telemetry runs timers and listens on `exit`, so a setup failure before the
314
+ // main try/catch below must still stop it.
315
+ const disposeTelemetryOnFailure = (work) => work.catch(async (error) => {
316
+ await telemetry.dispose();
317
+ throw error;
318
+ });
271
319
  sessionLogger.debug('creating session for runner=%s platform=%s', platform.name, platform.platformId);
272
320
  // Single, session-lifetime AbortController: it aborts on early SIGTERM/SIGINT
273
321
  // during setup (below) and, once the session is up, in dispose()'s finally
@@ -283,11 +331,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
283
331
  };
284
332
  process.once('SIGTERM', onEarlySignal);
285
333
  process.once('SIGINT', onEarlySignal);
286
- const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
334
+ const resourceLockKey = await disposeTelemetryOnFailure(Promise.resolve(platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform)));
287
335
  let didWaitForResourceLock = false;
288
336
  let lastStillWaitingLogAt = 0;
289
337
  logTestRunHeader(platform);
290
- const resourceLease = await diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
338
+ const resourceLease = await disposeTelemetryOnFailure(diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
291
339
  signal: sessionController.signal,
292
340
  onWait: () => {
293
341
  didWaitForResourceLock = true;
@@ -301,7 +349,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
301
349
  logRunnerStillWaitingInQueue(platform);
302
350
  sessionLogger.debug('still waiting in queue for runner=%s key=%s elapsedMs=%d', platform.name, resourceLockKey, elapsedMs);
303
351
  },
304
- }), { lockKey: resourceLockKey });
352
+ }), { lockKey: resourceLockKey }));
305
353
  if (didWaitForResourceLock)
306
354
  logRunnerStarting(platform);
307
355
  sessionLogger.debug('resource lock acquired for runner=%s key=%s', platform.name, resourceLockKey);
@@ -347,7 +395,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
347
395
  let currentRun = null;
348
396
  const getCurrentRunId = () => currentRun?.runId;
349
397
  const clientLogCollector = createClientLogCollector();
350
- const context = { platform };
398
+ const context = {
399
+ platform,
400
+ updateSnapshot: globalConfig.updateSnapshot,
401
+ };
351
402
  const crashArtifactWriter = createCrashArtifactWriter({
352
403
  runnerName: platform.name,
353
404
  platformId: platform.platformId,
@@ -357,6 +408,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
357
408
  noServer: true,
358
409
  timeout: runtimeConfig.bridgeTimeout,
359
410
  context,
411
+ liveness: {
412
+ // Loading a test file runs user code that has no timeout of its
413
+ // own, so it gets the same allowance as starting the bundle.
414
+ jsBlockBudget: { fileLoadMs: runtimeConfig.bundleStartTimeout },
415
+ },
360
416
  }));
361
417
  sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
362
418
  let metroInstance;
@@ -368,6 +424,28 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
368
424
  let initializationCancelled = false;
369
425
  // Noop until the Metro instance resolves and the deriver is attached.
370
426
  let disposeMetroDiagnostics = () => undefined;
427
+ const eagerPrewarm = runtimeConfig.eagerPrewarm !== false;
428
+ // On small hosts the bundle build and the platform boot compete for the
429
+ // same few cores, so the platform may ask to boot first.
430
+ const deferPrewarm = eagerPrewarm && (platform.deferMetroPrewarm?.() ?? false);
431
+ if (deferPrewarm) {
432
+ sessionLogger.debug('deferring Metro prewarm until the platform is ready');
433
+ }
434
+ const startPrewarm = (instance) => {
435
+ // prewarm() memoizes this first call, so its session-lifetime setup
436
+ // signal is the one that matters — later callers' signals cannot
437
+ // cancel it. Not awaited: callers must not wait for the bundle.
438
+ const prewarmSpan = diagnostics.start('metro.prewarm', { deferred: deferPrewarm });
439
+ instance
440
+ .prewarm({
441
+ platform: bundlePlatform,
442
+ signal: sessionController.signal,
443
+ })
444
+ .then((completed) => prewarmSpan.end({ completed }))
445
+ // Swallow AbortError on teardown so it never becomes an
446
+ // unhandled rejection; non-abort failures resolve false.
447
+ .catch((error) => prewarmSpan.fail(error));
448
+ };
371
449
  try {
372
450
  [metroInstance, platformInstance] = await Promise.all([
373
451
  diagnostics.measure('metro.init', () => getMetroInstance({
@@ -386,30 +464,20 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
386
464
  }
387
465
  initialized.metro = instance;
388
466
  sessionLogger.debug('Metro initialized');
467
+ telemetry.phase('metro.ready');
389
468
  // Attach the Metro diagnostics deriver before the eager prewarm
390
469
  // fires so its bundle build/request events land in the trace.
391
470
  disposeMetroDiagnostics = observeMetroEvents(instance.events, diagnostics);
392
- if (runtimeConfig.eagerPrewarm !== false) {
471
+ if (eagerPrewarm && !deferPrewarm) {
393
472
  // Kick off the first bundle build while the platform is still booting.
394
- // prewarm() memoizes this first call, so its session-lifetime setup
395
- // signal is the one that matters — later callers' signals cannot
396
- // cancel it. Not awaited: metro.init must resolve immediately.
397
- const prewarmSpan = diagnostics.start('metro.prewarm');
398
- instance
399
- .prewarm({
400
- platform: bundlePlatform,
401
- signal: sessionController.signal,
402
- })
403
- .then((completed) => prewarmSpan.end({ completed }))
404
- // Swallow AbortError on teardown so it never becomes an
405
- // unhandled rejection; non-abort failures resolve false.
406
- .catch((error) => prewarmSpan.fail(error));
473
+ startPrewarm(instance);
407
474
  }
408
475
  return instance;
409
476
  })),
410
477
  diagnostics.measure('platform.init', () => withPlatformReadyTimeout({
411
478
  timeout: runtimeConfig.platformReadyTimeout,
412
479
  signal: sessionController.signal,
480
+ getHostPressure: telemetry.hostPressure,
413
481
  work: async (signal) => {
414
482
  return await import(platform.runner).then((module) => module.default(platform.config, runtimeConfig, {
415
483
  signal,
@@ -421,6 +489,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
421
489
  }
422
490
  initialized.platform = instance;
423
491
  sessionLogger.debug('platform runner initialized');
492
+ telemetry.attachPlatform(instance);
493
+ telemetry.phase('platform.ready');
424
494
  return instance;
425
495
  });
426
496
  },
@@ -440,6 +510,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
440
510
  ]);
441
511
  throw error;
442
512
  }
513
+ if (deferPrewarm) {
514
+ startPrewarm(metroInstance);
515
+ }
443
516
  const appLaunchOptions = platform.config.appLaunchOptions;
444
517
  let currentAppSession = null;
445
518
  const crashMonitor = createCrashMonitor();
@@ -528,6 +601,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
528
601
  // Abort only cancels finite/in-flight work. Ownership remains explicit
529
602
  // below, so this cannot race a platform or app-session disposal path.
530
603
  sessionController.abort();
604
+ telemetry.phase('dispose');
531
605
  const hookWork = (async () => {
532
606
  await hooks.drain();
533
607
  await pluginManager.callHook('harness:after-run', {
@@ -554,6 +628,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
554
628
  else if (hookResult.status === 'rejected') {
555
629
  hookError = hookResult.reason;
556
630
  }
631
+ // End-of-run telemetry has to observe the platform before it is torn
632
+ // down, but must never be able to hold teardown up for long.
633
+ await waitForBoundedSettlement(telemetry.settled(), DISPOSE_TELEMETRY_TIMEOUT_MS);
557
634
  if (runtimeConfig.forwardClientLogs) {
558
635
  metroInstance.events.removeListener(clientLogListener);
559
636
  }
@@ -610,7 +687,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
610
687
  disposeSpan.end();
611
688
  };
612
689
  const dispose = (reason = 'normal') => {
613
- disposePromise ??= disposeOnce(reason);
690
+ disposePromise ??= disposeOnce(reason).finally(() => telemetry.dispose());
614
691
  return disposePromise;
615
692
  };
616
693
  // Switch from setup-phase signal handling to dispose-based handling now
@@ -667,6 +744,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
667
744
  await hooks.drain();
668
745
  sessionLogger.debug('app is ready for %s', testFilePath);
669
746
  appReadySpan.end({ file: testFilePath, reused: false });
747
+ telemetry.phase('app.ready', { file: testFilePath });
670
748
  };
671
749
  // Shared by restartApp (crash/timeout recovery) and the 'process' reset
672
750
  // strategy: kill the current app session and either wait for a fresh one
@@ -792,6 +870,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
792
870
  config: runtimeConfig,
793
871
  context,
794
872
  diagnostics,
873
+ sessionId: telemetry.sessionId,
874
+ lifecycle: telemetry.lifecycle,
795
875
  onTestRunnerEvent: (listener) => {
796
876
  testRunnerEventListeners.add(listener);
797
877
  return () => {
@@ -811,6 +891,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
811
891
  },
812
892
  setRunState: (state) => {
813
893
  currentRun = state;
894
+ if (state) {
895
+ telemetry.trackRun(state.runId);
896
+ }
814
897
  },
815
898
  dispose: async (reason = 'normal') => {
816
899
  try {
@@ -829,6 +912,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
829
912
  // Cancels any inflight init work still holding sessionController.signal
830
913
  // (e.g. a platform init call abandoned after a readiness timeout race).
831
914
  sessionController.abort();
915
+ await telemetry.dispose();
832
916
  await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
833
917
  if (receivedEarlySignal) {
834
918
  process.exit(1);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,2BAA2B,EAC3B,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAMrB,KAAK,iBAAiB,CAAC,IAAI,SAAS,MAAM,UAAU,IAAI,CACtD,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;IACzB,QAAQ,CAAC,qBAAqB,EAAG,IAAI,CAAU;gBAMnC,YAAY,EAAE,MAAM,CAAC,YAAY;IAI7C,EAAE,CAAC,IAAI,SAAS,MAAM,UAAU,EAC9B,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAChC,MAAM,IAAI;IAaP,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;CA6CjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,2BAA2B,EAC3B,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAOrB,KAAK,iBAAiB,CAAC,IAAI,SAAS,MAAM,UAAU,IAAI,CACtD,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;IACzB,QAAQ,CAAC,qBAAqB,EAAG,IAAI,CAAU;gBAKnC,YAAY,EAAE,MAAM,CAAC,YAAY;IAI7C,EAAE,CAAC,IAAI,SAAS,MAAM,UAAU,EAC9B,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAChC,MAAM,IAAI;IAaP,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;CA8CjB"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHarnessSession } from './harness-session.js';
2
+ import { getWatchSession } from './watch-session.js';
2
3
  import { executeRun } from './execute-run.js';
3
4
  import { HarnessError } from '@react-native-harness/tools';
4
5
  import { getErrorMessage } from './logs.js';
@@ -6,7 +7,6 @@ export default class JestHarness {
6
7
  isSerial = true;
7
8
  supportsEventEmitters = true;
8
9
  #globalConfig;
9
- #session = null;
10
10
  #listeners = new Map();
11
11
  constructor(globalConfig) {
12
12
  this.#globalConfig = globalConfig;
@@ -27,11 +27,12 @@ export default class JestHarness {
27
27
  throw new Error('Parallel test running is not supported');
28
28
  }
29
29
  const isWatchMode = this.#globalConfig.watch || this.#globalConfig.watchAll;
30
+ let session = null;
30
31
  try {
31
- if (!this.#session) {
32
- this.#session = await createHarnessSession(this.#globalConfig);
33
- }
34
- await executeRun(this.#session, tests, watcher, this.#emit, this.#globalConfig);
32
+ session = isWatchMode
33
+ ? await getWatchSession(this.#globalConfig)
34
+ : await createHarnessSession(this.#globalConfig);
35
+ await executeRun(session, tests, watcher, this.#emit, this.#globalConfig);
35
36
  }
36
37
  catch (error) {
37
38
  if (error instanceof HarnessError) {
@@ -41,8 +42,7 @@ export default class JestHarness {
41
42
  }
42
43
  finally {
43
44
  if (!isWatchMode) {
44
- await this.#session?.dispose();
45
- this.#session = null;
45
+ await session?.dispose();
46
46
  }
47
47
  }
48
48
  }
@@ -1,5 +1,8 @@
1
1
  declare const _default: {
2
2
  runner: string;
3
+ maxWorkers: number;
4
+ modulePathIgnorePatterns: string[];
5
+ watchman: boolean;
3
6
  };
4
7
  export default _default;
5
8
  //# sourceMappingURL=jest-preset.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":";;;AAAA,wBAEE"}
1
+ {"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":";;;;;;AAAA,wBAoBE"}