@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.
Files changed (68) hide show
  1. package/dist/__tests__/harness-cache.test.d.ts +2 -0
  2. package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
  3. package/dist/__tests__/harness-cache.test.js +51 -0
  4. package/dist/__tests__/harness.test.js +340 -47
  5. package/dist/__tests__/index.test.d.ts +2 -0
  6. package/dist/__tests__/index.test.d.ts.map +1 -0
  7. package/dist/__tests__/index.test.js +42 -0
  8. package/dist/cli-args.d.ts +1 -0
  9. package/dist/cli-args.d.ts.map +1 -1
  10. package/dist/cli-args.js +5 -0
  11. package/dist/crash-supervisor.d.ts.map +1 -1
  12. package/dist/crash-supervisor.js +7 -6
  13. package/dist/errors.d.ts +1 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +1 -0
  16. package/dist/harness.d.ts +23 -1
  17. package/dist/harness.d.ts.map +1 -1
  18. package/dist/harness.js +396 -50
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +205 -58
  21. package/dist/logs.d.ts +1 -0
  22. package/dist/logs.d.ts.map +1 -1
  23. package/dist/logs.js +3 -0
  24. package/dist/run.d.ts +7 -1
  25. package/dist/run.d.ts.map +1 -1
  26. package/dist/run.js +13 -8
  27. package/dist/setup.d.ts.map +1 -1
  28. package/dist/setup.js +18 -2
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  30. package/package.json +7 -6
  31. package/src/__tests__/harness-cache.test.ts +74 -0
  32. package/src/__tests__/harness.test.ts +469 -51
  33. package/src/__tests__/index.test.ts +61 -0
  34. package/src/cli-args.ts +6 -0
  35. package/src/crash-supervisor.ts +13 -6
  36. package/src/env.d.ts +1 -1
  37. package/src/errors.ts +5 -0
  38. package/src/harness.ts +504 -58
  39. package/src/index.ts +276 -64
  40. package/src/logs.ts +4 -0
  41. package/src/run.ts +20 -10
  42. package/src/setup.ts +22 -1
  43. package/tsconfig.json +3 -3
  44. package/tsconfig.lib.json +6 -6
  45. package/dist/cli-args.test.d.ts +0 -2
  46. package/dist/cli-args.test.d.ts.map +0 -1
  47. package/dist/cli-args.test.js +0 -1
  48. package/dist/crash-monitor.d.ts +0 -16
  49. package/dist/crash-monitor.d.ts.map +0 -1
  50. package/dist/crash-monitor.js +0 -75
  51. package/dist/event-mapper.d.ts +0 -16
  52. package/dist/event-mapper.d.ts.map +0 -1
  53. package/dist/event-mapper.js +0 -120
  54. package/dist/global-setup.d.ts +0 -3
  55. package/dist/global-setup.d.ts.map +0 -1
  56. package/dist/global-setup.js +0 -60
  57. package/dist/global-teardown.d.ts +0 -3
  58. package/dist/global-teardown.d.ts.map +0 -1
  59. package/dist/global-teardown.js +0 -11
  60. package/dist/harness-reporter.d.ts +0 -8
  61. package/dist/harness-reporter.d.ts.map +0 -1
  62. package/dist/harness-reporter.js +0 -12
  63. package/dist/jest-options.d.ts +0 -2
  64. package/dist/jest-options.d.ts.map +0 -1
  65. package/dist/jest-options.js +0 -1
  66. package/dist/types.d.ts +0 -17
  67. package/dist/types.d.ts.map +0 -1
  68. package/dist/types.js +0 -1
package/dist/harness.d.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  import { BridgeServer } from '@react-native-harness/bridge/server';
2
2
  import { HarnessContext, TestExecutionOptions, TestSuiteResult } from '@react-native-harness/bridge';
3
3
  import { type AppLaunchOptions, HarnessPlatform, HarnessPlatformRunner } from '@react-native-harness/platforms';
4
+ import { type MetroInstance } from '@react-native-harness/bundler-metro';
5
+ import { type HarnessPluginManager, type HarnessRunStatus, type HarnessRunSummary } from '@react-native-harness/plugins';
4
6
  import { Config as HarnessConfig } from '@react-native-harness/config';
