@react-native-harness/jest 1.4.1 → 1.6.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.
@@ -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;AA6BjF,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;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"}
@@ -8,6 +8,7 @@ import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './te
8
8
  import { formatHarnessErrorMessage } from './format-harness-error.js';
9
9
  import { logger } from '@react-native-harness/tools';
10
10
  import { printSummary, writeTraceFile } from './diagnostics/index.js';
11
+ import { toJestStatus } from './toJestStatus.js';
11
12
  const diagnosticsLogger = logger.child('diagnostics');
12
13
  const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
13
14
  const applyJestResultToSummary = (summary, result) => {
@@ -59,7 +60,7 @@ const emitHarnessTestFinished = async (emitEvent, event) => {
59
60
  location,
60
61
  numPassingAsserts: event.status === 'passed' ? 1 : 0,
61
62
  startedAt: event.startedAt,
62
- status: event.status,
63
+ status: toJestStatus(event.status),
63
64
  title: event.name,
64
65
  };
65
66
  await emitEvent('test-case-result', event.file, testCaseResult);
@@ -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":"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"}
@@ -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, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } from '@react-native-harness/tools';
6
+ import { createCrashArtifactWriter, createDiagnostics, delay, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } 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';
@@ -24,6 +24,19 @@ const defaultResourceLockManager = createResourceLockManager();
24
24
  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
