@react-native-harness/jest 1.0.0 → 1.1.0-rc.2
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__/crash-supervisor.test.d.ts +2 -0
- package/dist/__tests__/crash-supervisor.test.d.ts.map +1 -0
- package/dist/__tests__/crash-supervisor.test.js +151 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +54 -0
- package/dist/__tests__/harness-cache.test.d.ts +2 -0
- package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
- package/dist/__tests__/harness-cache.test.js +51 -0
- package/dist/__tests__/harness.test.d.ts +2 -0
- package/dist/__tests__/harness.test.d.ts.map +1 -0
- package/dist/__tests__/harness.test.js +399 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +42 -0
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/crash-supervisor.d.ts +21 -0
- package/dist/crash-supervisor.d.ts.map +1 -0
- package/dist/crash-supervisor.js +210 -0
- package/dist/errors.d.ts +9 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +43 -8
- package/dist/harness.d.ts +30 -8
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +440 -98
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -57
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +13 -8
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +18 -6
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/__tests__/crash-supervisor.test.ts +200 -0
- package/src/__tests__/errors.test.ts +70 -0
- package/src/__tests__/harness-cache.test.ts +74 -0
- package/src/__tests__/harness.test.ts +547 -0
- package/src/__tests__/index.test.ts +61 -0
- package/src/cli-args.ts +6 -0
- package/src/crash-supervisor.ts +321 -0
- package/src/env.d.ts +1 -1
- package/src/errors.ts +68 -8
- package/src/harness.ts +566 -120
- package/src/index.ts +276 -66
- package/src/logs.ts +4 -0
- package/src/run.ts +20 -10
- package/src/setup.ts +22 -6
- package/tsconfig.json +3 -3
- package/tsconfig.lib.json +6 -6
- package/dist/cli-args.test.d.ts +0 -2
- package/dist/cli-args.test.d.ts.map +0 -1
- package/dist/cli-args.test.js +0 -1
- package/dist/crash-monitor.d.ts +0 -16
- package/dist/crash-monitor.d.ts.map +0 -1
- package/dist/crash-monitor.js +0 -75
- package/dist/event-mapper.d.ts +0 -16
- package/dist/event-mapper.d.ts.map +0 -1
- package/dist/event-mapper.js +0 -120
- package/dist/global-setup.d.ts +0 -3
- package/dist/global-setup.d.ts.map +0 -1
- package/dist/global-setup.js +0 -60
- package/dist/global-teardown.d.ts +0 -3
- package/dist/global-teardown.d.ts.map +0 -1
- package/dist/global-teardown.js +0 -11
- package/dist/harness-reporter.d.ts +0 -8
- package/dist/harness-reporter.d.ts.map +0 -1
- package/dist/harness-reporter.js +0 -12
- package/dist/jest-options.d.ts +0 -2
- package/dist/jest-options.d.ts.map +0 -1
- package/dist/jest-options.js +0 -1
- package/dist/types.d.ts +0 -17
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
- package/src/crash-monitor.ts +0 -109
package/src/harness.ts
CHANGED
|
@@ -4,124 +4,192 @@ import {
|
|
|
4
4
|
} from '@react-native-harness/bridge/server';
|
|
5
5
|
import {
|
|
6
6
|
HarnessContext,
|
|
7
|
+
type BridgeEvents,
|
|
8
|
+
type DeviceDescriptor,
|
|
7
9
|
TestExecutionOptions,
|
|
8
10
|
TestSuiteResult,
|
|
9
11
|
} from '@react-native-harness/bridge';
|
|
10
12
|
import {
|
|
13
|
+
type AppMonitorEvent,
|
|
14
|
+
type AppLaunchOptions,
|
|
11
15
|
HarnessPlatform,
|
|
12
16
|
HarnessPlatformRunner,
|
|
13
17
|
} from '@react-native-harness/platforms';
|
|
14
18
|
import {
|
|
15
19
|
getMetroInstance,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
isMetroCacheReusable,
|
|
21
|
+
waitForMetroBackedAppReady,
|
|
22
|
+
type MetroInstance,
|
|
23
|
+
type ReportableEvent,
|
|
19
24
|
} from '@react-native-harness/bundler-metro';
|
|
20
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createHarnessPluginManager,
|
|
27
|
+
type FlatHarnessHookContexts,
|
|
28
|
+
type HarnessPlugin,
|
|
29
|
+
type HarnessPluginManager,
|
|
30
|
+
type HarnessRunStatus,
|
|
31
|
+
type HarnessRunSummary,
|
|
32
|
+
} from '@react-native-harness/plugins';
|
|
33
|
+
import { logger, createCrashArtifactWriter } from '@react-native-harness/tools';
|
|
34
|
+
import { InitializationTimeoutError } from './errors.js';
|
|
21
35
|
import { Config as HarnessConfig } from '@react-native-harness/config';
|
|
22
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
createCrashSupervisor,
|
|
38
|
+
type CrashSupervisor,
|
|
39
|
+
} from './crash-supervisor.js';
|
|
23
40
|
import { createClientLogListener } from './client-log-handler.js';
|
|
24
|
-
import
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { logMetroCacheReused } from './logs.js';
|
|
43
|
+
|
|
44
|
+
const harnessLogger = logger.child('runtime');
|
|
25
45
|
|
|
26
46
|
export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
27
47
|
|
|
48
|
+
export type HarnessRunState = {
|
|
49
|
+
runId: string;
|
|
50
|
+
startTime: number;
|
|
51
|
+
testFiles: string[];
|
|
52
|
+
watchMode: boolean;
|
|
53
|
+
coverageEnabled: boolean;
|
|
54
|
+
summary?: HarnessRunSummary;
|
|
55
|
+
status?: HarnessRunStatus;
|
|
56
|
+
error?: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
28
59
|
export type Harness = {
|
|
29
60
|
context: HarnessContext;
|
|
30
61
|
runTests: (
|
|
31
62
|
path: string,
|
|
32
63
|
options: HarnessRunTestsOptions
|
|
33
64
|
) => Promise<TestSuiteResult>;
|
|
34
|
-
|
|
65
|
+
ensureAppReady: (testFilePath: string) => Promise<void>;
|
|
66
|
+
restart: (testFilePath?: string) => Promise<void>;
|
|
35
67
|
dispose: () => Promise<void>;
|
|
36
|
-
|
|
68
|
+
crashSupervisor: CrashSupervisor;
|
|
69
|
+
callHook: HarnessPluginManager<HarnessConfig, HarnessPlatform>['callHook'];
|
|
70
|
+
setRunState: (runState: HarnessRunState | null) => void;
|
|
71
|
+
getRunState: () => HarnessRunState | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const maybeLogMetroCacheReuse = (
|
|
75
|
+
config: HarnessConfig,
|
|
76
|
+
platform: HarnessPlatform,
|
|
77
|
+
projectRoot: string
|
|
78
|
+
): void => {
|
|
79
|
+
if (
|
|
80
|
+
config.unstable__enableMetroCache &&
|
|
81
|
+
isMetroCacheReusable(projectRoot)
|
|
82
|
+
) {
|
|
83
|
+
logMetroCacheReused(platform);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const createAbortError = () =>
|
|
88
|
+
new DOMException('The operation was aborted', 'AbortError');
|
|
89
|
+
|
|
90
|
+
const waitForAbort = (signal: AbortSignal): Promise<never> => {
|
|
91
|
+
if (signal.aborted) {
|
|
92
|
+
return Promise.reject(signal.reason ?? createAbortError());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return new Promise((_, reject) => {
|
|
96
|
+
signal.addEventListener(
|
|
97
|
+
'abort',
|
|
98
|
+
() => {
|
|
99
|
+
reject(signal.reason ?? createAbortError());
|
|
100
|
+
},
|
|
101
|
+
{ once: true }
|
|
102
|
+
);
|
|
103
|
+
});
|
|
37
104
|
};
|
|
38
105
|
|
|
39
106
|
export const waitForAppReady = async (options: {
|
|
40
|
-
|
|
107
|
+
metroInstance: MetroInstance;
|
|
41
108
|
serverBridge: BridgeServer;
|
|
42
109
|
platformInstance: HarnessPlatformRunner;
|
|
110
|
+
platformId: string;
|
|
43
111
|
bundleStartTimeout: number;
|
|
44
|
-
|
|
45
|
-
|
|
112
|
+
readyTimeout: number;
|
|
113
|
+
maxAppRestarts: number;
|
|
114
|
+
testFilePath: string;
|
|
115
|
+
crashSupervisor: CrashSupervisor;
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
appLaunchOptions?: AppLaunchOptions;
|
|
118
|
+
launchApp?: () => Promise<void>;
|
|
46
119
|
}): Promise<void> => {
|
|
47
120
|
const {
|
|
48
|
-
|
|
121
|
+
metroInstance,
|
|
49
122
|
serverBridge,
|
|
50
123
|
platformInstance,
|
|
124
|
+
platformId,
|
|
51
125
|
bundleStartTimeout,
|
|
52
|
-
|
|
53
|
-
|
|
126
|
+
readyTimeout,
|
|
127
|
+
maxAppRestarts,
|
|
128
|
+
testFilePath,
|
|
129
|
+
crashSupervisor,
|
|
130
|
+
appLaunchOptions,
|
|
131
|
+
launchApp = () => platformInstance.restartApp(appLaunchOptions),
|
|
54
132
|
} = options;
|
|
133
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
55
134
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let bundleTimeoutId: NodeJS.Timeout | null = null;
|
|
59
|
-
|
|
60
|
-
const clearBundleTimeout = () => {
|
|
61
|
-
if (bundleTimeoutId) {
|
|
62
|
-
clearTimeout(bundleTimeoutId);
|
|
63
|
-
bundleTimeoutId = null;
|
|
64
|
-
}
|
|
135
|
+
const logWait = (message: string, ...args: Array<unknown>) => {
|
|
136
|
+
harnessLogger.debug(`waitForAppReady: ${message}`, ...args);
|
|
65
137
|
};
|
|
138
|
+
return await waitForMetroBackedAppReady({
|
|
139
|
+
metro: metroInstance,
|
|
140
|
+
platformId,
|
|
141
|
+
bundleStartTimeout,
|
|
142
|
+
readyTimeout,
|
|
143
|
+
maxAppRestarts,
|
|
144
|
+
signal,
|
|
145
|
+
startAttempt: async () => {
|
|
146
|
+
logWait('launching app for %s', testFilePath);
|
|
147
|
+
await launchApp();
|
|
148
|
+
logWait('launch request completed, waiting for bridge ready');
|
|
149
|
+
},
|
|
150
|
+
waitForReady: async (signal) => {
|
|
151
|
+
logWait('waiting for runtime ready');
|
|
152
|
+
return await Promise.race([
|
|
153
|
+
new Promise<void>((resolve) => {
|
|
154
|
+
const onReady = () => {
|
|
155
|
+
cleanup();
|
|
156
|
+
crashSupervisor.markReady();
|
|
157
|
+
logWait('runtime ready received');
|
|
158
|
+
resolve();
|
|
159
|
+
};
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
cleanup();
|
|
162
|
+
};
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
serverBridge.off('ready', onReady);
|
|
165
|
+
signal.removeEventListener('abort', onAbort);
|
|
166
|
+
};
|
|
66
167
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
reject(new MaxAppRestartsError(restartCount + 1));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
restartCount++;
|
|
87
|
-
platformInstance.restartApp().catch(reject);
|
|
88
|
-
startBundleTimeout(); // Reset timer for next attempt
|
|
89
|
-
}, bundleStartTimeout);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Metro event listener
|
|
93
|
-
const onMetroEvent = (event: ReportableEvent) => {
|
|
94
|
-
if (event.type === 'bundle_build_started') {
|
|
95
|
-
isBundling = true;
|
|
96
|
-
clearBundleTimeout(); // Cancel restart timer while bundling
|
|
97
|
-
} else if (
|
|
98
|
-
event.type === 'bundle_build_done' ||
|
|
99
|
-
event.type === 'bundle_build_failed'
|
|
100
|
-
) {
|
|
101
|
-
isBundling = false;
|
|
102
|
-
startBundleTimeout(); // Reset timer after bundle completes
|
|
168
|
+
serverBridge.on('ready', onReady);
|
|
169
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
170
|
+
}),
|
|
171
|
+
waitForAbort(signal),
|
|
172
|
+
]);
|
|
173
|
+
},
|
|
174
|
+
waitForCrash: async (signal) => {
|
|
175
|
+
try {
|
|
176
|
+
logWait('waiting for crash or runtime ready');
|
|
177
|
+
return await Promise.race([
|
|
178
|
+
crashSupervisor.waitForCrash(testFilePath),
|
|
179
|
+
waitForAbort(signal),
|
|
180
|
+
]);
|
|
181
|
+
} finally {
|
|
182
|
+
crashSupervisor.cancelCrashWaiters();
|
|
103
183
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
clearBundleTimeout();
|
|
114
|
-
metroEvents.removeListener(onMetroEvent);
|
|
115
|
-
serverBridge.off('ready', onReady);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Setup listeners
|
|
119
|
-
metroEvents.addListener(onMetroEvent);
|
|
120
|
-
serverBridge.once('ready', onReady);
|
|
121
|
-
|
|
122
|
-
// Start the app and timeout
|
|
123
|
-
platformInstance.restartApp().catch(reject);
|
|
124
|
-
startBundleTimeout();
|
|
184
|
+
},
|
|
185
|
+
onAttemptStart: () => {
|
|
186
|
+
logWait('beginning launch attempt for %s', testFilePath);
|
|
187
|
+
crashSupervisor.beginLaunch(testFilePath);
|
|
188
|
+
},
|
|
189
|
+
onAttemptReset: () => {
|
|
190
|
+
logWait('resetting launch attempt state');
|
|
191
|
+
crashSupervisor.cancelCrashWaiters();
|
|
192
|
+
},
|
|
125
193
|
});
|
|
126
194
|
};
|
|
127
195
|
|
|
@@ -134,100 +202,474 @@ const getHarnessInternal = async (
|
|
|
134
202
|
const context: HarnessContext = {
|
|
135
203
|
platform,
|
|
136
204
|
};
|
|
205
|
+
harnessLogger.debug(
|
|
206
|
+
'creating Harness internals for runner=%s platform=%s',
|
|
207
|
+
platform.name,
|
|
208
|
+
platform.platformId
|
|
209
|
+
);
|
|
210
|
+
maybeLogMetroCacheReuse(config, platform, projectRoot);
|
|
211
|
+
const pluginAbortController = new AbortController();
|
|
212
|
+
const pluginManager = createHarnessPluginManager<HarnessConfig, HarnessPlatform>({
|
|
213
|
+
plugins: (config.plugins ?? []) as Array<
|
|
214
|
+
HarnessPlugin<object, HarnessConfig, HarnessPlatform>
|
|
215
|
+
>,
|
|
216
|
+
projectRoot,
|
|
217
|
+
config,
|
|
218
|
+
runner: platform,
|
|
219
|
+
abortSignal: pluginAbortController.signal,
|
|
220
|
+
});
|
|
221
|
+
let currentRun: HarnessRunState | null = null;
|
|
222
|
+
let activeTestFilePath: string | undefined;
|
|
223
|
+
const pendingHookPromises = new Set<Promise<void>>();
|
|
224
|
+
let pendingHookError: unknown;
|
|
225
|
+
|
|
226
|
+
const getCurrentRunId = () => currentRun?.runId;
|
|
227
|
+
const toRelativeTestFilePath = (testFilePath?: string) =>
|
|
228
|
+
testFilePath == null ? undefined : path.relative(projectRoot, testFilePath);
|
|
229
|
+
const setActiveTestFilePath = (testFilePath?: string) => {
|
|
230
|
+
activeTestFilePath = toRelativeTestFilePath(testFilePath);
|
|
231
|
+
};
|
|
232
|
+
const flushPendingHooks = async () => {
|
|
233
|
+
if (pendingHookPromises.size > 0) {
|
|
234
|
+
await Promise.allSettled([...pendingHookPromises]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (pendingHookError !== undefined) {
|
|
238
|
+
const error = pendingHookError;
|
|
239
|
+
pendingHookError = undefined;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const trackHook = (promise: Promise<void>) => {
|
|
244
|
+
const trackedPromise = promise
|
|
245
|
+
.catch((error) => {
|
|
246
|
+
pendingHookError ??= error;
|
|
247
|
+
})
|
|
248
|
+
.finally(() => {
|
|
249
|
+
pendingHookPromises.delete(trackedPromise);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
pendingHookPromises.add(trackedPromise);
|
|
253
|
+
};
|
|
254
|
+
const scheduleHook = <
|
|
255
|
+
TName extends keyof FlatHarnessHookContexts<object, HarnessConfig, HarnessPlatform>,
|
|
256
|
+
>(
|
|
257
|
+
name: TName,
|
|
258
|
+
payload: Omit<
|
|
259
|
+
FlatHarnessHookContexts<object, HarnessConfig, HarnessPlatform>[TName],
|
|
260
|
+
| 'plugin'
|
|
261
|
+
| 'logger'
|
|
262
|
+
| 'projectRoot'
|
|
263
|
+
| 'config'
|
|
264
|
+
| 'runner'
|
|
265
|
+
| 'platform'
|
|
266
|
+
| 'state'
|
|
267
|
+
| 'timestamp'
|
|
268
|
+
| 'abortSignal'
|
|
269
|
+
| 'meta'
|
|
270
|
+
>
|
|
271
|
+
) => {
|
|
272
|
+
trackHook(pluginManager.callHook(name, payload));
|
|
273
|
+
};
|
|
137
274
|
|
|
275
|
+
harnessLogger.debug(
|
|
276
|
+
'starting Metro, platform runner, and bridge initialization'
|
|
277
|
+
);
|
|
138
278
|
const [metroInstance, platformInstance, serverBridge] = await Promise.all([
|
|
139
|
-
getMetroInstance({ projectRoot, harnessConfig: config }, signal)
|
|
140
|
-
|
|
141
|
-
|
|
279
|
+
getMetroInstance({ projectRoot, harnessConfig: config }, signal).then(
|
|
280
|
+
(instance) => {
|
|
281
|
+
harnessLogger.debug('Metro initialized');
|
|
282
|
+
return instance;
|
|
283
|
+
}
|
|
142
284
|
),
|
|
285
|
+
import(platform.runner)
|
|
286
|
+
.then((module) => module.default(platform.config, config))
|
|
287
|
+
.then((instance) => {
|
|
288
|
+
harnessLogger.debug('platform runner initialized');
|
|
289
|
+
return instance;
|
|
290
|
+
}),
|
|
143
291
|
getBridgeServer({
|
|
144
292
|
port: config.webSocketPort,
|
|
145
293
|
timeout: config.bridgeTimeout,
|
|
146
294
|
context,
|
|
295
|
+
}).then((bridge) => {
|
|
296
|
+
harnessLogger.debug(
|
|
297
|
+
'bridge server initialized on port %d',
|
|
298
|
+
config.webSocketPort
|
|
299
|
+
);
|
|
300
|
+
return bridge;
|
|
147
301
|
}),
|
|
148
302
|
]);
|
|
303
|
+
const crashArtifactWriter = createCrashArtifactWriter({
|
|
304
|
+
runnerName: platform.name,
|
|
305
|
+
platformId: platform.platformId,
|
|
306
|
+
});
|
|
307
|
+
const appMonitor = platformInstance.createAppMonitor({
|
|
308
|
+
crashArtifactWriter,
|
|
309
|
+
});
|
|
310
|
+
const appLaunchOptions = (
|
|
311
|
+
platform.config as { appLaunchOptions?: AppLaunchOptions }
|
|
312
|
+
).appLaunchOptions;
|
|
149
313
|
|
|
150
|
-
// Forward client logs to console if enabled
|
|
151
314
|
const clientLogListener = createClientLogListener();
|
|
315
|
+
const bridgeEventListener = (event: BridgeEvents) => {
|
|
316
|
+
const runId = getCurrentRunId();
|
|
317
|
+
if (!runId) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
switch (event.type) {
|
|
322
|
+
case 'collection-started':
|
|
323
|
+
scheduleHook('collection:started', {
|
|
324
|
+
runId,
|
|
325
|
+
file: event.file,
|
|
326
|
+
});
|
|
327
|
+
break;
|
|
328
|
+
case 'collection-finished':
|
|
329
|
+
scheduleHook('collection:finished', {
|
|
330
|
+
runId,
|
|
331
|
+
file: event.file,
|
|
332
|
+
duration: event.duration,
|
|
333
|
+
totalTests: event.totalTests,
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
case 'suite-started':
|
|
337
|
+
scheduleHook('suite:started', {
|
|
338
|
+
runId,
|
|
339
|
+
file: event.file,
|
|
340
|
+
name: event.name,
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
case 'suite-finished':
|
|
344
|
+
scheduleHook('suite:finished', {
|
|
345
|
+
runId,
|
|
346
|
+
file: event.file,
|
|
347
|
+
name: event.name,
|
|
348
|
+
duration: event.duration,
|
|
349
|
+
status: event.status,
|
|
350
|
+
error: event.error,
|
|
351
|
+
});
|
|
352
|
+
break;
|
|
353
|
+
case 'test-started':
|
|
354
|
+
scheduleHook('test:started', {
|
|
355
|
+
runId,
|
|
356
|
+
file: event.file,
|
|
357
|
+
suite: event.suite,
|
|
358
|
+
name: event.name,
|
|
359
|
+
});
|
|
360
|
+
break;
|
|
361
|
+
case 'test-finished':
|
|
362
|
+
scheduleHook('test:finished', {
|
|
363
|
+
runId,
|
|
364
|
+
file: event.file,
|
|
365
|
+
suite: event.suite,
|
|
366
|
+
name: event.name,
|
|
367
|
+
duration: event.duration,
|
|
368
|
+
status: event.status,
|
|
369
|
+
error: event.error,
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
case 'module-bundling-started':
|
|
373
|
+
scheduleHook('metro:bundle-started', {
|
|
374
|
+
runId,
|
|
375
|
+
target: 'module',
|
|
376
|
+
file: event.file,
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
379
|
+
case 'module-bundling-finished':
|
|
380
|
+
scheduleHook('metro:bundle-finished', {
|
|
381
|
+
runId,
|
|
382
|
+
target: 'module',
|
|
383
|
+
file: event.file,
|
|
384
|
+
duration: event.duration,
|
|
385
|
+
});
|
|
386
|
+
break;
|
|
387
|
+
case 'module-bundling-failed':
|
|
388
|
+
scheduleHook('metro:bundle-failed', {
|
|
389
|
+
runId,
|
|
390
|
+
target: 'module',
|
|
391
|
+
file: event.file,
|
|
392
|
+
duration: event.duration,
|
|
393
|
+
error: event.error,
|
|
394
|
+
});
|
|
395
|
+
break;
|
|
396
|
+
case 'setup-file-bundling-started':
|
|
397
|
+
scheduleHook('metro:bundle-started', {
|
|
398
|
+
runId,
|
|
399
|
+
target: 'setupFile',
|
|
400
|
+
file: event.file,
|
|
401
|
+
setupType: event.setupType,
|
|
402
|
+
});
|
|
403
|
+
break;
|
|
404
|
+
case 'setup-file-bundling-finished':
|
|
405
|
+
scheduleHook('metro:bundle-finished', {
|
|
406
|
+
runId,
|
|
407
|
+
target: 'setupFile',
|
|
408
|
+
file: event.file,
|
|
409
|
+
setupType: event.setupType,
|
|
410
|
+
duration: event.duration,
|
|
411
|
+
});
|
|
412
|
+
break;
|
|
413
|
+
case 'setup-file-bundling-failed':
|
|
414
|
+
scheduleHook('metro:bundle-failed', {
|
|
415
|
+
runId,
|
|
416
|
+
target: 'setupFile',
|
|
417
|
+
file: event.file,
|
|
418
|
+
setupType: event.setupType,
|
|
419
|
+
duration: event.duration,
|
|
420
|
+
error: event.error,
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const onMetroEvent = (event: ReportableEvent) => {
|
|
426
|
+
const runId = getCurrentRunId();
|
|
427
|
+
|
|
428
|
+
if (runId && event.type === 'client_log') {
|
|
429
|
+
scheduleHook('metro:client-log', {
|
|
430
|
+
runId,
|
|
431
|
+
level: event.level,
|
|
432
|
+
data: event.data,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const crashSupervisor = createCrashSupervisor({
|
|
437
|
+
appMonitor,
|
|
438
|
+
platformRunner: platformInstance,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const onReady = (device: DeviceDescriptor) => {
|
|
442
|
+
crashSupervisor.markReady();
|
|
443
|
+
|
|
444
|
+
const runId = getCurrentRunId();
|
|
445
|
+
if (!runId) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
scheduleHook('runtime:ready', {
|
|
450
|
+
runId,
|
|
451
|
+
device,
|
|
452
|
+
});
|
|
453
|
+
};
|
|
454
|
+
const onDisconnect = () => {
|
|
455
|
+
const runId = getCurrentRunId();
|
|
456
|
+
if (!runId) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
scheduleHook('runtime:disconnected', {
|
|
461
|
+
runId,
|
|
462
|
+
reason: 'bridge-disconnected',
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
const onAppMonitorEvent = (event: AppMonitorEvent) => {
|
|
466
|
+
const runId = getCurrentRunId();
|
|
467
|
+
if (!runId) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (event.type === 'app_started') {
|
|
472
|
+
scheduleHook('app:started', {
|
|
473
|
+
runId,
|
|
474
|
+
testFile: activeTestFilePath,
|
|
475
|
+
pid: event.pid,
|
|
476
|
+
source: event.source,
|
|
477
|
+
line: event.line,
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (event.type === 'app_exited') {
|
|
483
|
+
scheduleHook('app:exited', {
|
|
484
|
+
runId,
|
|
485
|
+
testFile: activeTestFilePath,
|
|
486
|
+
pid: event.pid,
|
|
487
|
+
source: event.source,
|
|
488
|
+
line: event.line,
|
|
489
|
+
isConfirmed: event.isConfirmed,
|
|
490
|
+
crashDetails: event.crashDetails,
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (event.type === 'possible_crash') {
|
|
496
|
+
scheduleHook('app:possible-crash', {
|
|
497
|
+
runId,
|
|
498
|
+
testFile: activeTestFilePath,
|
|
499
|
+
pid: event.pid,
|
|
500
|
+
source: event.source,
|
|
501
|
+
line: event.line,
|
|
502
|
+
isConfirmed: event.isConfirmed,
|
|
503
|
+
crashDetails: event.crashDetails,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
serverBridge.on('ready', onReady);
|
|
509
|
+
serverBridge.on('disconnect', onDisconnect);
|
|
510
|
+
serverBridge.on('event', bridgeEventListener);
|
|
511
|
+
metroInstance.events.addListener(onMetroEvent);
|
|
512
|
+
appMonitor.addListener(onAppMonitorEvent);
|
|
513
|
+
harnessLogger.debug('registered runtime, bridge, and Metro listeners');
|
|
152
514
|
|
|
153
515
|
if (config.forwardClientLogs) {
|
|
154
516
|
metroInstance.events.addListener(clientLogListener);
|
|
517
|
+
harnessLogger.debug('client log forwarding enabled');
|
|
155
518
|
}
|
|
156
519
|
|
|
157
|
-
const dispose = async () => {
|
|
520
|
+
const dispose = async (reason: 'normal' | 'abort' | 'error' = 'normal') => {
|
|
521
|
+
harnessLogger.debug('disposing Harness (reason=%s)', reason);
|
|
522
|
+
let hookError: unknown;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await flushPendingHooks();
|
|
526
|
+
await pluginManager.callHook('harness:before-dispose', {
|
|
527
|
+
runId: currentRun?.runId,
|
|
528
|
+
reason,
|
|
529
|
+
summary: currentRun?.summary,
|
|
530
|
+
status: currentRun?.status,
|
|
531
|
+
error: currentRun?.error,
|
|
532
|
+
});
|
|
533
|
+
await flushPendingHooks();
|
|
534
|
+
} catch (error) {
|
|
535
|
+
hookError = error;
|
|
536
|
+
}
|
|
537
|
+
|
|
158
538
|
if (config.forwardClientLogs) {
|
|
159
539
|
metroInstance.events.removeListener(clientLogListener);
|
|
160
540
|
}
|
|
541
|
+
metroInstance.events.removeListener(onMetroEvent);
|
|
542
|
+
appMonitor.removeListener(onAppMonitorEvent);
|
|
543
|
+
serverBridge.off('ready', onReady);
|
|
544
|
+
serverBridge.off('disconnect', onDisconnect);
|
|
545
|
+
serverBridge.off('event', bridgeEventListener);
|
|
161
546
|
await Promise.all([
|
|
547
|
+
crashSupervisor.dispose(),
|
|
162
548
|
serverBridge.dispose(),
|
|
163
549
|
platformInstance.dispose(),
|
|
164
550
|
metroInstance.dispose(),
|
|
165
551
|
]);
|
|
166
|
-
|
|
552
|
+
pluginAbortController.abort();
|
|
553
|
+
harnessLogger.debug('Harness resources disposed');
|
|
167
554
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
555
|
+
if (hookError) {
|
|
556
|
+
throw hookError;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
173
559
|
|
|
174
560
|
if (signal.aborted) {
|
|
175
|
-
await dispose();
|
|
561
|
+
await dispose('abort');
|
|
176
562
|
|
|
177
563
|
throw new DOMException('The operation was aborted', 'AbortError');
|
|
178
564
|
}
|
|
179
565
|
|
|
180
566
|
try {
|
|
181
|
-
await
|
|
182
|
-
|
|
183
|
-
entryPoint: config.entryPoint,
|
|
184
|
-
platform: platform.platformId,
|
|
185
|
-
dev: true,
|
|
186
|
-
minify: false,
|
|
187
|
-
signal,
|
|
567
|
+
await pluginManager.callHook('harness:before-creation', {
|
|
568
|
+
appLaunchOptions,
|
|
188
569
|
});
|
|
189
|
-
|
|
570
|
+
await appMonitor.start();
|
|
571
|
+
harnessLogger.debug('app monitor started');
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const runState = currentRun as HarnessRunState | null;
|
|
574
|
+
|
|
575
|
+
if (runState) {
|
|
576
|
+
runState.error = error;
|
|
577
|
+
currentRun = runState;
|
|
578
|
+
}
|
|
579
|
+
await dispose(error instanceof DOMException && error.name === 'AbortError' ? 'abort' : 'error');
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const ensureAppReady = async (testFilePath: string) => {
|
|
584
|
+
await flushPendingHooks();
|
|
585
|
+
setActiveTestFilePath(testFilePath);
|
|
586
|
+
crashSupervisor.setActiveTestFile(testFilePath);
|
|
587
|
+
harnessLogger.debug('ensuring app is ready for %s', testFilePath);
|
|
588
|
+
|
|
589
|
+
if (crashSupervisor.isReady() && (await platformInstance.isAppRunning())) {
|
|
590
|
+
harnessLogger.debug('reusing existing ready app for %s', testFilePath);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
190
593
|
|
|
594
|
+
crashSupervisor.reset();
|
|
595
|
+
harnessLogger.debug('app not ready, waiting for launch and runtime readiness');
|
|
191
596
|
await waitForAppReady({
|
|
192
|
-
|
|
597
|
+
metroInstance,
|
|
193
598
|
serverBridge,
|
|
194
599
|
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
600
|
+
platformId: platform.platformId,
|
|
601
|
+
bundleStartTimeout: config.bundleStartTimeout ?? 60000,
|
|
602
|
+
readyTimeout: config.bridgeTimeout,
|
|
603
|
+
maxAppRestarts: config.maxAppRestarts ?? 2,
|
|
604
|
+
testFilePath,
|
|
605
|
+
crashSupervisor,
|
|
606
|
+
appLaunchOptions,
|
|
198
607
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
608
|
+
await flushPendingHooks();
|
|
609
|
+
harnessLogger.debug('app is ready for %s', testFilePath);
|
|
610
|
+
};
|
|
203
611
|
|
|
204
|
-
const restart = () =>
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
612
|
+
const restart = async (testFilePath?: string) => {
|
|
613
|
+
await flushPendingHooks();
|
|
614
|
+
await crashSupervisor.stop();
|
|
615
|
+
setActiveTestFilePath(testFilePath);
|
|
616
|
+
harnessLogger.debug(
|
|
617
|
+
'restarting app (testFile=%s mode=%s)',
|
|
618
|
+
testFilePath ?? 'n/a',
|
|
619
|
+
testFilePath ? 'stop-and-ensure-ready' : 'direct-restart'
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (testFilePath) {
|
|
623
|
+
harnessLogger.debug('stopping app before restart');
|
|
624
|
+
await platformInstance.stopApp();
|
|
625
|
+
} else {
|
|
626
|
+
harnessLogger.debug('requesting direct app restart');
|
|
627
|
+
await platformInstance.restartApp(appLaunchOptions);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
crashSupervisor.reset();
|
|
631
|
+
await crashSupervisor.start();
|
|
632
|
+
|
|
633
|
+
if (testFilePath) {
|
|
634
|
+
await ensureAppReady(testFilePath);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
await flushPendingHooks();
|
|
638
|
+
harnessLogger.debug('restart completed');
|
|
639
|
+
};
|
|
213
640
|
|
|
214
641
|
return {
|
|
215
642
|
context,
|
|
216
643
|
runTests: async (path, options) => {
|
|
644
|
+
await flushPendingHooks();
|
|
645
|
+
activeTestFilePath = path;
|
|
217
646
|
const client = serverBridge.rpc.clients.at(-1);
|
|
218
647
|
|
|
219
648
|
if (!client) {
|
|
220
649
|
throw new Error('No client found');
|
|
221
650
|
}
|
|
222
651
|
|
|
223
|
-
|
|
652
|
+
harnessLogger.debug('running test file on client: %s', path);
|
|
653
|
+
const result = await client.runTests(path, {
|
|
224
654
|
...options,
|
|
225
655
|
runner: platform.runner,
|
|
226
656
|
});
|
|
657
|
+
await flushPendingHooks();
|
|
658
|
+
return result;
|
|
227
659
|
},
|
|
660
|
+
ensureAppReady,
|
|
228
661
|
restart,
|
|
229
|
-
dispose,
|
|
230
|
-
|
|
662
|
+
dispose: () => dispose('normal'),
|
|
663
|
+
crashSupervisor,
|
|
664
|
+
callHook: async (name, payload) => {
|
|
665
|
+
await flushPendingHooks();
|
|
666
|
+
await pluginManager.callHook(name, payload);
|
|
667
|
+
await flushPendingHooks();
|
|
668
|
+
},
|
|
669
|
+
setRunState: (runState) => {
|
|
670
|
+
currentRun = runState;
|
|
671
|
+
},
|
|
672
|
+
getRunState: () => currentRun,
|
|
231
673
|
};
|
|
232
674
|
};
|
|
233
675
|
|
|
@@ -237,6 +679,10 @@ export const getHarness = async (
|
|
|
237
679
|
projectRoot: string
|
|
238
680
|
): Promise<Harness> => {
|
|
239
681
|
const abortSignal = AbortSignal.timeout(config.bridgeTimeout);
|
|
682
|
+
harnessLogger.debug(
|
|
683
|
+
'creating Harness with bridge timeout %dms',
|
|
684
|
+
config.bridgeTimeout
|
|
685
|
+
);
|
|
240
686
|
|
|
241
687
|
try {
|
|
242
688
|
const harness = await getHarnessInternal(
|