@react-native-harness/jest 1.2.0-rc.1 → 1.3.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.
- package/dist/__tests__/bridge.test.d.ts +2 -0
- package/dist/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/__tests__/bridge.test.js +142 -0
- package/dist/__tests__/client-log-handler.test.js +116 -72
- package/dist/__tests__/crash-monitor.test.d.ts +2 -0
- package/dist/__tests__/crash-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/crash-monitor.test.js +212 -0
- package/dist/__tests__/execute-run.test.d.ts +2 -0
- package/dist/__tests__/execute-run.test.d.ts.map +1 -0
- package/dist/__tests__/execute-run.test.js +229 -0
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts +7 -0
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
- package/dist/__tests__/harness-session.test.d.ts +2 -0
- package/dist/__tests__/harness-session.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session.test.js +250 -0
- package/dist/__tests__/hook-queue.test.d.ts +2 -0
- package/dist/__tests__/hook-queue.test.d.ts.map +1 -0
- package/dist/__tests__/hook-queue.test.js +56 -0
- package/dist/__tests__/index.test.js +94 -37
- package/dist/__tests__/resource-lock.test.js +24 -24
- package/dist/client-log-handler.d.ts +10 -11
- package/dist/client-log-handler.d.ts.map +1 -1
- package/dist/client-log-handler.js +36 -58
- package/dist/crash-monitor.d.ts +17 -10
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +164 -63
- package/dist/execute-run.d.ts +4 -0
- package/dist/execute-run.d.ts.map +1 -0
- package/dist/execute-run.js +151 -0
- package/dist/harness-session.d.ts +34 -0
- package/dist/harness-session.d.ts.map +1 -0
- package/dist/harness-session.js +523 -0
- package/dist/hook-queue.d.ts +6 -0
- package/dist/hook-queue.d.ts.map +1 -0
- package/dist/hook-queue.js +18 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -230
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/resource-lock.d.ts +0 -1
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +1 -1
- package/dist/run.d.ts +2 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -3
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -8
- package/src/__tests__/bridge.test.ts +184 -0
- package/src/__tests__/client-log-handler.test.ts +117 -81
- package/src/__tests__/crash-monitor.test.ts +288 -0
- package/src/__tests__/execute-run.test.ts +325 -0
- package/src/__tests__/hook-queue.test.ts +77 -0
- package/src/__tests__/index.test.ts +135 -55
- package/src/__tests__/resource-lock.test.ts +29 -42
- package/src/client-log-handler.ts +48 -64
- package/src/crash-monitor.ts +248 -0
- package/src/execute-run.ts +199 -0
- package/src/harness-session.ts +734 -0
- package/src/hook-queue.ts +26 -0
- package/src/index.ts +17 -330
- package/src/logs.ts +4 -0
- package/src/resource-lock.ts +1 -1
- package/src/run.ts +5 -5
- package/tsconfig.tsbuildinfo +1 -0
- package/src/__tests__/crash-supervisor.test.ts +0 -200
- package/src/__tests__/harness-cache.test.ts +0 -74
- package/src/__tests__/harness.test.ts +0 -923
- package/src/crash-supervisor.ts +0 -321
- package/src/env.d.ts +0 -9
- package/src/harness.ts +0 -870
- package/src/setup.ts +0 -127
- 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
|
+
};
|
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|