@react-native-harness/jest 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/__tests__/bridge.test.d.ts +2 -0
  2. package/dist/__tests__/bridge.test.d.ts.map +1 -0
  3. package/dist/__tests__/bridge.test.js +142 -0
  4. package/dist/__tests__/client-log-handler.test.js +116 -72
  5. package/dist/__tests__/crash-monitor.test.d.ts +2 -0
  6. package/dist/__tests__/crash-monitor.test.d.ts.map +1 -0
  7. package/dist/__tests__/crash-monitor.test.js +212 -0
  8. package/dist/__tests__/execute-run.test.d.ts +2 -0
  9. package/dist/__tests__/execute-run.test.d.ts.map +1 -0
  10. package/dist/__tests__/execute-run.test.js +229 -0
  11. package/dist/__tests__/harness.test.js +35 -0
  12. package/dist/__tests__/hook-queue.test.d.ts +2 -0
  13. package/dist/__tests__/hook-queue.test.d.ts.map +1 -0
  14. package/dist/__tests__/hook-queue.test.js +56 -0
  15. package/dist/__tests__/index.test.js +94 -37
  16. package/dist/__tests__/resource-lock.test.js +24 -24
  17. package/dist/cli-args.test.d.ts +2 -0
  18. package/dist/cli-args.test.d.ts.map +1 -0
  19. package/dist/cli-args.test.js +1 -0
  20. package/dist/client-log-handler.d.ts +10 -11
  21. package/dist/client-log-handler.d.ts.map +1 -1
  22. package/dist/client-log-handler.js +36 -58
  23. package/dist/crash-monitor.d.ts +23 -0
  24. package/dist/crash-monitor.d.ts.map +1 -0
  25. package/dist/crash-monitor.js +176 -0
  26. package/dist/event-mapper.d.ts +16 -0
  27. package/dist/event-mapper.d.ts.map +1 -0
  28. package/dist/event-mapper.js +120 -0
  29. package/dist/execute-run.d.ts +4 -0
  30. package/dist/execute-run.d.ts.map +1 -0
  31. package/dist/execute-run.js +151 -0
  32. package/dist/global-setup.d.ts +3 -0
  33. package/dist/global-setup.d.ts.map +1 -0
  34. package/dist/global-setup.js +60 -0
  35. package/dist/global-teardown.d.ts +3 -0
  36. package/dist/global-teardown.d.ts.map +1 -0
  37. package/dist/global-teardown.js +11 -0
  38. package/dist/harness-reporter.d.ts +8 -0
  39. package/dist/harness-reporter.d.ts.map +1 -0
  40. package/dist/harness-reporter.js +12 -0
  41. package/dist/harness-session.d.ts +34 -0
  42. package/dist/harness-session.d.ts.map +1 -0
  43. package/dist/harness-session.js +523 -0
  44. package/dist/harness.d.ts.map +1 -1
  45. package/dist/harness.js +6 -1
  46. package/dist/hook-queue.d.ts +6 -0
  47. package/dist/hook-queue.d.ts.map +1 -0
  48. package/dist/hook-queue.js +18 -0
  49. package/dist/index.d.ts +0 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +12 -230
  52. package/dist/jest-options.d.ts +2 -0
  53. package/dist/jest-options.d.ts.map +1 -0
  54. package/dist/jest-options.js +1 -0
  55. package/dist/logs.d.ts +1 -0
  56. package/dist/logs.d.ts.map +1 -1
  57. package/dist/logs.js +3 -0
  58. package/dist/resource-lock.d.ts +0 -1
  59. package/dist/resource-lock.d.ts.map +1 -1
  60. package/dist/resource-lock.js +1 -1
  61. package/dist/run.d.ts +2 -2
  62. package/dist/run.d.ts.map +1 -1
  63. package/dist/run.js +3 -3
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +17 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +1 -0
  68. package/package.json +7 -8
  69. package/src/__tests__/bridge.test.ts +184 -0
  70. package/src/__tests__/client-log-handler.test.ts +117 -81
  71. package/src/__tests__/crash-monitor.test.ts +288 -0
  72. package/src/__tests__/execute-run.test.ts +325 -0
  73. package/src/__tests__/hook-queue.test.ts +77 -0
  74. package/src/__tests__/index.test.ts +135 -55
  75. package/src/__tests__/resource-lock.test.ts +29 -42
  76. package/src/client-log-handler.ts +48 -64
  77. package/src/crash-monitor.ts +248 -0
  78. package/src/execute-run.ts +199 -0
  79. package/src/harness-session.ts +734 -0
  80. package/src/hook-queue.ts +26 -0
  81. package/src/index.ts +17 -330
  82. package/src/logs.ts +4 -0
  83. package/src/resource-lock.ts +1 -1
  84. package/src/run.ts +5 -5
  85. package/tsconfig.tsbuildinfo +1 -0
  86. package/src/__tests__/crash-supervisor.test.ts +0 -200
  87. package/src/__tests__/harness-cache.test.ts +0 -74
  88. package/src/__tests__/harness.test.ts +0 -870
  89. package/src/crash-supervisor.ts +0 -321
  90. package/src/env.d.ts +0 -9
  91. package/src/harness.ts +0 -864
  92. package/src/setup.ts +0 -127
  93. package/src/teardown.ts +0 -14
