@react-native-harness/jest 1.1.0-rc.1 → 1.1.0-rc.3

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 (58) hide show
  1. package/dist/__tests__/errors.test.js +6 -1
  2. package/dist/__tests__/harness-cache.test.d.ts +2 -0
  3. package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
  4. package/dist/__tests__/harness-cache.test.js +51 -0
  5. package/dist/__tests__/harness.test.js +484 -47
  6. package/dist/__tests__/index.test.d.ts +2 -0
  7. package/dist/__tests__/index.test.d.ts.map +1 -0
  8. package/dist/__tests__/index.test.js +42 -0
  9. package/dist/__tests__/resource-lock.test.d.ts +2 -0
  10. package/dist/__tests__/resource-lock.test.d.ts.map +1 -0
  11. package/dist/__tests__/resource-lock.test.js +103 -0
  12. package/dist/action-hooks.d.ts +9 -0
  13. package/dist/action-hooks.d.ts.map +1 -0
  14. package/dist/action-hooks.js +36 -0
  15. package/dist/cli-args.d.ts +1 -0
  16. package/dist/cli-args.d.ts.map +1 -1
  17. package/dist/cli-args.js +5 -0
  18. package/dist/crash-supervisor.d.ts.map +1 -1
  19. package/dist/crash-supervisor.js +7 -6
  20. package/dist/errors.d.ts +5 -0
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +10 -2
  23. package/dist/harness.d.ts +23 -1
  24. package/dist/harness.d.ts.map +1 -1
  25. package/dist/harness.js +574 -136
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +205 -58
  28. package/dist/logs.d.ts +4 -0
  29. package/dist/logs.d.ts.map +1 -1
  30. package/dist/logs.js +12 -0
  31. package/dist/resource-lock.d.ts +25 -0
  32. package/dist/resource-lock.d.ts.map +1 -0
  33. package/dist/resource-lock.js +278 -0
  34. package/dist/run.d.ts +7 -1
  35. package/dist/run.d.ts.map +1 -1
  36. package/dist/run.js +13 -8
  37. package/dist/setup.d.ts.map +1 -1
  38. package/dist/setup.js +29 -2
  39. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  40. package/package.json +7 -6
  41. package/src/__tests__/errors.test.ts +18 -6
  42. package/src/__tests__/harness-cache.test.ts +74 -0
  43. package/src/__tests__/harness.test.ts +702 -49
  44. package/src/__tests__/index.test.ts +61 -0
  45. package/src/__tests__/resource-lock.test.ts +138 -0
  46. package/src/action-hooks.ts +66 -0
  47. package/src/cli-args.ts +6 -0
  48. package/src/crash-supervisor.ts +13 -6
  49. package/src/env.d.ts +1 -1
  50. package/src/errors.ts +15 -4
  51. package/src/harness.ts +743 -157
  52. package/src/index.ts +276 -64
  53. package/src/logs.ts +17 -3
  54. package/src/resource-lock.ts +450 -0
  55. package/src/run.ts +20 -10
  56. package/src/setup.ts +42 -1
  57. package/tsconfig.json +3 -3
  58. package/tsconfig.lib.json +6 -6