+ const DISPOSE_HOOK_TIMEOUT_MS = 5_000;
28
+ const waitForBoundedSettlement = async (promise, timeoutMs) => {
29
+ const timeout = delay(timeoutMs);
30
+ try {
31
+ return await Promise.race([
32
+ promise.then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason })),
33
+ timeout.promise.then(() => null),
34
+ ]);
35
+ }
36
+ finally {
37
+ timeout.cancel();
38
+ }
39
+ };
27
40
  export const waitForBridgeDisconnectOrTimeout = async ({ bridge, connection, timeoutMs, }) => {
28
41
  if (bridge.connection !== connection) {
29
42
  return true;
@@ -244,6 +257,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
244
257
  preRunMessage.remove(process.stderr);
245
258
  const setupStartedAt = Date.now();
246
259
  const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig);
260
+ // Metro platform for prewarm and startup detection. Vega apps request
261
+ // "kepler" bundles while the harness platform id stays "vega".
262
+ const bundlePlatform = platform.bundlePlatform ?? platform.platformId;
247
263
  applyEnvVars(harnessConfig, globalConfig);
248
264
  const diagnostics = createDiagnostics({
249
265
  enabled: isDiagnosticsEnabled(harnessConfig),
@@ -260,7 +276,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
260
276
  // child the runner threads it into.
261
277
  ensureSignalsDeliverable();
262
278
  const sessionController = new AbortController();
263
- const onEarlySignal = () => sessionController.abort();
279
+ let receivedEarlySignal = false;
280
+ const onEarlySignal = () => {
281
+ receivedEarlySignal = true;
282
+ sessionController.abort();
283
+ };
264
284
  process.once('SIGTERM', onEarlySignal);
265
285
  process.once('SIGINT', onEarlySignal);
266
286
  const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
@@ -341,6 +361,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
341
361
  sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
342
362
  let metroInstance;
343
363
  let platformInstance;
364
+ const initialized = {
365
+ metro: null,
366
+ platform: null,
367
+ };
368
+ let initializationCancelled = false;
344
369
  // Noop until the Metro instance resolves and the deriver is attached.
345
370
  let disposeMetroDiagnostics = () => undefined;
346
371
  try {
@@ -348,11 +373,18 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
348
373
  diagnostics.measure('metro.init', () => getMetroInstance({
349
374
  projectRoot,
350
375
  harnessConfig: runtimeConfig,
376
+ metroConfigEnhancer: platform.metroConfigEnhancer,
377
+ platformId: platform.platformId,
378
+ platformConfig: platform.config,
351
379
  websocketEndpoints: {
352
380
  [HARNESS_BRIDGE_PATH]: bridge.ws,
353
381
  },
354
382
  watchMode: globalConfig.watch || globalConfig.watchAll,
355
383
  }, sessionController.signal).then((instance) => {
384
+ if (initializationCancelled) {
385
+ return instance.dispose().then(() => instance);
386
+ }
387
+ initialized.metro = instance;
356
388
  sessionLogger.debug('Metro initialized');
357
389
  // Attach the Metro diagnostics deriver before the eager prewarm
358
390
  // fires so its bundle build/request events land in the trace.
@@ -365,7 +397,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
365
397
  const prewarmSpan = diagnostics.start('metro.prewarm');
366
398
  instance
367
399
  .prewarm({
368
- platform: platform.platformId,
400
+ platform: bundlePlatform,
369
401
  signal: sessionController.signal,
370
402
  })
371
403
  .then((completed) => prewarmSpan.end({ completed }))
@@ -384,6 +416,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
384
416
  crashArtifactWriter,
385
417
  diagnostics,
386
418
  })).then((instance) => {
419
+ if (initializationCancelled) {
420
+ return instance.dispose().then(() => instance);
421
+ }
422
+ initialized.platform = instance;
387
423
  sessionLogger.debug('platform runner initialized');
388
424
  return instance;
389
425
  });
@@ -392,10 +428,16 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
392
428
  ]);
393
429
  }
394
430
  catch (error) {
395
- // Only bridge and the Metro diagnostics listener need cleanup here;
396
- // leases are released by the outer catch.
431
+ // Roll back every resource whose initialization won a concurrent race.
432
+ // Leases are released by the outer catch.
433
+ initializationCancelled = true;
434
+ sessionController.abort();
397
435
  disposeMetroDiagnostics();
398
- await bridge.dispose();
436
+ await Promise.allSettled([
437
+ bridge.dispose(),
438
+ initialized.metro?.dispose(),
439
+ initialized.platform?.dispose(),
440
+ ]);
399
441
  throw error;
400
442
  }
401
443
  const appLaunchOptions = platform.config.appLaunchOptions;
@@ -423,7 +465,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
423
465
  const appReadyBaseOptions = {
424
466
  metroInstance,
425
467
  bridge,
426
- platformId: platform.platformId,
468
+ platformId: bundlePlatform,
427
469
  bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
428
470
  readyTimeout: runtimeConfig.bridgeTimeout,
429
471
  maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
@@ -483,7 +525,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
483
525
  sessionLogger.debug('disposing session (reason=%s)', reason);
484
526
  const disposeSpan = diagnostics.start('dispose.total', { reason });
485
527
  let hookError;
486
- try {
528
+ // Abort only cancels finite/in-flight work. Ownership remains explicit
529
+ // below, so this cannot race a platform or app-session disposal path.
530
+ sessionController.abort();
531
+ const hookWork = (async () => {
487
532
  await hooks.drain();
488
533
  await pluginManager.callHook('harness:after-run', {
489
534
  runId: currentRun?.runId,
@@ -501,9 +546,13 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
501
546
  error: currentRun?.error,
502
547
  });
503
548
  await hooks.drain();
549
+ })();
550
+ const hookResult = await waitForBoundedSettlement(hookWork, DISPOSE_HOOK_TIMEOUT_MS);
551
+ if (hookResult === null) {
552
+ hookError = new Error(`Timed out after ${DISPOSE_HOOK_TIMEOUT_MS}ms while running disposal hooks.`);
504
553
  }
505
- catch (error) {
506
- hookError = error;
554
+ else if (hookResult.status === 'rejected') {
555
+ hookError = hookResult.reason;
507
556
  }
508
557
  if (runtimeConfig.forwardClientLogs) {
509
558
  metroInstance.events.removeListener(clientLogListener);
@@ -532,28 +581,23 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
532
581
  sessionLogger.warn('failed to collect native coverage: %s', error);
533
582
  }
534
583
  }
535
- let cleanupError;
536
- try {
537
- // The app session (and its logcat/device log stream) must be fully
538
- // torn down before the platform is disposed: platform disposal can
539
- // shut down the device/emulator, and doing that concurrently with
540
- // the app session's own cleanup races the device's log stream
541
- // against the device disappearing out from under it.
542
- await Promise.all([crashMonitor.dispose(), disposeCurrentAppSession()]);
543
- await Promise.all([
544
- bridge.dispose(),
545
- platformInstance.dispose(),
546
- metroInstance.dispose(),
547
- metroPortLease?.release(),
548
- ]);
549
- }
550
- catch (error) {
551
- cleanupError = error;
552
- }
553
- finally {
554
- await resourceLease.release();
555
- sessionController.abort();
556
- }
584
+ // The app session (and its log streams) must finish before platform
585
+ // disposal can remove the underlying device. Run independent cleanup
586
+ // all-settled so one failure cannot strand another owned resource.
587
+ const appCleanup = await Promise.allSettled([
588
+ crashMonitor.dispose(),
589
+ disposeCurrentAppSession(),
590
+ ]);
591
+ const infrastructureCleanup = await Promise.allSettled([
592
+ bridge.dispose(),
593
+ platformInstance.dispose(),
594
+ metroInstance.dispose(),
595
+ metroPortLease?.release(),
596
+ ]);
597
+ const leaseCleanup = await Promise.allSettled([resourceLease.release()]);
598
+ const cleanupError = [...appCleanup, ...infrastructureCleanup, ...leaseCleanup]
599
+ .find((result) => result.status === 'rejected')
600
+ ?.reason;
557
601
  sessionLogger.debug('session resources disposed');
558
602
  if (hookError) {
559
603
  disposeSpan.fail(hookError);
@@ -786,6 +830,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
786
830
  // (e.g. a platform init call abandoned after a readiness timeout race).
787
831
  sessionController.abort();
788
832
  await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
833
+ if (receivedEarlySignal) {
834
+ process.exit(1);
835
+ }
789
836
  throw error;
790
837
  }
791
838
  };
@@ -1 +1 @@
1
- {"version":3,"file":"resource-lock.d.ts","sourceRoot":"","sources":["../src/resource-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAiBzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,KACjC,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C,CAAC;AAqQF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBAkKF,CAAC"}
1
+ {"version":3,"file":"resource-lock.d.ts","sourceRoot":"","sources":["../src/resource-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAiBzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,KACjC,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C,CAAC;AAqQF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBA+KF,CAAC"}
@@ -239,8 +239,18 @@ export const createResourceLockManager = (options = {}) => {
239
239
  if (released) {
240
240
  return;
241
241
  }
242
- await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
243
- scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
242
+ try {
243
+ await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
244
+ scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
245
+ }
246
+ catch (error) {
247
+ // A failed refresh is not fatal by design: the lock goes stale
248
+ // and another holder reclaims it. Swallow it so a transient
249
+ // write error (e.g. the owner file racing a concurrent release,
250
+ // or an EPERM on Windows when the directory is being torn down)
251
+ // never surfaces as an unhandled rejection from this interval.
252
+ scopedLogger.debug('heartbeat refresh for ticket %s failed: %s', ticketId, error instanceof Error ? error.message : String(error));
253
+ }
244
254
  }
245
255
  finally {
246
256
  heartbeatInFlight = false;
@@ -0,0 +1,4 @@
1
+ import type { Status } from '@jest/test-result';
2
+ import type { TestResultStatus } from '@react-native-harness/bridge';
3
+ export declare const toJestStatus: (status: TestResultStatus) => Status;
4
+ //# sourceMappingURL=toJestStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toJestStatus.d.ts","sourceRoot":"","sources":["../src/toJestStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,eAAO,MAAM,YAAY,GAAI,QAAQ,gBAAgB,KAAG,MACb,CAAC"}
@@ -0,0 +1,2 @@
1
+ // Jest's own runners expose skipped assertions as `pending` to reporters.
2
+ export const toJestStatus = (status) => status === 'skipped' ? 'pending' : status;
@@ -1,4 +1,5 @@
1
- import type { Status, TestResult } from '@jest/test-result';
1
+ import type { TestResult } from '@jest/test-result';
2
+ import type { TestResultStatus } from '@react-native-harness/bridge';
2
3
  export type Options = {
3
4
  stats: {
4
5
  failures: number;
@@ -16,7 +17,7 @@ export type Options = {
16
17
  testPath?: string;
17
18
  title?: string;
18
19
  fullName?: string;
19
- status: Status;
20
+ status: TestResultStatus;
20
21
  location?: {
21
22
  column: number;
22
23
  line: number;
@@ -1 +1 @@
1
- {"version":3,"file":"toTestResult.d.ts","sourceRoot":"","sources":["../src/toTestResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE;YACT,MAAM,EAAE,MAAM,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;CACnC,CAAC;AAwDF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,UAiB/C,CAAC"}
1
+ {"version":3,"file":"toTestResult.d.ts","sourceRoot":"","sources":["../src/toTestResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,gBAAgB,CAAC;QACzB,QAAQ,CAAC,EAAE;YACT,MAAM,EAAE,MAAM,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;CACnC,CAAC;AAwDF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,UAiB/C,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { toJestStatus } from './toJestStatus.js';
1
2
  const getPerfStats = ({ stats }) => {
2
3
  const start = new Date(stats.start).getTime();
3
4
  const end = new Date(stats.end).getTime();
@@ -38,7 +39,7 @@ const getTestResults = ({ errorMessage, tests, jestTestPath, }) => {
38
39
  failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
39
40
  fullName: test.fullName || test.testPath || jestTestPath || '',
40
41
  numPassingAsserts: test.status === 'passed' ? 1 : 0,
41
- status: test.status,
42
+ status: toJestStatus(test.status),
42
43
  title: test.title || '',
43
44
  location: test.location,
44
45
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-harness/jest",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -39,13 +39,13 @@
39
39
  "jest-util": "^30.2.0",
40
40
  "tslib": "^2.3.0",
41
41
  "yargs": "^17.7.2",
42
- "@react-native-harness/bridge": "1.4.1",
43
- "@react-native-harness/cache": "1.4.1",
44
- "@react-native-harness/config": "1.4.1",
45
- "@react-native-harness/bundler-metro": "1.4.1",
46
- "@react-native-harness/plugins": "1.4.1",
47
- "@react-native-harness/tools": "1.4.1",
48
- "@react-native-harness/platforms": "1.4.1"
42
+ "@react-native-harness/cache": "1.6.0",
43
+ "@react-native-harness/config": "1.6.0",
44
+ "@react-native-harness/bundler-metro": "1.6.0",
45
+ "@react-native-harness/platforms": "1.6.0",
46
+ "@react-native-harness/tools": "1.6.0",
47
+ "@react-native-harness/plugins": "1.6.0",
48
+ "@react-native-harness/bridge": "1.6.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/yargs": "^17.0.32",
@@ -32,6 +32,7 @@ import {
32
32
  import { formatHarnessErrorMessage } from './format-harness-error.js';
33
33
  import { logger } from '@react-native-harness/tools';
34
34
  import { printSummary, writeTraceFile } from './diagnostics/index.js';
35
+ import { toJestStatus } from './toJestStatus.js';
35
36
 
36
37
  const diagnosticsLogger = logger.child('diagnostics');
37
38
 
@@ -109,7 +110,7 @@ const emitHarnessTestFinished = async (
109
110
  location,
110
111
  numPassingAsserts: event.status === 'passed' ? 1 : 0,
111
112
  startedAt: event.startedAt,
112
- status: event.status,
113
+ status: toJestStatus(event.status),
113
114
  title: event.name,
114
115
  };
115
116
 
@@ -38,6 +38,7 @@ import {
38
38
  import {
39
39
  createCrashArtifactWriter,
40
40
  createDiagnostics,
41
+ delay,
41
42
  logger,
42
43
  getTimeoutSignal,
43
44
  ensureSignalsDeliverable,
@@ -94,6 +95,25 @@ const ignorePromiseRejection = () => undefined;
94
95
  const isBridgeDisconnectError = (error: unknown) =>
95
96
  error instanceof Error && error.message === 'App bridge disconnected';
96
97
  const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
98
+ const DISPOSE_HOOK_TIMEOUT_MS = 5_000;
99
+
100
+ const waitForBoundedSettlement = async <T>(
101
+ promise: Promise<T>,
102
+ timeoutMs: number
103
+ ): Promise<PromiseSettledResult<T> | null> => {
104
+ const timeout = delay(timeoutMs);
105
+ try {
106
+ return await Promise.race([
107
+ promise.then(
108
+ (value) => ({ status: 'fulfilled', value }) as const,
109
+ (reason) => ({ status: 'rejected', reason }) as const
110
+ ),
111
+ timeout.promise.then(() => null),
112
+ ]);
113
+ } finally {
114
+ timeout.cancel();
115
+ }
116
+ };
97
117
 
98
118
  type DisconnectObservableBridge = {
99
119
  readonly connection: AppConnection | null;
@@ -464,6 +484,9 @@ export const createHarnessSession = async (
464
484
 
465
485
  const setupStartedAt = Date.now();
466
486
  const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig);
487
+ // Metro platform for prewarm and startup detection. Vega apps request
488
+ // "kepler" bundles while the harness platform id stays "vega".
489
+ const bundlePlatform = platform.bundlePlatform ?? platform.platformId;
467
490
  applyEnvVars(harnessConfig, globalConfig);
468
491
 
469
492
  const diagnostics = createDiagnostics({
@@ -487,7 +510,11 @@ export const createHarnessSession = async (
487
510
  // child the runner threads it into.
488
511
  ensureSignalsDeliverable();
489
512
  const sessionController = new AbortController();
490
- const onEarlySignal = () => sessionController.abort();
513
+ let receivedEarlySignal = false;
514
+ const onEarlySignal = () => {
515
+ receivedEarlySignal = true;
516
+ sessionController.abort();
517
+ };
491
518
  process.once('SIGTERM', onEarlySignal);
492
519
  process.once('SIGINT', onEarlySignal);
493
520
 
@@ -584,6 +611,11 @@ export const createHarnessSession = async (
584
611
 
585
612
  let metroInstance: MetroInstance;
586
613
  let platformInstance: HarnessPlatformRunner;
614
+ const initialized = {
615
+ metro: null as MetroInstance | null,
616
+ platform: null as HarnessPlatformRunner | null,
617
+ };
618
+ let initializationCancelled = false;
587
619
  // Noop until the Metro instance resolves and the deriver is attached.
588
620
  let disposeMetroDiagnostics: () => void = () => undefined;
589
621
 
@@ -594,6 +626,9 @@ export const createHarnessSession = async (
594
626
  {
595
627
  projectRoot,
596
628
  harnessConfig: runtimeConfig,
629
+ metroConfigEnhancer: platform.metroConfigEnhancer,
630
+ platformId: platform.platformId,
631
+ platformConfig: platform.config,
597
632
  websocketEndpoints: {
598
633
  [HARNESS_BRIDGE_PATH]: bridge.ws as unknown as MetroWebSocketEndpoint,
599
634
  },
@@ -601,6 +636,10 @@ export const createHarnessSession = async (
601
636
  },
602
637
  sessionController.signal,
603
638
  ).then((instance) => {
639
+ if (initializationCancelled) {
640
+ return instance.dispose().then(() => instance);
641
+ }
642
+ initialized.metro = instance;
604
643
  sessionLogger.debug('Metro initialized');
605
644
  // Attach the Metro diagnostics deriver before the eager prewarm
606
645
  // fires so its bundle build/request events land in the trace.
@@ -613,7 +652,7 @@ export const createHarnessSession = async (
613
652
  const prewarmSpan = diagnostics.start('metro.prewarm');
614
653
  instance
615
654
  .prewarm({
616
- platform: platform.platformId,
655
+ platform: bundlePlatform,
617
656
  signal: sessionController.signal,
618
657
  })
619
658
  .then((completed) => prewarmSpan.end({ completed }))
@@ -638,6 +677,10 @@ export const createHarnessSession = async (
638
677
  diagnostics,
639
678
  } satisfies HarnessPlatformInitOptions),
640
679
  ).then((instance) => {
680
+ if (initializationCancelled) {
681
+ return instance.dispose().then(() => instance);
682
+ }
683
+ initialized.platform = instance;
641
684
  sessionLogger.debug('platform runner initialized');
642
685
  return instance;
643
686
  });
@@ -647,10 +690,16 @@ export const createHarnessSession = async (
647
690
  ),
648
691
  ]);
649
692
  } catch (error) {
650
- // Only bridge and the Metro diagnostics listener need cleanup here;
651
- // leases are released by the outer catch.
693
+ // Roll back every resource whose initialization won a concurrent race.
694
+ // Leases are released by the outer catch.
695
+ initializationCancelled = true;
696
+ sessionController.abort();
652
697
  disposeMetroDiagnostics();
653
- await bridge.dispose();
698
+ await Promise.allSettled([
699
+ bridge.dispose(),
700
+ initialized.metro?.dispose(),
701
+ initialized.platform?.dispose(),
702
+ ]);
654
703
  throw error;
655
704
  }
656
705
 
@@ -685,7 +734,7 @@ export const createHarnessSession = async (
685
734
  const appReadyBaseOptions: AppReadyOptions = {
686
735
  metroInstance,
687
736
  bridge,
688
- platformId: platform.platformId,
737
+ platformId: bundlePlatform,
689
738
  bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
690
739
  readyTimeout: runtimeConfig.bridgeTimeout,
691
740
  maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
@@ -759,7 +808,11 @@ export const createHarnessSession = async (
759
808
  const disposeSpan = diagnostics.start('dispose.total', { reason });
760
809
  let hookError: unknown;
761
810
 
762
- try {
811
+ // Abort only cancels finite/in-flight work. Ownership remains explicit
812
+ // below, so this cannot race a platform or app-session disposal path.
813
+ sessionController.abort();
814
+
815
+ const hookWork = (async () => {
763
816
  await hooks.drain();
764
817
  await pluginManager.callHook('harness:after-run', {
765
818
  runId: currentRun?.runId,
@@ -777,8 +830,17 @@ export const createHarnessSession = async (
777
830
  error: currentRun?.error,
778
831
  });
779
832
  await hooks.drain();
780
- } catch (error) {
781
- hookError = error;
833
+ })();
834
+ const hookResult = await waitForBoundedSettlement(
835
+ hookWork,
836
+ DISPOSE_HOOK_TIMEOUT_MS
837
+ );
838
+ if (hookResult === null) {
839
+ hookError = new Error(
840
+ `Timed out after ${DISPOSE_HOOK_TIMEOUT_MS}ms while running disposal hooks.`
841
+ );
842
+ } else if (hookResult.status === 'rejected') {
843
+ hookError = hookResult.reason;
782
844
  }
783
845
 
784
846
  if (runtimeConfig.forwardClientLogs) {
@@ -809,26 +871,23 @@ export const createHarnessSession = async (
809
871
  }
810
872
  }
811
873
 
812
- let cleanupError: unknown;
813
- try {
814
- // The app session (and its logcat/device log stream) must be fully
815
- // torn down before the platform is disposed: platform disposal can
816
- // shut down the device/emulator, and doing that concurrently with
817
- // the app session's own cleanup races the device's log stream
818
- // against the device disappearing out from under it.
819
- await Promise.all([crashMonitor.dispose(), disposeCurrentAppSession()]);
820
- await Promise.all([
821
- bridge.dispose(),
822
- platformInstance.dispose(),
823
- metroInstance.dispose(),
824
- metroPortLease?.release(),
825
- ]);
826
- } catch (error) {
827
- cleanupError = error;
828
- } finally {
829
- await resourceLease.release();
830
- sessionController.abort();
831
- }
874
+ // The app session (and its log streams) must finish before platform
875
+ // disposal can remove the underlying device. Run independent cleanup
876
+ // all-settled so one failure cannot strand another owned resource.
877
+ const appCleanup = await Promise.allSettled([
878
+ crashMonitor.dispose(),
879
+ disposeCurrentAppSession(),
880
+ ]);
881
+ const infrastructureCleanup = await Promise.allSettled([
882
+ bridge.dispose(),
883
+ platformInstance.dispose(),
884
+ metroInstance.dispose(),
885
+ metroPortLease?.release(),
886
+ ]);
887
+ const leaseCleanup = await Promise.allSettled([resourceLease.release()]);
888
+ const cleanupError = [...appCleanup, ...infrastructureCleanup, ...leaseCleanup]
889
+ .find((result): result is PromiseRejectedResult => result.status === 'rejected')
890
+ ?.reason;
832
891
 
833
892
  sessionLogger.debug('session resources disposed');
834
893
 
@@ -1102,6 +1161,9 @@ export const createHarnessSession = async (
1102
1161
  // (e.g. a platform init call abandoned after a readiness timeout race).
1103
1162
  sessionController.abort();
1104
1163
  await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
1164
+ if (receivedEarlySignal) {
1165
+ process.exit(1);
1166
+ }
1105
1167
  throw error;
1106
1168
  }
1107
1169
  };
@@ -392,8 +392,21 @@ export const createResourceLockManager = (
392
392
  return;
393
393
  }
394
394
 
395
- await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
396
- scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
395
+ try {
396
+ await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
397
+ scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
398
+ } catch (error) {
399
+ // A failed refresh is not fatal by design: the lock goes stale
400
+ // and another holder reclaims it. Swallow it so a transient
401
+ // write error (e.g. the owner file racing a concurrent release,
402
+ // or an EPERM on Windows when the directory is being torn down)
403
+ // never surfaces as an unhandled rejection from this interval.
404
+ scopedLogger.debug(
405
+ 'heartbeat refresh for ticket %s failed: %s',
406
+ ticketId,
407
+ error instanceof Error ? error.message : String(error),
408
+ );
409
+ }
397
410
  } finally {
398
411
  heartbeatInFlight = false;
399
412
  }
@@ -0,0 +1,6 @@
1
+ import type { Status } from '@jest/test-result';
2
+ import type { TestResultStatus } from '@react-native-harness/bridge';
3
+
4
+ // Jest's own runners expose skipped assertions as `pending` to reporters.
5
+ export const toJestStatus = (status: TestResultStatus): Status =>
6
+ status === 'skipped' ? 'pending' : status;
@@ -1,4 +1,6 @@
1
- import type { Status, TestResult } from '@jest/test-result';
1
+ import type { TestResult } from '@jest/test-result';
2
+ import type { TestResultStatus } from '@react-native-harness/bridge';
3
+ import { toJestStatus } from './toJestStatus.js';
2
4
 
3
5
  export type Options = {
4
6
  stats: {
@@ -17,7 +19,7 @@ export type Options = {
17
19
  testPath?: string;
18
20
  title?: string;
19
21
  fullName?: string;
20
- status: Status;
22
+ status: TestResultStatus;
21
23
  location?: {
22
24
  column: number;
23
25
  line: number;
@@ -75,7 +77,7 @@ const getTestResults = ({
75
77
  failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
76
78
  fullName: test.fullName || test.testPath || jestTestPath || '',
77
79
  numPassingAsserts: test.status === 'passed' ? 1 : 0,
78
- status: test.status,
80
+ status: toJestStatus(test.status),
79
81
  title: test.title || '',
80
82
  location: test.location,
81
83
  };