@react-native-harness/jest 1.1.0-rc.1 → 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__/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.js +340 -47
- 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.map +1 -1
- package/dist/crash-supervisor.js +7 -6
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/harness.d.ts +23 -1
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +396 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -58
- 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 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/__tests__/harness-cache.test.ts +74 -0
- package/src/__tests__/harness.test.ts +469 -51
- package/src/__tests__/index.test.ts +61 -0
- package/src/cli-args.ts +6 -0
- package/src/crash-supervisor.ts +13 -6
- package/src/env.d.ts +1 -1
- package/src/errors.ts +5 -0
- package/src/harness.ts +504 -58
- package/src/index.ts +276 -64
- package/src/logs.ts +4 -0
- package/src/run.ts +20 -10
- package/src/setup.ts +22 -1
- 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/harness.ts
CHANGED
|
@@ -4,19 +4,33 @@ 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,
|
|
11
14
|
type AppLaunchOptions,
|
|
12
15
|
HarnessPlatform,
|
|
13
16
|
HarnessPlatformRunner,
|
|
14
17
|
} from '@react-native-harness/platforms';
|
|
15
18
|
import {
|
|
16
19
|
getMetroInstance,
|
|
17
|
-
|
|
20
|
+
isMetroCacheReusable,
|
|
21
|
+
waitForMetroBackedAppReady,
|
|
22
|
+
type MetroInstance,
|
|
23
|
+
type ReportableEvent,
|
|
18
24
|
} from '@react-native-harness/bundler-metro';
|
|
19
|
-
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';
|
|
20
34
|
import { InitializationTimeoutError } from './errors.js';
|
|
21
35
|
import { Config as HarnessConfig } from '@react-native-harness/config';
|
|
22
36
|
import {
|
|
@@ -24,10 +38,24 @@ import {
|
|
|
24
38
|
type CrashSupervisor,
|
|
25
39
|
} from './crash-supervisor.js';
|
|
26
40
|
import { createClientLogListener } from './client-log-handler.js';
|
|
27
|
-
import
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { logMetroCacheReused } from './logs.js';
|
|
43
|
+
|
|
44
|
+
const harnessLogger = logger.child('runtime');
|
|
28
45
|
|
|
29
46
|
export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
30
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
|
+
|
|
31
59
|
export type Harness = {
|
|
32
60
|
context: HarnessContext;
|
|
33
61
|
runTests: (
|
|
@@ -38,61 +66,130 @@ export type Harness = {
|
|
|
38
66
|
restart: (testFilePath?: string) => Promise<void>;
|
|
39
67
|
dispose: () => Promise<void>;
|
|
40
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
|
+
});
|
|
41
104
|
};
|
|
42
105
|
|
|
43
106
|
export const waitForAppReady = async (options: {
|
|
107
|
+
metroInstance: MetroInstance;
|
|
44
108
|
serverBridge: BridgeServer;
|
|
45
109
|
platformInstance: HarnessPlatformRunner;
|
|
46
|
-
|
|
110
|
+
platformId: string;
|
|
111
|
+
bundleStartTimeout: number;
|
|
112
|
+
readyTimeout: number;
|
|
113
|
+
maxAppRestarts: number;
|
|
47
114
|
testFilePath: string;
|
|
48
115
|
crashSupervisor: CrashSupervisor;
|
|
116
|
+
signal?: AbortSignal;
|
|
49
117
|
appLaunchOptions?: AppLaunchOptions;
|
|
118
|
+
launchApp?: () => Promise<void>;
|
|
50
119
|
}): Promise<void> => {
|
|
51
120
|
const {
|
|
121
|
+
metroInstance,
|
|
52
122
|
serverBridge,
|
|
53
123
|
platformInstance,
|
|
54
|
-
|
|
124
|
+
platformId,
|
|
125
|
+
bundleStartTimeout,
|
|
126
|
+
readyTimeout,
|
|
127
|
+
maxAppRestarts,
|
|
55
128
|
testFilePath,
|
|
56
129
|
crashSupervisor,
|
|
57
130
|
appLaunchOptions,
|
|
131
|
+
launchApp = () => platformInstance.restartApp(appLaunchOptions),
|
|
58
132
|
} = options;
|
|
133
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
59
134
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
135
|
+
const logWait = (message: string, ...args: Array<unknown>) => {
|
|
136
|
+
harnessLogger.debug(`waitForAppReady: ${message}`, ...args);
|
|
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
|
+
};
|
|
167
|
+
|
|
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();
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
onAttemptStart: () => {
|
|
186
|
+
logWait('beginning launch attempt for %s', testFilePath);
|
|
64
187
|
crashSupervisor.beginLaunch(testFilePath);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const onReady = () => {
|
|
69
|
-
crashSupervisor.markReady();
|
|
70
|
-
cleanup();
|
|
71
|
-
resolve();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const onAbort = () => {
|
|
75
|
-
cleanup();
|
|
76
|
-
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const cleanup = () => {
|
|
80
|
-
serverBridge.off('ready', onReady);
|
|
81
|
-
signal.removeEventListener('abort', onAbort);
|
|
188
|
+
},
|
|
189
|
+
onAttemptReset: () => {
|
|
190
|
+
logWait('resetting launch attempt state');
|
|
82
191
|
crashSupervisor.cancelCrashWaiters();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
signal.addEventListener('abort', onAbort);
|
|
86
|
-
serverBridge.once('ready', onReady);
|
|
87
|
-
void crashSupervisor.waitForCrash(testFilePath).catch((error) => {
|
|
88
|
-
cleanup();
|
|
89
|
-
reject(error);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
void launchApp().catch((error) => {
|
|
93
|
-
cleanup();
|
|
94
|
-
reject(error);
|
|
95
|
-
});
|
|
192
|
+
},
|
|
96
193
|
});
|
|
97
194
|
};
|
|
98
195
|
|
|
@@ -105,16 +202,102 @@ const getHarnessInternal = async (
|
|
|
105
202
|
const context: HarnessContext = {
|
|
106
203
|
platform,
|
|
107
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
|
+
});
|
|
108
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
|
+
};
|
|
274
|
+
|
|
275
|
+
harnessLogger.debug(
|
|
276
|
+
'starting Metro, platform runner, and bridge initialization'
|
|
277
|
+
);
|
|
109
278
|
const [metroInstance, platformInstance, serverBridge] = await Promise.all([
|
|
110
|
-
getMetroInstance({ projectRoot, harnessConfig: config }, signal)
|
|
111
|
-
|
|
112
|
-
|
|
279
|
+
getMetroInstance({ projectRoot, harnessConfig: config }, signal).then(
|
|
280
|
+
(instance) => {
|
|
281
|
+
harnessLogger.debug('Metro initialized');
|
|
282
|
+
return instance;
|
|
283
|
+
}
|
|
113
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
|
+
}),
|
|
114
291
|
getBridgeServer({
|
|
115
292
|
port: config.webSocketPort,
|
|
116
293
|
timeout: config.bridgeTimeout,
|
|
117
294
|
context,
|
|
295
|
+
}).then((bridge) => {
|
|
296
|
+
harnessLogger.debug(
|
|
297
|
+
'bridge server initialized on port %d',
|
|
298
|
+
config.webSocketPort
|
|
299
|
+
);
|
|
300
|
+
return bridge;
|
|
118
301
|
}),
|
|
119
302
|
]);
|
|
120
303
|
const crashArtifactWriter = createCrashArtifactWriter({
|
|
@@ -129,76 +312,318 @@ const getHarnessInternal = async (
|
|
|
129
312
|
).appLaunchOptions;
|
|
130
313
|
|
|
131
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
|
+
};
|
|
132
436
|
const crashSupervisor = createCrashSupervisor({
|
|
133
437
|
appMonitor,
|
|
134
438
|
platformRunner: platformInstance,
|
|
135
439
|
});
|
|
136
440
|
|
|
137
|
-
|
|
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');
|
|
138
514
|
|
|
139
515
|
if (config.forwardClientLogs) {
|
|
140
516
|
metroInstance.events.addListener(clientLogListener);
|
|
517
|
+
harnessLogger.debug('client log forwarding enabled');
|
|
141
518
|
}
|
|
142
519
|
|
|
143
|
-
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
|
+
|
|
144
538
|
if (config.forwardClientLogs) {
|
|
145
539
|
metroInstance.events.removeListener(clientLogListener);
|
|
146
540
|
}
|
|
147
|
-
|
|
541
|
+
metroInstance.events.removeListener(onMetroEvent);
|
|
542
|
+
appMonitor.removeListener(onAppMonitorEvent);
|
|
543
|
+
serverBridge.off('ready', onReady);
|
|
544
|
+
serverBridge.off('disconnect', onDisconnect);
|
|
545
|
+
serverBridge.off('event', bridgeEventListener);
|
|
148
546
|
await Promise.all([
|
|
149
547
|
crashSupervisor.dispose(),
|
|
150
548
|
serverBridge.dispose(),
|
|
151
549
|
platformInstance.dispose(),
|
|
152
550
|
metroInstance.dispose(),
|
|
153
551
|
]);
|
|
552
|
+
pluginAbortController.abort();
|
|
553
|
+
harnessLogger.debug('Harness resources disposed');
|
|
554
|
+
|
|
555
|
+
if (hookError) {
|
|
556
|
+
throw hookError;
|
|
557
|
+
}
|
|
154
558
|
};
|
|
155
559
|
|
|
156
560
|
if (signal.aborted) {
|
|
157
|
-
await dispose();
|
|
561
|
+
await dispose('abort');
|
|
158
562
|
|
|
159
563
|
throw new DOMException('The operation was aborted', 'AbortError');
|
|
160
564
|
}
|
|
161
565
|
|
|
162
566
|
try {
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
entryPoint: config.entryPoint,
|
|
166
|
-
platform: platform.platformId,
|
|
167
|
-
dev: true,
|
|
168
|
-
minify: false,
|
|
169
|
-
signal,
|
|
567
|
+
await pluginManager.callHook('harness:before-creation', {
|
|
568
|
+
appLaunchOptions,
|
|
170
569
|
});
|
|
171
|
-
logMetroPrewarmCompleted(platform);
|
|
172
570
|
await appMonitor.start();
|
|
571
|
+
harnessLogger.debug('app monitor started');
|
|
173
572
|
} catch (error) {
|
|
174
|
-
|
|
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');
|
|
175
580
|
throw error;
|
|
176
581
|
}
|
|
177
582
|
|
|
178
583
|
const ensureAppReady = async (testFilePath: string) => {
|
|
584
|
+
await flushPendingHooks();
|
|
585
|
+
setActiveTestFilePath(testFilePath);
|
|
179
586
|
crashSupervisor.setActiveTestFile(testFilePath);
|
|
587
|
+
harnessLogger.debug('ensuring app is ready for %s', testFilePath);
|
|
180
588
|
|
|
181
589
|
if (crashSupervisor.isReady() && (await platformInstance.isAppRunning())) {
|
|
590
|
+
harnessLogger.debug('reusing existing ready app for %s', testFilePath);
|
|
182
591
|
return;
|
|
183
592
|
}
|
|
184
593
|
|
|
185
594
|
crashSupervisor.reset();
|
|
595
|
+
harnessLogger.debug('app not ready, waiting for launch and runtime readiness');
|
|
186
596
|
await waitForAppReady({
|
|
597
|
+
metroInstance,
|
|
187
598
|
serverBridge,
|
|
188
599
|
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
189
|
-
|
|
600
|
+
platformId: platform.platformId,
|
|
601
|
+
bundleStartTimeout: config.bundleStartTimeout ?? 60000,
|
|
602
|
+
readyTimeout: config.bridgeTimeout,
|
|
603
|
+
maxAppRestarts: config.maxAppRestarts ?? 2,
|
|
190
604
|
testFilePath,
|
|
191
605
|
crashSupervisor,
|
|
192
606
|
appLaunchOptions,
|
|
193
607
|
});
|
|
608
|
+
await flushPendingHooks();
|
|
609
|
+
harnessLogger.debug('app is ready for %s', testFilePath);
|
|
194
610
|
};
|
|
195
611
|
|
|
196
612
|
const restart = async (testFilePath?: string) => {
|
|
613
|
+
await flushPendingHooks();
|
|
197
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
|
+
);
|
|
198
621
|
|
|
199
622
|
if (testFilePath) {
|
|
623
|
+
harnessLogger.debug('stopping app before restart');
|
|
200
624
|
await platformInstance.stopApp();
|
|
201
625
|
} else {
|
|
626
|
+
harnessLogger.debug('requesting direct app restart');
|
|
202
627
|
await platformInstance.restartApp(appLaunchOptions);
|
|
203
628
|
}
|
|
204
629
|
|
|
@@ -208,26 +633,43 @@ const getHarnessInternal = async (
|
|
|
208
633
|
if (testFilePath) {
|
|
209
634
|
await ensureAppReady(testFilePath);
|
|
210
635
|
}
|
|
636
|
+
|
|
637
|
+
await flushPendingHooks();
|
|
638
|
+
harnessLogger.debug('restart completed');
|
|
211
639
|
};
|
|
212
640
|
|
|
213
641
|
return {
|
|
214
642
|
context,
|
|
215
643
|
runTests: async (path, options) => {
|
|
644
|
+
await flushPendingHooks();
|
|
645
|
+
activeTestFilePath = path;
|
|
216
646
|
const client = serverBridge.rpc.clients.at(-1);
|
|
217
647
|
|
|
218
648
|
if (!client) {
|
|
219
649
|
throw new Error('No client found');
|
|
220
650
|
}
|
|
221
651
|
|
|
222
|
-
|
|
652
|
+
harnessLogger.debug('running test file on client: %s', path);
|
|
653
|
+
const result = await client.runTests(path, {
|
|
223
654
|
...options,
|
|
224
655
|
runner: platform.runner,
|
|
225
656
|
});
|
|
657
|
+
await flushPendingHooks();
|
|
658
|
+
return result;
|
|
226
659
|
},
|
|
227
660
|
ensureAppReady,
|
|
228
661
|
restart,
|
|
229
|
-
dispose,
|
|
662
|
+
dispose: () => dispose('normal'),
|
|
230
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(
|