@@ -0,0 +1,450 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { logger, type HarnessLogger } from '@react-native-harness/tools';
6
+
7
+ const resourceLockLogger = logger.child('resource-lock');
8
+
9
+ const DEFAULT_ROOT_DIR = path.join(os.tmpdir(), 'react-native-harness-locks');
10
+ const DEFAULT_POLL_INTERVAL_MS = 1000;
11
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 2000;
12
+ const DEFAULT_STALE_LOCK_TIMEOUT_MS = 15000;
13
+
14
+ type ResourceLockMetadata = {
15
+ ticketId: string;
16
+ key: string;
17
+ pid: number;
18
+ createdAt: number;
19
+ heartbeatAt: number;
20
+ };
21
+
22
+ export type ResourceLockAcquireOptions = {
23
+ signal?: AbortSignal;
24
+ onWait?: () => void;
25
+ onStillWaiting?: (elapsedMs: number) => void;
26
+ };
27
+
28
+ export type ResourceLease = {
29
+ release: () => Promise<void>;
30
+ };
31
+
32
+ export type ResourceLockManager = {
33
+ acquire: (
34
+ key: string,
35
+ options?: ResourceLockAcquireOptions
36
+ ) => Promise<ResourceLease>;
37
+ };
38
+
39
+ type ResourceLockManagerOptions = {
40
+ rootDir?: string;
41
+ pollIntervalMs?: number;
42
+ heartbeatIntervalMs?: number;
43
+ staleLockTimeoutMs?: number;
44
+ pid?: number;
45
+ logger?: HarnessLogger;
46
+ isProcessActive?: (pid: number) => boolean;
47
+ };
48
+
49
+ type LockPaths = {
50
+ rootDir: string;
51
+ keyDir: string;
52
+ queueDir: string;
53
+ ownerFilePath: string;
54
+ };
55
+
56
+ const wait = async (ms: number): Promise<void> => {
57
+ await new Promise((resolve) => setTimeout(resolve, ms));
58
+ };
59
+
60
+ const createAbortError = () =>
61
+ new DOMException('The operation was aborted', 'AbortError');
62
+
63
+ export const hashResourceLockKey = (key: string): string => {
64
+ return crypto.createHash('sha256').update(key).digest('hex');
65
+ };
66
+
67
+ const getPathsForKey = (rootDir: string, key: string): LockPaths => {
68
+ const keyDir = path.join(rootDir, hashResourceLockKey(key));
69
+ return {
70
+ rootDir,
71
+ keyDir,
72
+ queueDir: path.join(keyDir, 'queue'),
73
+ ownerFilePath: path.join(keyDir, 'owner.json'),
74
+ };
75
+ };
76
+
77
+ const createTicketId = (createdAt: number, pid: number): string => {
78
+ return `${createdAt
79
+ .toString()
80
+ .padStart(16, '0')}-${pid}-${crypto.randomUUID()}`;
81
+ };
82
+
83
+ const isMissingFileError = (error: unknown): boolean => {
84
+ return (
85
+ error instanceof Error &&
86
+ 'code' in error &&
87
+ typeof error.code === 'string' &&
88
+ error.code === 'ENOENT'
89
+ );
90
+ };
91
+
92
+ const isExclusiveCreateError = (error: unknown): boolean => {
93
+ return (
94
+ error instanceof Error &&
95
+ 'code' in error &&
96
+ typeof error.code === 'string' &&
97
+ error.code === 'EEXIST'
98
+ );
99
+ };
100
+
101
+ const ensureLockDirectories = async (paths: LockPaths): Promise<void> => {
102
+ await fs.mkdir(paths.queueDir, { recursive: true });
103
+ };
104
+
105
+ const readJsonFile = async <T>(filePath: string): Promise<T | null> => {
106
+ try {
107
+ const content = await fs.readFile(filePath, 'utf8');
108
+ return JSON.parse(content) as T;
109
+ } catch (error) {
110
+ if (isMissingFileError(error)) {
111
+ return null;
112
+ }
113
+
114
+ throw error;
115
+ }
116
+ };
117
+
118
+ const removeFileIfPresent = async (filePath: string): Promise<void> => {
119
+ try {
120
+ await fs.rm(filePath, { force: true });
121
+ } catch (error) {
122
+ if (!isMissingFileError(error)) {
123
+ throw error;
124
+ }
125
+ }
126
+ };
127
+
128
+ const isPidActive = (pid: number): boolean => {
129
+ try {
130
+ process.kill(pid, 0);
131
+ return true;
132
+ } catch (error) {
133
+ return !(
134
+ error instanceof Error &&
135
+ 'code' in error &&
136
+ typeof error.code === 'string' &&
137
+ error.code === 'ESRCH'
138
+ );
139
+ }
140
+ };
141
+
142
+ const isMetadataStale = (
143
+ metadata: ResourceLockMetadata,
144
+ now: number,
145
+ staleLockTimeoutMs: number,
146
+ isProcessActive: (pid: number) => boolean
147
+ ): boolean => {
148
+ if (!isProcessActive(metadata.pid)) {
149
+ return true;
150
+ }
151
+
152
+ return now - metadata.heartbeatAt > staleLockTimeoutMs;
153
+ };
154
+
155
+ const isQueuedTicketStale = (
156
+ metadata: ResourceLockMetadata,
157
+ isProcessActive: (pid: number) => boolean
158
+ ): boolean => {
159
+ return !isProcessActive(metadata.pid);
160
+ };
161
+
162
+ const waitForPollInterval = (
163
+ ms: number,
164
+ signal?: AbortSignal
165
+ ): Promise<void> => {
166
+ if (!signal) {
167
+ return wait(ms);
168
+ }
169
+
170
+ if (signal.aborted) {
171
+ return Promise.reject(signal.reason ?? createAbortError());
172
+ }
173
+
174
+ return new Promise((resolve, reject) => {
175
+ const timer = setTimeout(() => {
176
+ signal.removeEventListener('abort', onAbort);
177
+ resolve();
178
+ }, ms);
179
+ const onAbort = () => {
180
+ clearTimeout(timer);
181
+ signal.removeEventListener('abort', onAbort);
182
+ reject(signal.reason ?? createAbortError());
183
+ };
184
+
185
+ signal.addEventListener('abort', onAbort, { once: true });
186
+ });
187
+ };
188
+
189
+ const readQueueTickets = async (
190
+ queueDir: string
191
+ ): Promise<ResourceLockMetadata[]> => {
192
+ const ticketEntries = await fs.readdir(queueDir, { withFileTypes: true });
193
+ const tickets = await Promise.all(
194
+ ticketEntries
195
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
196
+ .map(async (entry) => ({
197
+ name: entry.name,
198
+ metadata: await readJsonFile<ResourceLockMetadata>(
199
+ path.join(queueDir, entry.name)
200
+ ),
201
+ }))
202
+ );
203
+
204
+ return tickets
205
+ .filter(
206
+ (entry): entry is { name: string; metadata: ResourceLockMetadata } =>
207
+ entry.metadata !== null
208
+ )
209
+ .sort((left, right) => left.name.localeCompare(right.name))
210
+ .map((entry) => entry.metadata);
211
+ };
212
+
213
+ const cleanupQueue = async (options: {
214
+ paths: LockPaths;
215
+ currentTicketId: string;
216
+ logger: HarnessLogger;
217
+ isProcessActive: (pid: number) => boolean;
218
+ }): Promise<ResourceLockMetadata[]> => {
219
+ const { paths, currentTicketId, logger, isProcessActive } = options;
220
+ const tickets = await readQueueTickets(paths.queueDir);
221
+ const activeTickets: ResourceLockMetadata[] = [];
222
+
223
+ for (const ticket of tickets) {
224
+ const isCurrentTicket = ticket.ticketId === currentTicketId;
225
+ const isStale =
226
+ !isCurrentTicket && isQueuedTicketStale(ticket, isProcessActive);
227
+
228
+ if (isStale) {
229
+ logger.debug(
230
+ 'removing stale queued ticket %s for key %s',
231
+ ticket.ticketId,
232
+ ticket.key
233
+ );
234
+ await removeFileIfPresent(
235
+ path.join(paths.queueDir, `${ticket.ticketId}.json`)
236
+ );
237
+ continue;
238
+ }
239
+
240
+ activeTickets.push(ticket);
241
+ }
242
+
243
+ return activeTickets;
244
+ };
245
+
246
+ const maybeClearStaleOwner = async (options: {
247
+ ownerFilePath: string;
248
+ staleLockTimeoutMs: number;
249
+ now: number;
250
+ logger: HarnessLogger;
251
+ isProcessActive: (pid: number) => boolean;
252
+ }): Promise<ResourceLockMetadata | null> => {
253
+ const { ownerFilePath, staleLockTimeoutMs, now, logger, isProcessActive } =
254
+ options;
255
+ const owner = await readJsonFile<ResourceLockMetadata>(ownerFilePath);
256
+
257
+ if (!owner) {
258
+ return null;
259
+ }
260
+
261
+ if (!isMetadataStale(owner, now, staleLockTimeoutMs, isProcessActive)) {
262
+ return owner;
263
+ }
264
+
265
+ logger.debug(
266
+ 'removing stale owner ticket %s for key %s',
267
+ owner.ticketId,
268
+ owner.key
269
+ );
270
+ await removeFileIfPresent(ownerFilePath);
271
+ return null;
272
+ };
273
+
274
+ const claimOwnership = async (
275
+ ownerFilePath: string,
276
+ metadata: ResourceLockMetadata
277
+ ): Promise<boolean> => {
278
+ try {
279
+ await fs.writeFile(ownerFilePath, JSON.stringify(metadata), {
280
+ encoding: 'utf8',
281
+ flag: 'wx',
282
+ });
283
+ return true;
284
+ } catch (error) {
285
+ if (isExclusiveCreateError(error)) {
286
+ return false;
287
+ }
288
+
289
+ throw error;
290
+ }
291
+ };
292
+
293
+ export const createResourceLockManager = (
294
+ options: ResourceLockManagerOptions = {}
295
+ ): ResourceLockManager => {
296
+ const rootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
297
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
298
+ const heartbeatIntervalMs =
299
+ options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
300
+ const staleLockTimeoutMs =
301
+ options.staleLockTimeoutMs ?? DEFAULT_STALE_LOCK_TIMEOUT_MS;
302
+ const pid = options.pid ?? process.pid;
303
+ const scopedLogger = options.logger ?? resourceLockLogger;
304
+ const isProcessActive = options.isProcessActive ?? isPidActive;
305
+
306
+ return {
307
+ acquire: async (key, acquireOptions = {}) => {
308
+ const paths = getPathsForKey(rootDir, key);
309
+ await ensureLockDirectories(paths);
310
+
311
+ const createdAt = Date.now();
312
+ const ticketId = createTicketId(createdAt, pid);
313
+ const ticketPath = path.join(paths.queueDir, `${ticketId}.json`);
314
+ const metadata: ResourceLockMetadata = {
315
+ ticketId,
316
+ key,
317
+ pid,
318
+ createdAt,
319
+ heartbeatAt: createdAt,
320
+ };
321
+
322
+ await fs.writeFile(ticketPath, JSON.stringify(metadata), 'utf8');
323
+ scopedLogger.debug('queued ticket %s for key %s', ticketId, key);
324
+
325
+ let heartbeatTimer: NodeJS.Timeout | null = null;
326
+ let released = false;
327
+ let didNotifyWait = false;
328
+ const waitStartedAt = Date.now();
329
+
330
+ const release = async () => {
331
+ if (released) {
332
+ return;
333
+ }
334
+
335
+ released = true;
336
+ if (heartbeatTimer) {
337
+ clearInterval(heartbeatTimer);
338
+ heartbeatTimer = null;
339
+ }
340
+
341
+ const owner = await readJsonFile<ResourceLockMetadata>(
342
+ paths.ownerFilePath
343
+ );
344
+ if (owner?.ticketId === ticketId) {
345
+ await removeFileIfPresent(paths.ownerFilePath);
346
+ }
347
+
348
+ await removeFileIfPresent(ticketPath);
349
+ scopedLogger.debug('released ticket %s for key %s', ticketId, key);
350
+ };
351
+
352
+ const startHeartbeat = () => {
353
+ heartbeatTimer = setInterval(async () => {
354
+ const nextHeartbeatAt = Date.now();
355
+ const owner = await readJsonFile<ResourceLockMetadata>(
356
+ paths.ownerFilePath
357
+ );
358
+
359
+ if (released || owner?.ticketId !== ticketId) {
360
+ return;
361
+ }
362
+
363
+ const nextMetadata: ResourceLockMetadata = {
364
+ ...owner,
365
+ heartbeatAt: nextHeartbeatAt,
366
+ };
367
+
368
+ if (released) {
369
+ return;
370
+ }
371
+
372
+ await fs.writeFile(
373
+ paths.ownerFilePath,
374
+ JSON.stringify(nextMetadata),
375
+ 'utf8'
376
+ );
377
+ scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
378
+ }, heartbeatIntervalMs);
379
+ heartbeatTimer.unref?.();
380
+ };
381
+
382
+ try {
383
+ while (true) {
384
+ acquireOptions.signal?.throwIfAborted();
385
+
386
+ const now = Date.now();
387
+ const activeTickets = await cleanupQueue({
388
+ paths,
389
+ currentTicketId: ticketId,
390
+ logger: scopedLogger,
391
+ isProcessActive,
392
+ });
393
+ const ownIndex = activeTickets.findIndex(
394
+ (entry) => entry.ticketId === ticketId
395
+ );
396
+
397
+ if (ownIndex === -1) {
398
+ throw new Error(
399
+ `Queued ticket ${ticketId} disappeared before acquisition.`
400
+ );
401
+ }
402
+
403
+ const owner = await maybeClearStaleOwner({
404
+ ownerFilePath: paths.ownerFilePath,
405
+ staleLockTimeoutMs,
406
+ now,
407
+ logger: scopedLogger,
408
+ isProcessActive,
409
+ });
410
+
411
+ if (ownIndex === 0 && owner === null) {
412
+ const claimed = await claimOwnership(paths.ownerFilePath, {
413
+ ...metadata,
414
+ heartbeatAt: Date.now(),
415
+ });
416
+
417
+ if (claimed) {
418
+ await removeFileIfPresent(ticketPath);
419
+ startHeartbeat();
420
+ scopedLogger.debug(
421
+ 'acquired lock for key %s with ticket %s',
422
+ key,
423
+ ticketId
424
+ );
425
+ return { release };
426
+ }
427
+ }
428
+
429
+ if (!didNotifyWait) {
430
+ didNotifyWait = true;
431
+ acquireOptions.onWait?.();
432
+ }
433
+
434
+ acquireOptions.onStillWaiting?.(Date.now() - waitStartedAt);
435
+ scopedLogger.debug(
436
+ 'waiting for key %s with ticket %s at queue position %d',
437
+ key,
438
+ ticketId,
439
+ ownIndex + 1
440
+ );
441
+
442
+ await waitForPollInterval(pollIntervalMs, acquireOptions.signal);
443
+ }
444
+ } catch (error) {
445
+ await release();
446
+ throw error;
447
+ }
448
+ },
449
+ };
450
+ };
package/src/run.ts CHANGED
@@ -65,7 +65,12 @@ export type RunHarnessTestFile = (options: {
65
65
  harness: Harness;
66
66
  projectConfig: JestConfig.ProjectConfig;
67
67
  globalConfig: JestConfig.GlobalConfig;
68
- }) => Promise<JestTestResult>;
68
+ }) => Promise<{
69
+ jestResult: JestTestResult;
70
+ harnessResult: HarnessTestSuiteResult;
71
+ relativeTestPath: string;
72
+ duration: number;
73
+ }>;
69
74
 