5
7
  import { type CrashSupervisor } from './crash-supervisor.js';
6
8
  export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
9
+ export type HarnessRunState = {
10
+ runId: string;
11
+ startTime: number;
12
+ testFiles: string[];
13
+ watchMode: boolean;
14
+ coverageEnabled: boolean;
15
+ summary?: HarnessRunSummary;
16
+ status?: HarnessRunStatus;
17
+ error?: unknown;
18
+ };
7
19
  export type Harness = {
8
20
  context: HarnessContext;
9
21
  runTests: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
@@ -11,14 +23,24 @@ export type Harness = {
11
23
  restart: (testFilePath?: string) => Promise<void>;
12
24
  dispose: () => Promise<void>;
13
25
  crashSupervisor: CrashSupervisor;
26
+ callHook: HarnessPluginManager<HarnessConfig, HarnessPlatform>['callHook'];
27
+ setRunState: (runState: HarnessRunState | null) => void;
28
+ getRunState: () => HarnessRunState | null;
14
29
  };
30
+ export declare const maybeLogMetroCacheReuse: (config: HarnessConfig, platform: HarnessPlatform, projectRoot: string) => void;
15
31
  export declare const waitForAppReady: (options: {
32
+ metroInstance: MetroInstance;
16
33
  serverBridge: BridgeServer;
17
34
  platformInstance: HarnessPlatformRunner;
18
- bridgeTimeout: number;
35
+ platformId: string;
36
+ bundleStartTimeout: number;
37
+ readyTimeout: number;
38
+ maxAppRestarts: number;
19
39
  testFilePath: string;
20
40
  crashSupervisor: CrashSupervisor;
41
+ signal?: AbortSignal;
21
42
  appLaunchOptions?: AppLaunchOptions;
43
+ launchApp?: () => Promise<void>;
22
44
  }) => Promise<void>;
23
45
  export declare const getHarness: (config: HarnessConfig, platform: HarnessPlatform, projectRoot: string) => Promise<Harness>;
24
46
  //# sourceMappingURL=harness.d.ts.map
@@ -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,EACL,cAAc,EACd,oBAAoB,EACpB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,KAAK,gBAAgB,EACrB,eAAe,EACf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AAOzC,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAI/B,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,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;CAClC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,SAAS;IAC7C,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;IACjC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC,KAAG,OAAO,CAAC,IAAI,CA+Cf,CAAC;AAyIF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAkBjB,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,EACL,cAAc,EAGd,oBAAoB,EACpB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,gBAAgB,EACrB,eAAe,EACf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAIL,KAAK,aAAa,EAEnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAO/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,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,IAOF,CAAC;AAqBF,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;AAkeF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAsBjB,CAAC"}
package/dist/harness.js CHANGED
@@ -1,55 +1,158 @@
1
1
  import { getBridgeServer, } from '@react-native-harness/bridge/server';
2
- import { getMetroInstance, prewarmMetroBundle, } from '@react-native-harness/bundler-metro';
3
- import { createCrashArtifactWriter } from '@react-native-harness/tools';
2
+ import { getMetroInstance, isMetroCacheReusable, waitForMetroBackedAppReady, } from '@react-native-harness/bundler-metro';
3
+ import { createHarnessPluginManager, } from '@react-native-harness/plugins';
4
+ import { logger, createCrashArtifactWriter } from '@react-native-harness/tools';
4
5
  import { InitializationTimeoutError } from './errors.js';
5
6
  import { createCrashSupervisor, } from './crash-supervisor.js';
6
7
  import { createClientLogListener } from './client-log-handler.js';
7
- import { logMetroPrewarmCompleted } from './logs.js';
8
+ import path from 'node:path';
9
+ import { logMetroCacheReused } from './logs.js';
10
+ const harnessLogger = logger.child('runtime');
11
+ export const maybeLogMetroCacheReuse = (config, platform, projectRoot) => {
12
+ if (config.unstable__enableMetroCache &&
13
+ isMetroCacheReusable(projectRoot)) {
14
+ logMetroCacheReused(platform);
15
+ }
16
+ };
17
+ const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
18
+ const waitForAbort = (signal) => {
19
+ if (signal.aborted) {
20
+ return Promise.reject(signal.reason ?? createAbortError());
21
+ }
22
+ return new Promise((_, reject) => {
23
+ signal.addEventListener('abort', () => {
24
+ reject(signal.reason ?? createAbortError());
25
+ }, { once: true });
26
+ });
27
+ };
8
28
  export const waitForAppReady = async (options) => {
9
- const { serverBridge, platformInstance, bridgeTimeout, testFilePath, crashSupervisor, appLaunchOptions, } = options;
10
- const signal = AbortSignal.timeout(bridgeTimeout);
11
- return new Promise((resolve, reject) => {
12
- const launchApp = async () => {
29
+ const { metroInstance, serverBridge, platformInstance, platformId, bundleStartTimeout, readyTimeout, maxAppRestarts, testFilePath, crashSupervisor, appLaunchOptions, launchApp = () => platformInstance.restartApp(appLaunchOptions), } = options;
30
+ const signal = options.signal ?? new AbortController().signal;
31
+ const logWait = (message, ...args) => {
32
+ harnessLogger.debug(`waitForAppReady: ${message}`, ...args);
33
+ };
34
+ return await waitForMetroBackedAppReady({
35
+ metro: metroInstance,
36
+ platformId,
37
+ bundleStartTimeout,
38
+ readyTimeout,
39
+ maxAppRestarts,
40
+ signal,
41
+ startAttempt: async () => {
42
+ logWait('launching app for %s', testFilePath);
43
+ await launchApp();
44
+ logWait('launch request completed, waiting for bridge ready');
45
+ },
46
+ waitForReady: async (signal) => {
47
+ logWait('waiting for runtime ready');
48
+ return await Promise.race([
49
+ new Promise((resolve) => {
50
+ const onReady = () => {
51
+ cleanup();
52
+ crashSupervisor.markReady();
53
+ logWait('runtime ready received');
54
+ resolve();
55
+ };
56
+ const onAbort = () => {
57
+ cleanup();
58
+ };
59
+ const cleanup = () => {
60
+ serverBridge.off('ready', onReady);
61
+ signal.removeEventListener('abort', onAbort);
62
+ };
63
+ serverBridge.on('ready', onReady);
64
+ signal.addEventListener('abort', onAbort, { once: true });
65
+ }),
66
+ waitForAbort(signal),
67
+ ]);
68
+ },
69
+ waitForCrash: async (signal) => {
70
+ try {
71
+ logWait('waiting for crash or runtime ready');
72
+ return await Promise.race([
73
+ crashSupervisor.waitForCrash(testFilePath),
74
+ waitForAbort(signal),
75
+ ]);
76
+ }
77
+ finally {
78
+ crashSupervisor.cancelCrashWaiters();
79
+ }
80
+ },
81
+ onAttemptStart: () => {
82
+ logWait('beginning launch attempt for %s', testFilePath);
13
83
  crashSupervisor.beginLaunch(testFilePath);
14
- await platformInstance.restartApp(appLaunchOptions);
15
- };
16
- const onReady = () => {
17
- crashSupervisor.markReady();
18
- cleanup();
19
- resolve();
20
- };
21
- const onAbort = () => {
22
- cleanup();
23
- reject(new DOMException('The operation was aborted', 'AbortError'));
24
- };
25
- const cleanup = () => {
26
- serverBridge.off('ready', onReady);
27
- signal.removeEventListener('abort', onAbort);
84
+ },
85
+ onAttemptReset: () => {
86
+ logWait('resetting launch attempt state');
28
87
  crashSupervisor.cancelCrashWaiters();
29
- };
30
- signal.addEventListener('abort', onAbort);
31
- serverBridge.once('ready', onReady);
32
- void crashSupervisor.waitForCrash(testFilePath).catch((error) => {
33
- cleanup();
34
- reject(error);
35
- });
36
- void launchApp().catch((error) => {
37
- cleanup();
38
- reject(error);
39
- });
88
+ },
40
89
  });
41
90
  };
42
91
  const getHarnessInternal = async (config, platform, projectRoot, signal) => {
43
92
  const context = {
44
93
  platform,
45
94
  };
95
+ harnessLogger.debug('creating Harness internals for runner=%s platform=%s', platform.name, platform.platformId);
96
+ maybeLogMetroCacheReuse(config, platform, projectRoot);
97
+ const pluginAbortController = new AbortController();
98
+ const pluginManager = createHarnessPluginManager({
99
+ plugins: (config.plugins ?? []),
100
+ projectRoot,
101
+ config,
102
+ runner: platform,
103
+ abortSignal: pluginAbortController.signal,
104
+ });
105
+ let currentRun = null;
106
+ let activeTestFilePath;
107
+ const pendingHookPromises = new Set();
108
+ let pendingHookError;
109
+ const getCurrentRunId = () => currentRun?.runId;
110
+ const toRelativeTestFilePath = (testFilePath) => testFilePath == null ? undefined : path.relative(projectRoot, testFilePath);
111
+ const setActiveTestFilePath = (testFilePath) => {
112
+ activeTestFilePath = toRelativeTestFilePath(testFilePath);
113
+ };
114
+ const flushPendingHooks = async () => {
115
+ if (pendingHookPromises.size > 0) {
116
+ await Promise.allSettled([...pendingHookPromises]);
117
+ }
118
+ if (pendingHookError !== undefined) {
119
+ const error = pendingHookError;
120
+ pendingHookError = undefined;
121
+ throw error;
122
+ }
123
+ };
124
+ const trackHook = (promise) => {
125
+ const trackedPromise = promise
126
+ .catch((error) => {
127
+ pendingHookError ??= error;
128
+ })
129
+ .finally(() => {
130
+ pendingHookPromises.delete(trackedPromise);
131
+ });
132
+ pendingHookPromises.add(trackedPromise);
133
+ };
134
+ const scheduleHook = (name, payload) => {
135
+ trackHook(pluginManager.callHook(name, payload));
136
+ };
137
+ harnessLogger.debug('starting Metro, platform runner, and bridge initialization');
46
138
  const [metroInstance, platformInstance, serverBridge] = await Promise.all([
47
- getMetroInstance({ projectRoot, harnessConfig: config }, signal),
48
- import(platform.runner).then((module) => module.default(platform.config, config)),
139
+ getMetroInstance({ projectRoot, harnessConfig: config }, signal).then((instance) => {
140
+ harnessLogger.debug('Metro initialized');
141
+ return instance;
142
+ }),
143
+ import(platform.runner)
144
+ .then((module) => module.default(platform.config, config))
145
+ .then((instance) => {
146
+ harnessLogger.debug('platform runner initialized');
147
+ return instance;
148
+ }),
49
149
  getBridgeServer({
50
150
  port: config.webSocketPort,
51
151
  timeout: config.bridgeTimeout,
52
152
  context,
153
+ }).then((bridge) => {
154
+ harnessLogger.debug('bridge server initialized on port %d', config.webSocketPort);
155
+ return bridge;
53
156
  }),
54
157
  ]);
55
158
  const crashArtifactWriter = createCrashArtifactWriter({
@@ -61,67 +164,293 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
61
164
  });
62
165
  const appLaunchOptions = platform.config.appLaunchOptions;
63
166
  const clientLogListener = createClientLogListener();
167
+ const bridgeEventListener = (event) => {
168
+ const runId = getCurrentRunId();
169
+ if (!runId) {
170
+ return;
171
+ }
172
+ switch (event.type) {
173
+ case 'collection-started':
174
+ scheduleHook('collection:started', {
175
+ runId,
176
+ file: event.file,
177
+ });
178
+ break;
179
+ case 'collection-finished':
180
+ scheduleHook('collection:finished', {
181
+ runId,
182
+ file: event.file,
183
+ duration: event.duration,
184
+ totalTests: event.totalTests,
185
+ });
186
+ break;
187
+ case 'suite-started':
188
+ scheduleHook('suite:started', {
189
+ runId,
190
+ file: event.file,
191
+ name: event.name,
192
+ });
193
+ break;
194
+ case 'suite-finished':
195
+ scheduleHook('suite:finished', {
196
+ runId,
197
+ file: event.file,
198
+ name: event.name,
199
+ duration: event.duration,
200
+ status: event.status,
201
+ error: event.error,
202
+ });
203
+ break;
204
+ case 'test-started':
205
+ scheduleHook('test:started', {
206
+ runId,
207
+ file: event.file,
208
+ suite: event.suite,
209
+ name: event.name,
210
+ });
211
+ break;
212
+ case 'test-finished':
213
+ scheduleHook('test:finished', {
214
+ runId,
215
+ file: event.file,
216
+ suite: event.suite,
217
+ name: event.name,
218
+ duration: event.duration,
219
+ status: event.status,
220
+ error: event.error,
221
+ });
222
+ break;
223
+ case 'module-bundling-started':
224
+ scheduleHook('metro:bundle-started', {
225
+ runId,
226
+ target: 'module',
227
+ file: event.file,
228
+ });
229
+ break;
230
+ case 'module-bundling-finished':
231
+ scheduleHook('metro:bundle-finished', {
232
+ runId,
233
+ target: 'module',
234
+ file: event.file,
235
+ duration: event.duration,
236
+ });
237
+ break;
238
+ case 'module-bundling-failed':
239
+ scheduleHook('metro:bundle-failed', {
240
+ runId,
241
+ target: 'module',
242
+ file: event.file,
243
+ duration: event.duration,
244
+ error: event.error,
245
+ });
246
+ break;
247
+ case 'setup-file-bundling-started':
248
+ scheduleHook('metro:bundle-started', {
249
+ runId,
250
+ target: 'setupFile',
251
+ file: event.file,
252
+ setupType: event.setupType,
253
+ });
254
+ break;
255
+ case 'setup-file-bundling-finished':
256
+ scheduleHook('metro:bundle-finished', {
257
+ runId,
258
+ target: 'setupFile',
259
+ file: event.file,
260
+ setupType: event.setupType,
261
+ duration: event.duration,
262
+ });
263
+ break;
264
+ case 'setup-file-bundling-failed':
265
+ scheduleHook('metro:bundle-failed', {
266
+ runId,
267
+ target: 'setupFile',
268
+ file: event.file,
269
+ setupType: event.setupType,
270
+ duration: event.duration,
271
+ error: event.error,
272
+ });
273
+ break;
274
+ }
275
+ };
276
+ const onMetroEvent = (event) => {
277
+ const runId = getCurrentRunId();
278
+ if (runId && event.type === 'client_log') {
279
+ scheduleHook('metro:client-log', {
280
+ runId,
281
+ level: event.level,
282
+ data: event.data,
283
+ });
284
+ }
285
+ };
64
286
  const crashSupervisor = createCrashSupervisor({
65
287
  appMonitor,
66
288
  platformRunner: platformInstance,
67
289
  });
68
- serverBridge.on('ready', crashSupervisor.markReady);
290
+ const onReady = (device) => {
291
+ crashSupervisor.markReady();
292
+ const runId = getCurrentRunId();
293
+ if (!runId) {
294
+ return;
295
+ }
296
+ scheduleHook('runtime:ready', {
297
+ runId,
298
+ device,
299
+ });
300
+ };
301
+ const onDisconnect = () => {
302
+ const runId = getCurrentRunId();
303
+ if (!runId) {
304
+ return;
305
+ }
306
+ scheduleHook('runtime:disconnected', {
307
+ runId,
308
+ reason: 'bridge-disconnected',
309
+ });
310
+ };
311
+ const onAppMonitorEvent = (event) => {
312
+ const runId = getCurrentRunId();
313
+ if (!runId) {
314
+ return;
315
+ }
316
+ if (event.type === 'app_started') {
317
+ scheduleHook('app:started', {
318
+ runId,
319
+ testFile: activeTestFilePath,
320
+ pid: event.pid,
321
+ source: event.source,
322
+ line: event.line,
323
+ });
324
+ return;
325
+ }
326
+ if (event.type === 'app_exited') {
327
+ scheduleHook('app:exited', {
328
+ runId,
329
+ testFile: activeTestFilePath,
330
+ pid: event.pid,
331
+ source: event.source,
332
+ line: event.line,
333
+ isConfirmed: event.isConfirmed,
334
+ crashDetails: event.crashDetails,
335
+ });
336
+ return;
337
+ }
338
+ if (event.type === 'possible_crash') {
339
+ scheduleHook('app:possible-crash', {
340
+ runId,
341
+ testFile: activeTestFilePath,
342
+ pid: event.pid,
343
+ source: event.source,
344
+ line: event.line,
345
+ isConfirmed: event.isConfirmed,
346
+ crashDetails: event.crashDetails,
347
+ });
348
+ }
349
+ };
350
+ serverBridge.on('ready', onReady);
351
+ serverBridge.on('disconnect', onDisconnect);
352
+ serverBridge.on('event', bridgeEventListener);
353
+ metroInstance.events.addListener(onMetroEvent);
354
+ appMonitor.addListener(onAppMonitorEvent);
355
+ harnessLogger.debug('registered runtime, bridge, and Metro listeners');
69
356
  if (config.forwardClientLogs) {
70
357
  metroInstance.events.addListener(clientLogListener);
358
+ harnessLogger.debug('client log forwarding enabled');
71
359
  }
72
- const dispose = async () => {
360
+ const dispose = async (reason = 'normal') => {
361
+ harnessLogger.debug('disposing Harness (reason=%s)', reason);
362
+ let hookError;
363
+ try {
364
+ await flushPendingHooks();
365
+ await pluginManager.callHook('harness:before-dispose', {
366
+ runId: currentRun?.runId,
367
+ reason,
368
+ summary: currentRun?.summary,
369
+ status: currentRun?.status,
370
+ error: currentRun?.error,
371
+ });
372
+ await flushPendingHooks();
373
+ }
374
+ catch (error) {
375
+ hookError = error;
376
+ }
73
377
  if (config.forwardClientLogs) {
74
378
  metroInstance.events.removeListener(clientLogListener);
75
379
  }
76
- serverBridge.off('ready', crashSupervisor.markReady);
380
+ metroInstance.events.removeListener(onMetroEvent);
381
+ appMonitor.removeListener(onAppMonitorEvent);
382
+ serverBridge.off('ready', onReady);
383
+ serverBridge.off('disconnect', onDisconnect);
384
+ serverBridge.off('event', bridgeEventListener);
77
385
  await Promise.all([
78
386
  crashSupervisor.dispose(),
79
387
  serverBridge.dispose(),
80
388
  platformInstance.dispose(),
81
389
  metroInstance.dispose(),
82
390
  ]);
391
+ pluginAbortController.abort();
392
+ harnessLogger.debug('Harness resources disposed');
393
+ if (hookError) {
394
+ throw hookError;
395
+ }
83
396
  };
84
397
  if (signal.aborted) {
85
- await dispose();
398
+ await dispose('abort');
86
399
  throw new DOMException('The operation was aborted', 'AbortError');
87
400
  }
88
401
  try {
89
- await prewarmMetroBundle({
90
- projectRoot,
91
- entryPoint: config.entryPoint,
92
- platform: platform.platformId,
93
- dev: true,
94
- minify: false,
95
- signal,
402
+ await pluginManager.callHook('harness:before-creation', {
403
+ appLaunchOptions,
96
404
  });
97
- logMetroPrewarmCompleted(platform);
98
405
  await appMonitor.start();
406
+ harnessLogger.debug('app monitor started');
99
407
  }
100
408
  catch (error) {
101
- await dispose();
409
+ const runState = currentRun;
410
+ if (runState) {
411
+ runState.error = error;
412
+ currentRun = runState;
413
+ }
414
+ await dispose(error instanceof DOMException && error.name === 'AbortError' ? 'abort' : 'error');
102
415
  throw error;
103
416
  }
104
417
  const ensureAppReady = async (testFilePath) => {
418
+ await flushPendingHooks();
419
+ setActiveTestFilePath(testFilePath);
105
420
  crashSupervisor.setActiveTestFile(testFilePath);
421
+ harnessLogger.debug('ensuring app is ready for %s', testFilePath);
106
422
  if (crashSupervisor.isReady() && (await platformInstance.isAppRunning())) {
423
+ harnessLogger.debug('reusing existing ready app for %s', testFilePath);
107
424
  return;
108
425
  }
109
426
  crashSupervisor.reset();
427
+ harnessLogger.debug('app not ready, waiting for launch and runtime readiness');
110
428
  await waitForAppReady({
429
+ metroInstance,
111
430
  serverBridge,
112
431
  platformInstance: platformInstance,
113
- bridgeTimeout: config.bridgeTimeout,
432
+ platformId: platform.platformId,
433
+ bundleStartTimeout: config.bundleStartTimeout ?? 60000,
434
+ readyTimeout: config.bridgeTimeout,
435
+ maxAppRestarts: config.maxAppRestarts ?? 2,
114
436
  testFilePath,
115
437
  crashSupervisor,
116
438
  appLaunchOptions,
117
439
  });
440
+ await flushPendingHooks();
441
+ harnessLogger.debug('app is ready for %s', testFilePath);
118
442
  };
119
443
  const restart = async (testFilePath) => {
444
+ await flushPendingHooks();
120
445
  await crashSupervisor.stop();
446
+ setActiveTestFilePath(testFilePath);
447
+ harnessLogger.debug('restarting app (testFile=%s mode=%s)', testFilePath ?? 'n/a', testFilePath ? 'stop-and-ensure-ready' : 'direct-restart');
121
448
  if (testFilePath) {
449
+ harnessLogger.debug('stopping app before restart');
122
450
  await platformInstance.stopApp();
123
451
  }
124
452
  else {
453
+ harnessLogger.debug('requesting direct app restart');
125
454
  await platformInstance.restartApp(appLaunchOptions);
126
455
  }
127
456
  crashSupervisor.reset();
@@ -129,27 +458,44 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
129
458
  if (testFilePath) {
130
459
  await ensureAppReady(testFilePath);
131
460
  }
461
+ await flushPendingHooks();
462
+ harnessLogger.debug('restart completed');
132
463
  };
133
464
  return {
134
465
  context,
135
466
  runTests: async (path, options) => {
467
+ await flushPendingHooks();
468
+ activeTestFilePath = path;
136
469
  const client = serverBridge.rpc.clients.at(-1);
137
470
  if (!client) {
138
471
  throw new Error('No client found');
139
472
  }
140
- return await client.runTests(path, {
473
+ harnessLogger.debug('running test file on client: %s', path);
474
+ const result = await client.runTests(path, {
141
475
  ...options,
142
476
  runner: platform.runner,
143
477
  });
478
+ await flushPendingHooks();
479
+ return result;
144
480
  },
145
481
  ensureAppReady,
146
482
  restart,
147
- dispose,
483
+ dispose: () => dispose('normal'),
148
484
  crashSupervisor,
485
+ callHook: async (name, payload) => {
486
+ await flushPendingHooks();
487
+ await pluginManager.callHook(name, payload);
488
+ await flushPendingHooks();
489
+ },
490
+ setRunState: (runState) => {
491
+ currentRun = runState;
492
+ },
493
+ getRunState: () => currentRun,
149
494
  };
150
495
  };
151
496
  export const getHarness = async (config, platform, projectRoot) => {
152
497
  const abortSignal = AbortSignal.timeout(config.bridgeTimeout);
498
+ harnessLogger.debug('creating Harness with bridge timeout %dms', config.bridgeTimeout);
153
499
  try {
154
500
  const harness = await getHarnessInternal(config, platform, projectRoot, abortSignal);
155
501
  return harness;
@@ -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,EAAE,MAAM,cAAc,CAAC;AAe5C,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;CAgFjB"}
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"}