@react-native-harness/jest 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/cli-args.d.ts +3 -4
  2. package/dist/cli-args.d.ts.map +1 -1
  3. package/dist/cli-args.js +3 -8
  4. package/dist/diagnostics/index.d.ts +4 -1
  5. package/dist/diagnostics/index.d.ts.map +1 -1
  6. package/dist/diagnostics/index.js +2 -1
  7. package/dist/diagnostics/report.d.ts +81 -1
  8. package/dist/diagnostics/report.d.ts.map +1 -1
  9. package/dist/diagnostics/report.js +225 -3
  10. package/dist/diagnostics/telemetry.d.ts +42 -0
  11. package/dist/diagnostics/telemetry.d.ts.map +1 -0
  12. package/dist/diagnostics/telemetry.js +120 -0
  13. package/dist/errors.d.ts +8 -2
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +29 -3
  16. package/dist/execute-run.d.ts.map +1 -1
  17. package/dist/execute-run.js +73 -21
  18. package/dist/format-harness-error.d.ts.map +1 -1
  19. package/dist/format-harness-error.js +3 -0
  20. package/dist/harness-session.d.ts +8 -1
  21. package/dist/harness-session.d.ts.map +1 -1
  22. package/dist/harness-session.js +106 -22
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -7
  25. package/dist/jest-preset.d.ts +3 -0
  26. package/dist/jest-preset.d.ts.map +1 -1
  27. package/dist/jest-preset.js +18 -0
  28. package/dist/relative-path.d.ts +8 -0
  29. package/dist/relative-path.d.ts.map +1 -0
  30. package/dist/relative-path.js +8 -0
  31. package/dist/resource-lock.d.ts.map +1 -1
  32. package/dist/resource-lock.js +47 -27
  33. package/dist/run.d.ts.map +1 -1
  34. package/dist/run.js +4 -4
  35. package/dist/watch-session.d.ts +6 -0
  36. package/dist/watch-session.d.ts.map +1 -0
  37. package/dist/watch-session.js +37 -0
  38. package/package.json +8 -8
  39. package/src/cli-args.ts +5 -13
  40. package/src/diagnostics/index.ts +12 -1
  41. package/src/diagnostics/report.ts +326 -5
  42. package/src/diagnostics/telemetry.ts +186 -0
  43. package/src/errors.ts +42 -5
  44. package/src/execute-run.ts +93 -23
  45. package/src/format-harness-error.ts +5 -0
  46. package/src/harness-session.ts +139 -21
  47. package/src/index.ts +8 -7
  48. package/src/jest-preset.ts +18 -0
  49. package/src/relative-path.ts +10 -0
  50. package/src/resource-lock.ts +64 -33
  51. package/src/run.ts +4 -4
  52. package/src/watch-session.ts +65 -0
  53. package/dist/crash-supervisor.d.ts +0 -21
  54. package/dist/crash-supervisor.d.ts.map +0 -1
  55. package/dist/crash-supervisor.js +0 -210
  56. package/dist/event-mapper.d.ts +0 -16
  57. package/dist/event-mapper.d.ts.map +0 -1
  58. package/dist/event-mapper.js +0 -120
  59. package/dist/global-setup.d.ts +0 -3
  60. package/dist/global-setup.d.ts.map +0 -1
  61. package/dist/global-setup.js +0 -60
  62. package/dist/global-teardown.d.ts +0 -3
  63. package/dist/global-teardown.d.ts.map +0 -1
  64. package/dist/global-teardown.js +0 -11
  65. package/dist/harness-reporter.d.ts +0 -8
  66. package/dist/harness-reporter.d.ts.map +0 -1
  67. package/dist/harness-reporter.js +0 -12
  68. package/dist/harness.d.ts +0 -47
  69. package/dist/harness.d.ts.map +0 -1
  70. package/dist/harness.js +0 -619
  71. package/dist/jest-options.d.ts +0 -2
  72. package/dist/jest-options.d.ts.map +0 -1
  73. package/dist/jest-options.js +0 -1
  74. package/dist/setup.d.ts +0 -3
  75. package/dist/setup.d.ts.map +0 -1
  76. package/dist/setup.js +0 -80
  77. package/dist/teardown.d.ts +0 -3
  78. package/dist/teardown.d.ts.map +0 -1
  79. package/dist/teardown.js +0 -10
  80. package/dist/types.d.ts +0 -17
  81. package/dist/types.d.ts.map +0 -1
  82. package/dist/types.js +0 -1