70
75
  export const runHarnessTestFile: RunHarnessTestFile = async ({
71
76
  testPath,
@@ -84,7 +89,7 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
84
89
  (setupFile) => path.relative(globalConfig.rootDir, setupFile)
85
90
  );
86
91
 
87
- const results = await harness.runTests(relativeTestPath, {
92
+ const harnessResult = await harness.runTests(relativeTestPath, {
88
93
  testNamePattern: globalConfig.testNamePattern,
89
94
  setupFiles,
90
95
  setupFilesAfterEnv,
@@ -92,7 +97,7 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
92
97
  });
93
98
  const end = Date.now();
94
99
 
95
- const allTests = flattenTests(results);
100
+ const allTests = flattenTests(harnessResult);
96
101
  const stats = calculateStats(allTests);
97
102
 
98
103
  // Convert TestResult[] to the format expected by toTestResult
@@ -115,12 +120,12 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
115
120
  });
116
121
 
117
122
  // Check if the entire suite was skipped
118
- const skipped = results.status === 'skipped';
123
+ const skipped = harnessResult.status === 'skipped';
119
124
 
120
125
  // Get error message from suite if it failed
121
- const errorMessage = results.error?.message || null;
126
+ const errorMessage = harnessResult.error?.message || null;
122
127
 
123
- const totalResults = toTestResult({
128
+ const jestResult = toTestResult({
124
129
  stats: {
125
130
  failures: stats.failures,
126
131
  pending: stats.pending,
@@ -133,15 +138,20 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
133
138
  errorMessage,
134
139
  tests,
135
140
  jestTestPath: testPath,
136
- coverage: results.coverage as JestTestResult['coverage'],
141
+ coverage: harnessResult.coverage as JestTestResult['coverage'],
137
142
  });
138
143
 
139
- totalResults.failureMessage = formatResultsErrors(
140
- totalResults.testResults,
144
+ jestResult.failureMessage = formatResultsErrors(
145
+ jestResult.testResults,
141
146
  projectConfig,
142
147
  globalConfig,
143
148
  testPath
144
149
  );
145
150
 
146
- return totalResults;
151
+ return {
152
+ jestResult,
153
+ harnessResult,
154
+ relativeTestPath,
155
+ duration: end - start,
156
+ };
147
157
  };
package/src/setup.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getConfig,
3
3
  type Config as HarnessConfig,
4
+ ConfigSchema,
4
5
  } from '@react-native-harness/config';
5
6
  import type { Config as JestConfig } from 'jest-runner';
6
7
  import { getHarness } from './harness.js';
@@ -9,12 +10,18 @@ import { getAdditionalCliArgs, HarnessCliArgs } from './cli-args.js';
9
10
  import { logTestEnvironmentReady, logTestRunHeader } from './logs.js';
10
11
  import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
11
12
  import { HarnessPlatform } from '@react-native-harness/platforms';
13
+ import { logger } from '@react-native-harness/tools';
14
+ import { createActionHooksPlugin } from './action-hooks.js';
15
+
16
+ const setupLogger = logger.child('setup');
12
17
 
13
18
  const getHarnessConfig = async (
14
19
  globalConfig: JestConfig.GlobalConfig
15
20
  ): Promise<HarnessConfig> => {
16
21
  const projectRoot = globalConfig.rootDir;
22
+ setupLogger.debug('loading Harness config from %s', projectRoot);
17
23
  const { config: harnessConfig } = await getConfig(projectRoot);
24
+ setupLogger.debug('loaded Harness config');
18
25
  return harnessConfig;
19
26
  };
20
27
 
@@ -36,12 +43,13 @@ const getHarnessRunner = (
36
43
  throw new RunnerNotFoundError(selectedRunnerName);
37
44
  }
38
45
 
46
+ setupLogger.debug('selected runner %s (%s)', runner.name, runner.platformId);
39
47
  return runner;
40
48
  };
41
49
 
42
50
  export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
43
51
  preRunMessage.remove(process.stderr);
44
- const harnessConfig =
52
+ let harnessConfig =
45
53
  global.HARNESS_CONFIG ?? (await getHarnessConfig(globalConfig));
46
54
 
47
55
  if (global.HARNESS) {
@@ -57,8 +65,36 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
57
65
  });
58
66
 
59
67
  const cliArgs = getAdditionalCliArgs();
68
+
69
+ if (cliArgs.metroPort != null) {
70
+ setupLogger.debug(
71
+ 'applying CLI metro port override: %d',
72
+ cliArgs.metroPort
73
+ );
74
+ harnessConfig = ConfigSchema.parse({
75
+ ...harnessConfig,
76
+ metroPort: cliArgs.metroPort,
77
+ });
78
+ }
79
+
80
+ if (process.env.PRE_RUN_HOOK || process.env.AFTER_RUN_HOOK) {
81
+ harnessConfig = ConfigSchema.parse({
82
+ ...harnessConfig,
83
+ plugins: [...(harnessConfig.plugins ?? []), createActionHooksPlugin()],
84
+ });
85
+ }
86
+
60
87
  const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);