@@ -0,0 +1,523 @@
1
+ import { createHarnessBridge, } from '@react-native-harness/bridge/server';
2
+ import { HARNESS_BRIDGE_PATH, } from '@react-native-harness/bridge';
3
+ import { getMetroInstance, isMetroCacheReusable, waitForMetroBackedAppReady, } from '@react-native-harness/bundler-metro';
4
+ import { createHarnessPluginManager, } from '@react-native-harness/plugins';
5
+ import { logger, createCrashArtifactWriter, getTimeoutSignal, raceAbortSignals, } from '@react-native-harness/tools';
6
+ import { getConfig, ConfigSchema, } from '@react-native-harness/config';
7
+ import { preRunMessage } from 'jest-util';
8
+ import { PlatformReadyTimeoutError } from './errors.js';
9
+ import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
10
+ import { createCrashMonitor } from './crash-monitor.js';
11
+ import { createHookQueue } from './hook-queue.js';
12
+ import { createClientLogCollector, } from './client-log-handler.js';
13
+ import { createActionHooksPlugin } from './action-hooks.js';
14
+ import { createResourceLockManager, } from './resource-lock.js';
15
+ import { resolveHarnessMetroPort } from './metro-port.js';
16
+ import { getAdditionalCliArgs } from './cli-args.js';
17
+ import { logMetroCacheReused, logMetroPortFallback, logNativeCoverageCollected, logRunnerStarting, logRunnerStillWaitingInQueue, logRunnerWaitingInQueue, logTestEnvironmentReady, logTestRunHeader, } from './logs.js';
18
+ const sessionLogger = logger.child('runtime');
19
+ const defaultResourceLockManager = createResourceLockManager();
20
+ const ignorePromiseRejection = () => undefined;
21
+ // ---------------------------------------------------------------------------
22
+ // Internal helpers
23
+ // ---------------------------------------------------------------------------
24
+ const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
25
+ const waitForAbort = (signal) => {
26
+ if (signal.aborted) {
27
+ return Promise.reject(signal.reason ?? createAbortError());
28
+ }
29
+ return new Promise((_, reject) => {
30
+ signal.addEventListener('abort', () => reject(signal.reason ?? createAbortError()), { once: true });
31
+ });
32
+ };
33
+ const withPlatformReadyTimeout = async (options) => {
34
+ const timeoutSignal = getTimeoutSignal(options.timeout);
35
+ const combinedSignal = raceAbortSignals([options.signal, timeoutSignal]);
36
+ try {
37
+ return await options.work(combinedSignal);
38
+ }
39
+ catch (error) {
40
+ if (error instanceof DOMException &&
41
+ error.name === 'AbortError' &&
42
+ timeoutSignal.aborted &&
43
+ !options.signal.aborted) {
44
+ throw new PlatformReadyTimeoutError(options.timeout);
45
+ }
46
+ throw error;
47
+ }
48
+ };
49
+ const waitForAppReady = async (base, testFilePath) => {
50
+ const { metroInstance, bridge, platformInstance, platformId, bundleStartTimeout, readyTimeout, maxAppRestarts, crashMonitor, appLaunchOptions, } = base;
51
+ const logWait = (message, ...args) => sessionLogger.debug(`waitForAppReady: ${message}`, ...args);
52
+ return await waitForMetroBackedAppReady({
53
+ metro: metroInstance,
54
+ platformId,
55
+ bundleStartTimeout,
56
+ readyTimeout,
57
+ maxAppRestarts,
58
+ signal: new AbortController().signal,
59
+ startAttempt: async () => {
60
+ logWait('launching app for %s', testFilePath);
61
+ await platformInstance.restartApp(appLaunchOptions);
62
+ logWait('launch request completed, waiting for bridge ready');
63
+ },
64
+ waitForReady: async (signal) => {
65
+ logWait('waiting for runtime ready');
66
+ // Listen for the NEXT 'connected' event rather than using nextConnection(),
67
+ // because nextConnection() returns the existing connection immediately if one
68
+ // is already set. waitForReady is called before startAttempt, so a stale
69
+ // connection from a previous run would resolve the promise before startAttempt
70
+ // even restarts the app — leaving bridge.connection null after the restart.
71
+ await new Promise((resolve, reject) => {
72
+ const onConnected = (_conn) => { cleanup(); resolve(); };
73
+ const onAbort = () => { cleanup(); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
74
+ const cleanup = () => {
75
+ bridge.off('connected', onConnected);
76
+ signal.removeEventListener('abort', onAbort);
77
+ };
78
+ if (signal.aborted) {
79
+ onAbort();
80
+ return;
81
+ }
82
+ bridge.on('connected', onConnected);
83
+ signal.addEventListener('abort', onAbort, { once: true });
84
+ });
85
+ logWait('runtime ready received');
86
+ },
87
+ waitForCrash: async (signal) => {
88
+ const watch = crashMonitor.watch(testFilePath, 'startup');
89
+ watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
90
+ try {
91
+ logWait('waiting for crash or runtime ready');
92
+ return await Promise.race([watch.promise, waitForAbort(signal)]);
93
+ }
94
+ finally {
95
+ watch.cancel();
96
+ }
97
+ },
98
+ onAttemptStart: () => {
99
+ logWait('beginning launch attempt for %s', testFilePath);
100
+ },
101
+ onAttemptReset: () => {
102
+ logWait('resetting launch attempt state');
103
+ },
104
+ });
105
+ };
106
+ const getDefaultResourceLockKey = (platform) => `${platform.platformId}:${platform.name}`;
107
+ const buildBridgeHookScheduler = (hooks, pluginManager, getCurrentRunId) => (event) => {
108
+ const runId = getCurrentRunId();
109
+ if (!runId)
110
+ return;
111
+ const schedule = (name, payload) => hooks.schedule(() => pluginManager.callHook(name, payload));
112
+ switch (event.type) {
113
+ case 'collection-started':
114
+ return schedule('collection:started', { runId, file: event.file });
115
+ case 'collection-finished':
116
+ return schedule('collection:finished', { runId, file: event.file, duration: event.duration, totalTests: event.totalTests });
117
+ case 'suite-started':
118
+ return schedule('suite:started', { runId, file: event.file, name: event.name });
119
+ case 'suite-finished':
120
+ return schedule('suite:finished', { runId, file: event.file, name: event.name, duration: event.duration, status: event.status, error: event.error });
121
+ case 'test-started':
122
+ return schedule('test:started', { runId, file: event.file, suite: event.suite, name: event.name });
123
+ case 'test-finished':
124
+ return schedule('test:finished', { runId, file: event.file, suite: event.suite, name: event.name, duration: event.duration, status: event.status, error: event.error });
125
+ case 'module-bundling-started':
126
+ return schedule('metro:bundle-started', { runId, target: 'module', file: event.file });
127
+ case 'module-bundling-finished':
128
+ return schedule('metro:bundle-finished', { runId, target: 'module', file: event.file, duration: event.duration });
129
+ case 'module-bundling-failed':
130
+ return schedule('metro:bundle-failed', { runId, target: 'module', file: event.file, duration: event.duration, error: event.error });
131
+ case 'setup-file-bundling-started':
132
+ return schedule('metro:bundle-started', { runId, target: 'setupFile', file: event.file, setupType: event.setupType });
133
+ case 'setup-file-bundling-finished':
134
+ return schedule('metro:bundle-finished', { runId, target: 'setupFile', file: event.file, setupType: event.setupType, duration: event.duration });
135
+ case 'setup-file-bundling-failed':
136
+ return schedule('metro:bundle-failed', { runId, target: 'setupFile', file: event.file, setupType: event.setupType, duration: event.duration, error: event.error });
137
+ }
138
+ };
139
+ // ---------------------------------------------------------------------------
140
+ // Config loading
141
+ // ---------------------------------------------------------------------------
142
+ const applyEnvVars = (harnessConfig, globalConfig) => {
143
+ if (globalConfig.collectCoverage) {
144
+ process.env.RN_HARNESS_COLLECT_COVERAGE = 'true';
145
+ if (harnessConfig.coverage?.root) {
146
+ process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverage.root;
147
+ }
148
+ }
149
+ if (harnessConfig.disableViewFlattening) {
150
+ process.env.RN_HARNESS_VIEW_FLATTENING = 'false';
151
+ }
152
+ };
153
+ const loadConfig = async (globalConfig) => {
154
+ const projectRoot = globalConfig.rootDir;
155
+ const { config: rawConfig } = await getConfig(projectRoot);
156
+ const cliArgs = getAdditionalCliArgs();
157
+ let harnessConfig = cliArgs.metroPort != null
158
+ ? ConfigSchema.parse({ ...rawConfig, metroPort: cliArgs.metroPort })
159
+ : rawConfig;
160
+ if (process.env.PRE_RUN_HOOK || process.env.AFTER_RUN_HOOK) {
161
+ harnessConfig = ConfigSchema.parse({
162
+ ...harnessConfig,
163
+ plugins: [...(harnessConfig.plugins ?? []), createActionHooksPlugin()],
164
+ });
165
+ }
166
+ if (harnessConfig.webSocketPort != null &&
167
+ harnessConfig.webSocketPort !== harnessConfig.metroPort) {
168
+ logger.warn(`Config option "webSocketPort" is deprecated and ignored. Harness now uses metroPort (${harnessConfig.metroPort}) for bridge traffic.`);
169
+ }
170
+ const selectedRunnerName = cliArgs.harnessRunner ?? harnessConfig.defaultRunner;
171
+ if (!selectedRunnerName)
172
+ throw new NoRunnerSpecifiedError();
173
+ const platform = harnessConfig.runners.find((r) => r.name === selectedRunnerName);
174
+ if (!platform)
175
+ throw new RunnerNotFoundError(selectedRunnerName);
176
+ return { harnessConfig, platform, projectRoot };
177
+ };
178
+ // ---------------------------------------------------------------------------
179
+ // Session creation
180
+ // ---------------------------------------------------------------------------
181
+ export const createHarnessSession = async (globalConfig, { lockManager = defaultResourceLockManager } = {}) => {
182
+ preRunMessage.remove(process.stderr);
183
+ const { harnessConfig, platform, projectRoot } = await loadConfig(globalConfig);
184
+ applyEnvVars(harnessConfig, globalConfig);
185
+ sessionLogger.debug('creating session for runner=%s platform=%s', platform.name, platform.platformId);
186
+ // Single AbortController for the entire setup phase. Registered signal
187
+ // handlers abort this so that slow inflight operations (Metro init, platform
188
+ // runner startup) are cancelled promptly on SIGTERM/SIGINT.
189
+ const setupController = new AbortController();
190
+ const onEarlySignal = () => setupController.abort();
191
+ process.once('SIGTERM', onEarlySignal);
192
+ process.once('SIGINT', onEarlySignal);
193
+ const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
194
+ let didWaitForResourceLock = false;
195
+ let lastStillWaitingLogAt = 0;
196
+ logTestRunHeader(platform);
197
+ const resourceLease = await lockManager.acquire(resourceLockKey, {
198
+ signal: setupController.signal,
199
+ onWait: () => {
200
+ didWaitForResourceLock = true;
201
+ logRunnerWaitingInQueue(platform);
202
+ sessionLogger.debug('waiting in queue for runner=%s key=%s', platform.name, resourceLockKey);
203
+ },
204
+ onStillWaiting: (elapsedMs) => {
205
+ if (elapsedMs - lastStillWaitingLogAt < 5000)
206
+ return;
207
+ lastStillWaitingLogAt = elapsedMs;
208
+ logRunnerStillWaitingInQueue(platform);
209
+ sessionLogger.debug('still waiting in queue for runner=%s key=%s elapsedMs=%d', platform.name, resourceLockKey, elapsedMs);
210
+ },
211
+ });
212
+ if (didWaitForResourceLock)
213
+ logRunnerStarting(platform);
214
+ sessionLogger.debug('resource lock acquired for runner=%s key=%s', platform.name, resourceLockKey);
215
+ // Hoisted so the outer catch can release it even if an error occurs after
216
+ // port resolution but before the inner try/catch (e.g. bridge creation failure).
217
+ let metroPortLease = null;
218
+ try {
219
+ const resolution = await resolveHarnessMetroPort({
220
+ config: harnessConfig,
221
+ platform,
222
+ resourceLockManager: lockManager,
223
+ signal: setupController.signal,
224
+ });
225
+ metroPortLease = resolution.metroPortLease;
226
+ const { config: runtimeConfig, initialMetroPort, didFallback } = resolution;
227
+ if (didFallback)
228
+ logMetroPortFallback(initialMetroPort, runtimeConfig.metroPort);
229
+ if (runtimeConfig.unstable__enableMetroCache && isMetroCacheReusable(projectRoot)) {
230
+ logMetroCacheReused(platform);
231
+ }
232
+ const pluginAbortController = new AbortController();
233
+ const pluginManager = createHarnessPluginManager({
234
+ plugins: (runtimeConfig.plugins ?? []),
235
+ projectRoot,
236
+ config: runtimeConfig,
237
+ runner: platform,
238
+ abortSignal: pluginAbortController.signal,
239
+ });
240
+ const hooks = createHookQueue();
241
+ let currentRun = null;
242
+ const getCurrentRunId = () => currentRun?.runId;
243
+ const clientLogCollector = createClientLogCollector();
244
+ const context = { platform };
245
+ const bridge = await createHarnessBridge({
246
+ noServer: true,
247
+ timeout: runtimeConfig.bridgeTimeout,
248
+ context,
249
+ });
250
+ sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
251
+ let metroInstance;
252
+ let platformInstance;
253
+ try {
254
+ [metroInstance, platformInstance] = await Promise.all([
255
+ getMetroInstance({
256
+ projectRoot,
257
+ harnessConfig: runtimeConfig,
258
+ websocketEndpoints: {
259
+ [HARNESS_BRIDGE_PATH]: bridge.ws,
260
+ },
261
+ }, setupController.signal).then((instance) => {
262
+ sessionLogger.debug('Metro initialized');
263
+ return instance;
264
+ }),
265
+ withPlatformReadyTimeout({
266
+ timeout: runtimeConfig.platformReadyTimeout,
267
+ signal: setupController.signal,
268
+ work: async (signal) => {
269
+ return await import(platform.runner).then((module) => module.default(platform.config, runtimeConfig, {
270
+ signal,
271
+ })).then((instance) => {
272
+ sessionLogger.debug('platform runner initialized');
273
+ return instance;
274
+ });
275
+ },
276
+ }),
277
+ ]);
278
+ }
279
+ catch (error) {
280
+ // Only bridge needs cleanup here; leases are released by the outer catch.
281
+ await bridge.dispose();
282
+ throw error;
283
+ }
284
+ const crashArtifactWriter = createCrashArtifactWriter({
285
+ runnerName: platform.name,
286
+ platformId: platform.platformId,
287
+ });
288
+ const appMonitor = platformInstance.createAppMonitor({ crashArtifactWriter });
289
+ const appLaunchOptions = platform.config.appLaunchOptions;
290
+ const crashMonitor = createCrashMonitor({ appMonitor, platformRunner: platformInstance });
291
+ // Pre-build the options that are constant across all app-ready calls;
292
+ // only testFilePath varies per call.
293
+ const appReadyBaseOptions = {
294
+ metroInstance,
295
+ bridge,
296
+ platformInstance,
297
+ platformId: platform.platformId,
298
+ bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
299
+ readyTimeout: runtimeConfig.bridgeTimeout,
300
+ maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
301
+ crashMonitor,
302
+ appLaunchOptions,
303
+ };
304
+ // --- Event listeners ---
305
+ const bridgeEventListener = buildBridgeHookScheduler(hooks, pluginManager, getCurrentRunId);
306
+ const onMetroEvent = (event) => {
307
+ const runId = getCurrentRunId();
308
+ if (runId && event.type === 'client_log') {
309
+ hooks.schedule(() => pluginManager.callHook('metro:client-log', { runId, level: event.level, data: event.data }));
310
+ }
311
+ };
312
+ const flushClientLogs = () => clientLogCollector.flush();
313
+ const clientLogListener = clientLogCollector.handleEvent;
314
+ const onConnected = (conn) => {
315
+ const runId = getCurrentRunId();
316
+ if (!runId)
317
+ return;
318
+ hooks.schedule(() => pluginManager.callHook('runtime:ready', { runId, device: conn.device }));
319
+ };
320
+ const onDisconnected = () => {
321
+ const runId = getCurrentRunId();
322
+ if (!runId)
323
+ return;
324
+ hooks.schedule(() => pluginManager.callHook('runtime:disconnected', { runId, reason: 'bridge-disconnected' }));
325
+ };
326
+ bridge.on('connected', onConnected);
327
+ bridge.on('disconnected', onDisconnected);
328
+ bridge.on('event', bridgeEventListener);
329
+ metroInstance.events.addListener(onMetroEvent);
330
+ if (runtimeConfig.forwardClientLogs) {
331
+ metroInstance.events.addListener(clientLogListener);
332
+ }
333
+ sessionLogger.debug('registered runtime, bridge, and Metro listeners');
334
+ // --- Dispose ---
335
+ let disposePromise = null;
336
+ const disposeOnce = async (reason) => {
337
+ sessionLogger.debug('disposing session (reason=%s)', reason);
338
+ let hookError;
339
+ try {
340
+ await hooks.drain();
341
+ await pluginManager.callHook('harness:after-run', {
342
+ runId: currentRun?.runId,
343
+ reason,
344
+ summary: currentRun?.summary,
345
+ status: currentRun?.status,
346
+ error: currentRun?.error,
347
+ });
348
+ await hooks.drain();
349
+ await pluginManager.callHook('harness:before-dispose', {
350
+ runId: currentRun?.runId,
351
+ reason,
352
+ summary: currentRun?.summary,
353
+ status: currentRun?.status,
354
+ error: currentRun?.error,
355
+ });
356
+ await hooks.drain();
357
+ }
358
+ catch (error) {
359
+ hookError = error;
360
+ }
361
+ if (runtimeConfig.forwardClientLogs) {
362
+ metroInstance.events.removeListener(clientLogListener);
363
+ }
364
+ metroInstance.events.removeListener(onMetroEvent);
365
+ bridge.off('connected', onConnected);
366
+ bridge.off('disconnected', onDisconnected);
367
+ bridge.off('event', bridgeEventListener);
368
+ const nativeCoverageConfig = runtimeConfig.coverage?.native?.ios;
369
+ if (nativeCoverageConfig?.pods?.length && platformInstance.collectNativeCoverage) {
370
+ try {
371
+ await platformInstance.stopApp();
372
+ const lcovPath = await platformInstance.collectNativeCoverage({
373
+ pods: nativeCoverageConfig.pods,
374
+ outputDir: projectRoot,
375
+ });
376
+ if (lcovPath) {
377
+ logNativeCoverageCollected(lcovPath);
378
+ }
379
+ }
380
+ catch (error) {
381
+ sessionLogger.warn('failed to collect native coverage: %s', error);
382
+ }
383
+ }
384
+ let cleanupError;
385
+ try {
386
+ await Promise.all([
387
+ crashMonitor.dispose(),
388
+ bridge.dispose(),
389
+ platformInstance.dispose(),
390
+ metroInstance.dispose(),
391
+ metroPortLease?.release(),
392
+ ]);
393
+ }
394
+ catch (error) {
395
+ cleanupError = error;
396
+ }
397
+ finally {
398
+ await resourceLease.release();
399
+ pluginAbortController.abort();
400
+ }
401
+ sessionLogger.debug('session resources disposed');
402
+ if (hookError)
403
+ throw hookError;
404
+ if (cleanupError)
405
+ throw cleanupError;
406
+ };
407
+ const dispose = (reason = 'normal') => {
408
+ disposePromise ??= disposeOnce(reason);
409
+ return disposePromise;
410
+ };
411
+ // Switch from setup-phase signal handling to dispose-based handling now
412
+ // that all infrastructure is up.
413
+ process.off('SIGTERM', onEarlySignal);
414
+ process.off('SIGINT', onEarlySignal);
415
+ const onSignal = () => void dispose('abort').then(() => process.exit(0));
416
+ process.once('SIGTERM', onSignal);
417
+ process.once('SIGINT', onSignal);
418
+ // --- Startup hooks ---
419
+ try {
420
+ await pluginManager.callHook('harness:before-creation', { appLaunchOptions });
421
+ await hooks.drain();
422
+ await appMonitor.start();
423
+ sessionLogger.debug('app monitor started');
424
+ await pluginManager.callHook('harness:before-run', { appLaunchOptions });
425
+ await hooks.drain();
426
+ }
427
+ catch (error) {
428
+ process.off('SIGTERM', onSignal);
429
+ process.off('SIGINT', onSignal);
430
+ await dispose('error');
431
+ throw error;
432
+ }
433
+ logTestEnvironmentReady(platform);
434
+ sessionLogger.debug('session ready');
435
+ // --- Public API ---
436
+ const ensureAppReady = async (testFilePath) => {
437
+ await hooks.drain();
438
+ sessionLogger.debug('ensuring app is ready for %s', testFilePath);
439
+ if (crashMonitor.isAlive() && bridge.connection !== null && await platformInstance.isAppRunning()) {
440
+ sessionLogger.debug('reusing existing ready app for %s', testFilePath);
441
+ return;
442
+ }
443
+ crashMonitor.reset();
444
+ sessionLogger.debug('app not ready, waiting for launch and runtime readiness');
445
+ await waitForAppReady(appReadyBaseOptions, testFilePath);
446
+ await hooks.drain();
447
+ sessionLogger.debug('app is ready for %s', testFilePath);
448
+ };
449
+ const restartApp = async (testFilePath) => {
450
+ await hooks.drain();
451
+ await crashMonitor.stop();
452
+ sessionLogger.debug('restarting app (testFile=%s mode=%s)', testFilePath ?? 'n/a', testFilePath ? 'stop-and-ensure-ready' : 'direct-restart');
453
+ if (testFilePath) {
454
+ await platformInstance.stopApp();
455
+ }
456
+ else {
457
+ await platformInstance.restartApp(appLaunchOptions);
458
+ }
459
+ crashMonitor.reset();
460
+ await crashMonitor.start();
461
+ if (testFilePath) {
462
+ await ensureAppReady(testFilePath);
463
+ }
464
+ await hooks.drain();
465
+ sessionLogger.debug('restart completed');
466
+ };
467
+ const runTestFile = async (testPath, options) => {
468
+ await hooks.drain();
469
+ const conn = bridge.connection;
470
+ if (!conn)
471
+ throw new Error('No active app connection');
472
+ sessionLogger.debug('running test file on client: %s', testPath);
473
+ if (!runtimeConfig.detectNativeCrashes) {
474
+ const result = await conn.runTests(testPath, { ...options, runner: platform.runner });
475
+ await hooks.drain();
476
+ return result;
477
+ }
478
+ const crashWatch = crashMonitor.watch(testPath, 'execution');
479
+ // Attach a handler now so the rejection is always observed, whether the
480
+ // crash wins the race or cancel() is called after the test run wins.
481
+ crashWatch.promise.catch(ignorePromiseRejection);
482
+ try {
483
+ const result = await Promise.race([
484
+ conn.runTests(testPath, { ...options, runner: platform.runner }),
485
+ crashWatch.promise,
486
+ ]);
487
+ await hooks.drain();
488
+ return result;
489
+ }
490
+ finally {
491
+ crashWatch.cancel();
492
+ }
493
+ };
494
+ return {
495
+ config: runtimeConfig,
496
+ context,
497
+ runTestFile,
498
+ ensureAppReady,
499
+ restartApp,
500
+ resetCrashState: () => crashMonitor.reset(),
501
+ flushClientLogs,
502
+ callHook: async (name, payload) => {
503
+ await hooks.drain();
504
+ await pluginManager.callHook(name, payload);
505
+ await hooks.drain();
506
+ },
507
+ setRunState: (state) => {
508
+ currentRun = state;
509
+ },
510
+ dispose: (reason = 'normal') => {
511
+ process.off('SIGTERM', onSignal);
512
+ process.off('SIGINT', onSignal);
513
+ return dispose(reason);
514
+ },
515
+ };
516
+ }
517
+ catch (error) {
518
+ process.off('SIGTERM', onEarlySignal);
519
+ process.off('SIGINT', onEarlySignal);
520
+ await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
521
+ throw error;
522
+ }
523
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EACb,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,cAAc,EAGd,oBAAoB,EACpB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,gBAAgB,EACrB,eAAe,EAEf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAgB/B,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,sBAAsB,KAC5B,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC,QAAQ,EAAE,oBAAoB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC;IAC3E,WAAW,EAAE,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,IAIF,CAAC;AAgDF,eAAO,MAAM,eAAe,GAAU,SAAS;IAC7C,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,KAAG,OAAO,CAAC,IAAI,CA2Ef,CAAC;AAomBF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAYjB,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,EAEL,cAAc,EAGd,oBAAoB,EACpB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,gBAAgB,EACrB,eAAe,EAEf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAgB/B,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,sBAAsB,KAC5B,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC,QAAQ,EAAE,oBAAoB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC;IAC3E,WAAW,EAAE,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,IAIF,CAAC;AAgDF,eAAO,MAAM,eAAe,GAAU,SAAS;IAC7C,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,KAAG,OAAO,CAAC,IAAI,CA2Ef,CAAC;AA0mBF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAYjB,CAAC"}
package/dist/harness.js CHANGED
@@ -437,7 +437,8 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
437
437
  metroInstance.events.addListener(clientLogListener);
438
438
  harnessLogger.debug('client log forwarding enabled');
439
439
  }
440
- const dispose = async (reason = 'normal') => {
440
+ let disposePromise = null;
441
+ const disposeOnce = async (reason = 'normal') => {
441
442
  harnessLogger.debug('disposing Harness (reason=%s)', reason);
442
443
  let hookError;
443
444
  try {
@@ -495,6 +496,10 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
495
496
  throw cleanupError;
496
497
  }
497
498
  };
499
+ const dispose = (reason = 'normal') => {
500
+ disposePromise ??= disposeOnce(reason);
501
+ return disposePromise;
502
+ };
498
503
  if (signal.aborted) {
499
504
  await dispose('abort');
500
505
  throw new DOMException('The operation was aborted', 'AbortError');
@@ -0,0 +1,6 @@
1
+ export type HookQueue = {
2
+ schedule: (work: () => Promise<void>) => void;
3
+ drain: () => Promise<void>;
4
+ };
5
+ export declare const createHookQueue: () => HookQueue;
6
+ //# sourceMappingURL=hook-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-queue.d.ts","sourceRoot":"","sources":["../src/hook-queue.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAC9C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,eAAe,QAAO,SAoBlC,CAAC"}
@@ -0,0 +1,18 @@
1
+ export const createHookQueue = () => {
2
+ let tail = Promise.resolve();
3
+ let firstError;
4
+ const schedule = (work) => {
5
+ tail = tail.then(work).catch((err) => {
6
+ firstError ??= err;
7
+ });
8
+ };
9
+ const drain = async () => {
10
+ await tail;
11
+ if (firstError !== undefined) {
12
+ const err = firstError;
13
+ firstError = undefined;
14
+ throw err;
15
+ }
16
+ };
17
+ return { schedule, drain };
18
+ };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import type { CallbackTestRunnerInterface, Config, OnTestFailure, OnTestStart, OnTestSuccess, Test, TestRunnerOptions, TestWatcher } from 'jest-runner';
2
- import { Config as HarnessConfig } from '@react-native-harness/config';
3
- import { type Harness } from './harness.js';
4
2
  export default class JestHarness implements CallbackTestRunnerInterface {
5
3
  #private;
6
4
  readonly isSerial = true;
7
5
  constructor(globalConfig: Config.GlobalConfig);
8
6
  runTests(tests: Array<Test>, watcher: TestWatcher, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure, options: TestRunnerOptions): Promise<void>;
9
- _createInBandTestRun(tests: Array<Test>, watcher: TestWatcher, harness: Harness, harnessConfig: HarnessConfig, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure): Promise<void>;
10
7
  }
11
8
  //# sourceMappingURL=index.d.ts.map
@@ -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,EAAwB,MAAM,cAAc,CAAC;AA0ClE,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;CAyQjB"}
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;AAMrB,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;gBAKb,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;CAiCjB"}