@@ -89,6 +89,37 @@ const isMissingFileError = (error: unknown): boolean => {
89
89
  );
90
90
  };
91
91
 
92
+ // On Windows, a file being deleted by another process can briefly fail
93
+ // reads and deletes with EPERM/EBUSY instead of ENOENT.
94
+ const isTransientFsError = (error: unknown): boolean => {
95
+ return (
96
+ error instanceof Error &&
97
+ 'code' in error &&
98
+ (error.code === 'EPERM' || error.code === 'EBUSY')
99
+ );
100
+ };
101
+
102
+ const TRANSIENT_FS_RETRIES = 5;
103
+ const TRANSIENT_FS_RETRY_DELAY_MS = 20;
104
+
105
+ const retryTransientFsError = async <T>(
106
+ operation: () => Promise<T>,
107
+ ): Promise<T> => {
108
+ for (let attempt = 1; ; attempt += 1) {
109
+ try {
110
+ return await operation();
111
+ } catch (error) {
112
+ if (!isTransientFsError(error) || attempt >= TRANSIENT_FS_RETRIES) {
113
+ throw error;
114
+ }
115
+
116
+ await new Promise((resolve) =>
117
+ setTimeout(resolve, TRANSIENT_FS_RETRY_DELAY_MS),
118
+ );
119
+ }
120
+ }
121
+ };
122
+
92
123
  const isExclusiveCreateError = (error: unknown): boolean => {
93
124
  return (
94
125
  error instanceof Error &&
@@ -104,7 +135,9 @@ const ensureLockDirectories = async (paths: LockPaths): Promise<void> => {
104
135
 
105
136
  const readJsonFile = async <T>(filePath: string): Promise<T | null> => {
106
137
  try {
107
- const content = await fs.readFile(filePath, 'utf8');
138
+ const content = await retryTransientFsError(() =>
139
+ fs.readFile(filePath, 'utf8'),
140
+ );
108
141
  return JSON.parse(content) as T;
109
142
  } catch (error) {
110
143
  if (isMissingFileError(error)) {
@@ -132,7 +165,7 @@ const writeJsonFileAtomic = async <T>(
132
165
 
133
166
  const removeFileIfPresent = async (filePath: string): Promise<void> => {
134
167
  try {
135
- await fs.rm(filePath, { force: true });
168
+ await retryTransientFsError(() => fs.rm(filePath, { force: true }));
136
169
  } catch (error) {
137
170
  if (!isMissingFileError(error)) {
138
171
  throw error;
@@ -338,7 +371,7 @@ export const createResourceLockManager = (
338
371
  scopedLogger.debug('queued ticket %s for key %s', ticketId, key);
339
372
 
340
373
  let heartbeatTimer: NodeJS.Timeout | null = null;
341
- let heartbeatInFlight = false;
374
+ let heartbeatInFlight: Promise<void> | null = null;
342
375
  let released = false;
343
376
  let didNotifyWait = false;
344
377
  const waitStartedAt = Date.now();
@@ -354,6 +387,10 @@ export const createResourceLockManager = (
354
387
  heartbeatTimer = null;
355
388
  }
356
389
 
390
+ // Let an in-flight refresh finish so it cannot recreate owner.json
391
+ // after we remove it.
392
+ await heartbeatInFlight;
393
+
357
394
  const owner = await readJsonFile<ResourceLockMetadata>(
358
395
  paths.ownerFilePath,
359
396
  );
@@ -366,13 +403,7 @@ export const createResourceLockManager = (
366
403
  };
367
404
 
368
405
  const startHeartbeat = () => {
369
- heartbeatTimer = setInterval(async () => {
370
- if (heartbeatInFlight) {
371
- return;
372
- }
373
-
374
- heartbeatInFlight = true;
375
-
406
+ const refreshHeartbeat = async () => {
376
407
  try {
377
408
  const nextHeartbeatAt = Date.now();
378
409
  const owner = await readJsonFile<ResourceLockMetadata>(
@@ -383,33 +414,33 @@ export const createResourceLockManager = (
383
414
  return;
384
415
  }
385
416
 
386
- const nextMetadata: ResourceLockMetadata = {
417
+ await writeJsonFileAtomic(paths.ownerFilePath, {
387
418
  ...owner,
388
419
  heartbeatAt: nextHeartbeatAt,
389
- };
390
-
391
- if (released) {
392
- return;
393
- }
420
+ });
421
+ scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
422
+ } catch (error) {
423
+ // A failed refresh is not fatal by design: the lock goes stale
424
+ // and another holder reclaims it. Swallow it so a transient
425
+ // error (e.g. the owner file racing a concurrent release, or an
426
+ // EPERM on Windows while a file is being deleted) never surfaces
427
+ // as an unhandled rejection from this interval.
428
+ scopedLogger.debug(
429
+ 'heartbeat refresh for ticket %s failed: %s',
430
+ ticketId,
431
+ error instanceof Error ? error.message : String(error),
432
+ );
433
+ }
434
+ };
394
435
 
395
- try {
396
- await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
397
- scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
398
- } catch (error) {
399
- // A failed refresh is not fatal by design: the lock goes stale
400
- // and another holder reclaims it. Swallow it so a transient
401
- // write error (e.g. the owner file racing a concurrent release,
402
- // or an EPERM on Windows when the directory is being torn down)
403
- // never surfaces as an unhandled rejection from this interval.
404
- scopedLogger.debug(
405
- 'heartbeat refresh for ticket %s failed: %s',
406
- ticketId,
407
- error instanceof Error ? error.message : String(error),
408
- );
409
- }
410
- } finally {
411
- heartbeatInFlight = false;
436
+ heartbeatTimer = setInterval(() => {
437
+ if (heartbeatInFlight) {
438
+ return;
412
439
  }
440
+
441
+ heartbeatInFlight = refreshHeartbeat().finally(() => {
442
+ heartbeatInFlight = null;
443
+ });
413
444
  }, heartbeatIntervalMs);
414
445
  heartbeatTimer.unref?.();
415
446
  };
package/src/run.ts CHANGED
@@ -1,4 +1,3 @@
1
- import path from 'node:path';
2
1
  import type { Config as JestConfig } from 'jest-runner';
3
2
  import { TestResult as JestTestResult } from '@jest/test-result';
4
3
  import type {
@@ -9,6 +8,7 @@ import type { HarnessSession } from './harness-session.js';
9
8
  import { formatResultsErrors } from 'jest-message-util';
10
9
  import { toTestResult } from './toTestResult.js';
11
10
  import { formatHarnessErrorMessage } from './format-harness-error.js';
11
+ import { toRelativePosixPath } from './relative-path.js';
12
12
 
13
13
  // Helper function to flatten nested test suites into a flat array of tests with hierarchy
14
14
  const flattenTests = (
@@ -91,14 +91,14 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
91
91
  session,
92
92
  }) => {
93
93
  const start = Date.now();
94
- const relativeTestPath = path.relative(globalConfig.rootDir, testPath);
94
+ const relativeTestPath = toRelativePosixPath(globalConfig.rootDir, testPath);
95
95
 
96
96
  // Extract setup files from Jest config and convert to relative paths
97
97
  const setupFiles = projectConfig.setupFiles?.map((setupFile) =>
98
- path.relative(globalConfig.rootDir, setupFile)
98
+ toRelativePosixPath(globalConfig.rootDir, setupFile)
99
99
  );
100
100
  const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map(
101
- (setupFile) => path.relative(globalConfig.rootDir, setupFile)
101
+ (setupFile) => toRelativePosixPath(globalConfig.rootDir, setupFile)
102
102
  );
103
103
  const testTimeout =
104
104
  session.config.testTimeout ??
@@ -0,0 +1,65 @@
1
+ import type { Config } from 'jest-runner';
2
+ import {
3
+ createHarnessSession,
4
+ type HarnessSession,
5
+ } from './harness-session.js';
6
+
7
+ // Jest constructs a new runner (and scheduler) for every watch-mode run, so a
8
+ // per-instance session would leave the previous run's session holding the
9
+ // resource lock while the next run queues behind it. Watch-mode sessions
10
+ // therefore live at module scope and are shared by all runner instances.
11
+ //
12
+ // There is no explicit teardown here: in interactive watch mode Jest exits
13
+ // via process.exit on Ctrl-C / `q`, which does not dispose the session. The
14
+ // lock is reclaimed by the lock manager's pid / stale-owner detection.
15
+ type WatchSessionEntry = {
16
+ key: string;
17
+ promise: Promise<HarnessSession>;
18
+ };
19
+
20
+ let current: WatchSessionEntry | null = null;
21
+
22
+ // Everything createHarnessSession reads from the Jest config.
23
+ const getSessionKey = (globalConfig: Config.GlobalConfig): string =>
24
+ JSON.stringify([globalConfig.rootDir, Boolean(globalConfig.collectCoverage)]);
25
+
26
+ export const getWatchSession = (
27
+ globalConfig: Config.GlobalConfig
28
+ ): Promise<HarnessSession> => {
29
+ const key = getSessionKey(globalConfig);
30
+
31
+ if (current?.key === key) {
32
+ return current.promise;
33
+ }
34
+
35
+ const stale = current;
36
+ // The entry is installed synchronously so concurrent callers share it, even
37
+ // while a stale session is still being disposed.
38
+ const entry: WatchSessionEntry = {
39
+ key,
40
+ promise: (async () => {
41
+ if (stale) {
42
+ // A failed disposal must not fail the run.
43
+ await stale.promise
44
+ .then((session) => session.dispose())
45
+ .catch(() => undefined);
46
+ }
47
+ return createHarnessSession(globalConfig);
48
+ })(),
49
+ };
50
+ current = entry;
51
+
52
+ // Never cache a failed creation; the next run retries.
53
+ entry.promise.catch(() => {
54
+ if (current === entry) {
55
+ current = null;
56
+ }
57
+ });
58
+
59
+ return entry.promise;
60
+ };
61
+
62
+ /** @internal Test-only: forget the module-level session without disposing. */
63
+ export const resetWatchSessionForTests = (): void => {
64
+ current = null;
65
+ };
@@ -1,21 +0,0 @@
1
- import { type AppMonitor, type HarnessPlatformRunner } from '@react-native-harness/platforms';
2
- export type CrashSupervisorState = 'idle' | 'launching' | 'ready' | 'running' | 'disposing';
3
- export type CrashSupervisor = {
4
- setActiveTestFile: (testFilePath: string | null) => void;
5
- beginLaunch: (testFilePath: string) => void;
6
- markReady: () => void;
7
- beginTestRun: (testFilePath: string) => void;
8
- stop: () => Promise<void>;
9
- start: () => Promise<void>;
10
- waitForCrash: (testFilePath: string) => Promise<never>;
11
- isReady: () => boolean;
12
- cancelCrashWaiters: () => void;
13
- reset: () => void;
14
- dispose: () => Promise<void>;
15
- };
16
- export type CrashSupervisorOptions = {
17
- appMonitor: AppMonitor;
18
- platformRunner: HarnessPlatformRunner;
19
- };
20
- export declare const createCrashSupervisor: ({ appMonitor, platformRunner, }: CrashSupervisorOptions) => CrashSupervisor;
21
- //# sourceMappingURL=crash-supervisor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"crash-supervisor.d.ts","sourceRoot":"","sources":["../src/crash-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAUzC,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,WAAW,GACX,OAAO,GACP,SAAS,GACT,WAAW,CAAC;AAEhB,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,qBAAqB,CAAC;CACvC,CAAC;AAmCF,eAAO,MAAM,qBAAqB,GAAI,iCAGnC,sBAAsB,KAAG,eAkP3B,CAAC"}
@@ -1,210 +0,0 @@
1
- import { NativeCrashError, } from './errors.js';
2
- import { logger } from '@react-native-harness/tools';
3
- const crashLogger = logger.child('crash');
4
- const getCrashPhase = (state) => state === 'running' ? 'execution' : 'startup';
5
- const mergeCrashDetails = (phase, initialDetails, enrichedDetails, fallbackSummary) => ({
6
- phase,
7
- source: enrichedDetails?.source ?? initialDetails?.source,
8
- summary: enrichedDetails?.summary ?? initialDetails?.summary ?? fallbackSummary,
9
- signal: enrichedDetails?.signal ?? initialDetails?.signal,
10
- exceptionType: enrichedDetails?.exceptionType ?? initialDetails?.exceptionType,
11
- processName: enrichedDetails?.processName ?? initialDetails?.processName,
12
- pid: enrichedDetails?.pid ?? initialDetails?.pid,
13
- stackTrace: enrichedDetails?.stackTrace ?? initialDetails?.stackTrace,
14
- rawLines: enrichedDetails?.rawLines ?? initialDetails?.rawLines,
15
- artifactType: enrichedDetails?.artifactType ?? initialDetails?.artifactType,
16
- artifactPath: enrichedDetails?.artifactPath ?? initialDetails?.artifactPath,
17
- });
18
- export const createCrashSupervisor = ({ appMonitor, platformRunner, }) => {
19
- let state = 'idle';
20
- let activeTestFilePath = null;
21
- let crashRejectors = new Set();
22
- let disposed = false;
23
- let monitoring = true;
24
- let isResolvingCrash = false;
25
- const getCrashDetailsProvider = () => {
26
- if ('getCrashDetails' in appMonitor) {
27
- return appMonitor;
28
- }
29
- if (platformRunner.getCrashDetails) {
30
- return platformRunner;
31
- }
32
- return null;
33
- };
34
- const rejectCrashWaiters = (testFilePath, details) => {
35
- const error = new NativeCrashError(testFilePath, details);
36
- for (const reject of crashRejectors) {
37
- reject(error);
38
- }
39
- crashRejectors = new Set();
40
- };
41
- const handleCrash = async (reason, details) => {
42
- if (isResolvingCrash ||
43
- (state !== 'launching' && state !== 'running' && state !== 'ready')) {
44
- return;
45
- }
46
- if (!activeTestFilePath) {
47
- crashLogger.debug('ignoring crash signal without active test: %s', reason);
48
- return;
49
- }
50
- isResolvingCrash = true;
51
- crashLogger.debug('native crash detected during %s (state=%s reason=%s)', activeTestFilePath, state, reason || '(none)');
52
- for (const line of details?.rawLines ?? []) {
53
- crashLogger.debug('%s', line);
54
- }
55
- const phase = getCrashPhase(state);
56
- const testFilePath = activeTestFilePath;
57
- state = 'idle';
58
- try {
59
- const enrichedDetails = await getCrashDetailsProvider()?.getCrashDetails?.({
60
- processName: details?.processName,
61
- pid: details?.pid,
62
- occurredAt: Date.now(),
63
- });
64
- const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
65
- crashLogger.debug('crash details: %o', {
66
- phase: mergedDetails.phase,
67
- source: mergedDetails.source,
68
- summary: mergedDetails.summary,
69
- signal: mergedDetails.signal,
70
- exceptionType: mergedDetails.exceptionType,
71
- processName: mergedDetails.processName,
72
- pid: mergedDetails.pid,
73
- });
74
- rejectCrashWaiters(testFilePath, mergedDetails);
75
- }
76
- finally {
77
- isResolvingCrash = false;
78
- }
79
- };
80
- const confirmCrash = async (reason, details) => {
81
- if (disposed || !monitoring || state === 'disposing') {
82
- return;
83
- }
84
- try {
85
- const isRunning = await platformRunner.isAppRunning();
86
- if (!isRunning) {
87
- void handleCrash(reason, details);
88
- }
89
- }
90
- catch (error) {
91
- crashLogger.debug('crash confirmation failed', error);
92
- }
93
- };
94
- const appMonitorListener = (event) => {
95
- if (disposed || !monitoring) {
96
- return;
97
- }
98
- if (event.type === 'app_started') {
99
- return;
100
- }
101
- if (event.type === 'app_exited') {
102
- const details = {
103
- source: event.crashDetails?.source ?? event.source,
104
- summary: event.crashDetails?.summary,
105
- signal: event.crashDetails?.signal,
106
- exceptionType: event.crashDetails?.exceptionType,
107
- processName: event.crashDetails?.processName,
108
- pid: event.crashDetails?.pid ?? event.pid,
109
- stackTrace: event.crashDetails?.stackTrace,
110
- rawLines: event.crashDetails?.rawLines ??
111
- (event.line ? [event.line] : undefined),
112
- };
113
- if (event.isConfirmed ?? event.source === 'polling') {
114
- void handleCrash('', details);
115
- }
116
- else {
117
- void confirmCrash('', details);
118
- }
119
- return;
120
- }
121
- if (event.type === 'possible_crash') {
122
- const details = {
123
- source: event.crashDetails?.source ?? event.source,
124
- summary: event.crashDetails?.summary,
125
- signal: event.crashDetails?.signal,
126
- exceptionType: event.crashDetails?.exceptionType,
127
- processName: event.crashDetails?.processName,
128
- pid: event.crashDetails?.pid ?? event.pid,
129
- stackTrace: event.crashDetails?.stackTrace,
130
- rawLines: event.crashDetails?.rawLines ??
131
- (event.line ? [event.line] : undefined),
132
- };
133
- if (event.isConfirmed) {
134
- void handleCrash(`possible crash signal (${event.source ?? 'unknown'})`, details);
135
- }
136
- else {
137
- void confirmCrash(`possible crash signal (${event.source ?? 'unknown'})`, details);
138
- }
139
- }
140
- };
141
- appMonitor.addListener(appMonitorListener);
142
- const setActiveTestFile = (testFilePath) => {
143
- activeTestFilePath = testFilePath;
144
- };
145
- const beginLaunch = (testFilePath) => {
146
- activeTestFilePath = testFilePath;
147
- state = 'launching';
148
- };
149
- const markReady = () => {
150
- if (state !== 'disposing') {
151
- state = 'ready';
152
- }
153
- };
154
- const beginTestRun = (testFilePath) => {
155
- activeTestFilePath = testFilePath;
156
- state = 'running';
157
- };
158
- const stop = async () => {
159
- monitoring = false;
160
- await appMonitor.stop();
161
- };
162
- const start = async () => {
163
- monitoring = true;
164
- await appMonitor.start();
165
- };
166
- const waitForCrash = (testFilePath) => new Promise((_, reject) => {
167
- if (disposed) {
168
- reject(new NativeCrashError(testFilePath, {
169
- phase: 'startup',
170
- source: 'polling',
171
- summary: 'Crash supervisor disposed while waiting for app startup.',
172
- }));
173
- return;
174
- }
175
- crashRejectors.add(reject);
176
- });
177
- const isReady = () => state === 'ready' || state === 'running';
178
- const cancelCrashWaiters = () => {
179
- crashRejectors = new Set();
180
- };
181
- const reset = () => {
182
- cancelCrashWaiters();
183
- isResolvingCrash = false;
184
- if (state !== 'disposing') {
185
- state = 'idle';
186
- }
187
- };
188
- const dispose = async () => {
189
- disposed = true;
190
- monitoring = false;
191
- state = 'disposing';
192
- crashRejectors = new Set();
193
- isResolvingCrash = false;
194
- appMonitor.removeListener(appMonitorListener);
195
- await appMonitor.dispose();
196
- };
197
- return {
198
- setActiveTestFile,
199
- beginLaunch,
200
- markReady,
201
- beginTestRun,
202
- stop,
203
- start,
204
- waitForCrash,
205
- isReady,
206
- cancelCrashWaiters,
207
- reset,
208
- dispose,
209
- };
210
- };
@@ -1,16 +0,0 @@
1
- import type { TestEvents } from '@jest/test-result';
2
- import type { Test } from 'jest-runner';
3
- import type { BridgeEvents } from '@react-native-harness/bridge';
4
- import type { TestCaseResult } from '@jest/test-result';
5
- export type EventMapperState = {
6
- currentTest?: Test;
7
- suiteHierarchy: string[];
8
- testResults: Map<string, TestCaseResult>;
9
- fileStartTime: number;
10
- };
11
- export type HarnessEventMapper = {
12
- handleBridgeEvent: (event: BridgeEvents, test: Test) => void;
13
- reset: () => void;
14
- };
15
- export declare const getHarnessEventMapper: (emit: <T extends keyof TestEvents>(eventName: T, eventData: TestEvents[T]) => void) => HarnessEventMapper;
16
- //# sourceMappingURL=event-mapper.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"event-mapper.d.ts","sourceRoot":"","sources":["../src/event-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EASV,YAAY,EACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,CAAC,CAAC,SAAS,MAAM,UAAU,EAC/B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,KACrB,IAAI,KACR,kBA8JF,CAAC"}
@@ -1,120 +0,0 @@
1
- export const getHarnessEventMapper = (emit) => {
2
- const state = {
3
- suiteHierarchy: [],
4
- testResults: new Map(),
5
- fileStartTime: 0,
6
- };
7
- const handleBridgeEvent = (event, test) => {
8
- // Filter to only process test runner events
9
- if (event.type.startsWith('test-') ||
10
- event.type.startsWith('suite-') ||
11
- event.type.startsWith('file-')) {
12
- switch (event.type) {
13
- case 'file-started':
14
- handleFileStarted(event, test);
15
- break;
16
- case 'file-finished':
17
- handleFileFinished(event, test);
18
- break;
19
- case 'suite-started':
20
- handleSuiteStarted(event);
21
- break;
22
- case 'suite-finished':
23
- handleSuiteFinished(event);
24
- break;
25
- case 'test-started':
26
- handleTestStarted(event, test);
27
- break;
28
- case 'test-finished':
29
- handleTestFinished(event, test);
30
- break;
31
- }
32
- }
33
- };
34
- const handleFileStarted = (event, test) => {
35
- state.currentTest = test;
36
- state.fileStartTime = Date.now();
37
- state.suiteHierarchy = [];
38
- state.testResults.clear();
39
- // Don't emit test-file-start here - that should be handled by the Jest runner
40
- // when it actually starts running the test file
41
- };
42
- const handleFileFinished = (_event, _test) => {
43
- // File finished - all test results should already be emitted
44
- // The final TestResult will be handled by the runHarnessTestFile function
45
- };
46
- const handleSuiteStarted = (event) => {
47
- state.suiteHierarchy.push(event.name);
48
- };
49
- const handleSuiteFinished = (event) => {
50
- // Remove the suite from hierarchy when it finishes
51
- const index = state.suiteHierarchy.lastIndexOf(event.name);
52
- if (index !== -1) {
53
- state.suiteHierarchy.splice(index, 1);
54
- }
55
- };
56
- const handleTestStarted = (event, test) => {
57
- const testCaseStartInfo = {
58
- ancestorTitles: [...state.suiteHierarchy],
59
- fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
60
- title: event.name,
61
- mode: 'skip', // Default mode, will be updated based on test status
62
- };
63
- emit('test-case-start', [test.path, testCaseStartInfo]);
64
- };
65
- const handleTestFinished = (event, test) => {
66
- const testCaseResult = {
67
- ancestorTitles: [...state.suiteHierarchy],
68
- duration: event.duration,
69
- failureDetails: [],
70
- failureMessages: event.error ? [formatError(event.error)] : [],
71
- fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
72
- numPassingAsserts: event.status === 'passed' ? 1 : 0,
73
- status: mapStatus(event.status),
74
- title: event.name,
75
- startedAt: Date.now() - event.duration,
76
- };
77
- state.testResults.set(event.name, testCaseResult);
78
- emit('test-case-result', [test.path, testCaseResult]);
79
- };
80
- const mapStatus = (status) => {
81
- return status;
82
- };
83
- const formatError = (error) => {
84
- let errorMessage = `${error.name}: ${error.message}`;
85
- if (error.codeFrame) {
86
- errorMessage += '\n\n' + formatCodeFrame(error.codeFrame);
87
- }
88
- return errorMessage;
89
- };
90
- const formatCodeFrame = (codeFrame) => {
91
- const lines = codeFrame.content.split('\n');
92
- const lineNumber = codeFrame.location?.row || 1;
93
- const column = codeFrame.location?.column || 1;
94
- let formatted = `\n${codeFrame.fileName}:${lineNumber}:${column}\n`;
95
- // Add context lines around the error
96
- const startLine = Math.max(0, lineNumber - 3);
97
- const endLine = Math.min(lines.length, lineNumber + 2);
98
- for (let i = startLine; i < endLine; i++) {
99
- const lineNum = i + 1;
100
- const line = lines[i];
101
- const isErrorLine = lineNum === lineNumber;
102
- formatted += `${lineNum.toString().padStart(3)} | ${line}\n`;
103
- if (isErrorLine && column > 0) {
104
- const pointer = ' '.repeat(column + 5) + '^';
105
- formatted += pointer + '\n';
106
- }
107
- }
108
- return formatted;
109
- };
110
- const reset = () => {
111
- state.suiteHierarchy = [];
112
- state.testResults.clear();
113
- state.fileStartTime = 0;
114
- state.currentTest = undefined;
115
- };
116
- return {
117
- handleBridgeEvent,
118
- reset,
119
- };
120
- };
@@ -1,3 +0,0 @@
1
- import type { Config as JestConfig } from 'jest-runner';
2
- export default function (globalConfig: JestConfig.GlobalConfig): Promise<void>;
3
- //# sourceMappingURL=global-setup.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../src/global-setup.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AA4CxD,yBAA+B,YAAY,EAAE,UAAU,CAAC,YAAY,iBA4CnE"}