61
88
 
89
+ if (
90
+ harnessConfig.webSocketPort != null &&
91
+ harnessConfig.webSocketPort !== harnessConfig.metroPort
92
+ ) {
93
+ logger.warn(
94
+ `Config option "webSocketPort" is deprecated and ignored. Harness now uses metroPort (${harnessConfig.metroPort}) for bridge traffic.`
95
+ );
96
+ }
97
+
62
98
  if (globalConfig.collectCoverage) {
63
99
  // This is going to be used by @react-native-harness/babel-preset
64
100
  // to enable instrumentation of test files.
@@ -67,19 +103,24 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
67
103
  if (harnessConfig.coverage?.root) {
68
104
  process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverage.root;
69
105
  }
106
+
107
+ setupLogger.debug('coverage enabled for this run');
70
108
  }
71
109
 
72
110
  if (harnessConfig.disableViewFlattening) {
73
111
  process.env.RN_HARNESS_VIEW_FLATTENING = 'false';
112
+ setupLogger.debug('view flattening disabled for runtime');
74
113
  }
75
114
 
76
115
  logTestRunHeader(selectedRunner);
116
+ setupLogger.debug('creating Harness instance');
77
117
  const harness = await getHarness(
78
118
  harnessConfig,
79
119
  selectedRunner,
80
120
  globalConfig.rootDir
81
121
  );
82
122
  logTestEnvironmentReady(selectedRunner);
123
+ setupLogger.debug('Harness instance is ready');
83
124
 
84
125
  global.HARNESS_CONFIG = harnessConfig;
85
126
  global.HARNESS = harness;
package/tsconfig.json CHANGED
@@ -3,15 +3,15 @@
3
3
  "files": [],
4
4
  "include": [],
5
5
  "references": [
6
- {
7
- "path": "../cli"
8
- },
9
6
  {
10
7
  "path": "../tools"
11
8
  },
12
9
  {
13
10
  "path": "../platforms"
14
11
  },
12
+ {
13
+ "path": "../plugins"
14
+ },
15
15
  {
16
16
  "path": "../config"
17
17
  },
package/tsconfig.lib.json CHANGED
@@ -8,24 +8,24 @@
8
8
  "emitDeclarationOnly": false,
9
9
  "forceConsistentCasingInFileNames": true,
10
10
  "types": ["node"],
11
- "lib": ["DOM"]
11
+ "lib": ["DOM", "ES2022"]
12
12
  },
13
13
  "include": ["src/**/*.ts"],
14
14
  "references": [
15
15
  {
16
- "path": "../cli/tsconfig.lib.json"
16
+ "path": "../tools/tsconfig.lib.json"
17
17
  },
18
18
  {
19
- "path": "../bundler-metro/tsconfig.lib.json"
19
+ "path": "../platforms/tsconfig.lib.json"
20
20
  },
21
21
  {
22
- "path": "../tools/tsconfig.lib.json"
22
+ "path": "../plugins/tsconfig.lib.json"
23
23
  },
24
24
  {
25
- "path": "../platforms/tsconfig.lib.json"
25
+ "path": "../config/tsconfig.lib.json"
26
26
  },
27
27
  {
28
- "path": "../config/tsconfig.lib.json"
28
+ "path": "../bundler-metro/tsconfig.lib.json"
29
29
  },
30
30
  {
31
31
  "path": "../bridge/tsconfig.lib.json"