@react-native-harness/jest 1.4.0-rc.1 → 1.4.1

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 (98) hide show
  1. package/dist/crash-monitor.d.ts +7 -0
  2. package/dist/crash-monitor.d.ts.map +1 -1
  3. package/dist/crash-monitor.js +19 -0
  4. package/dist/diagnostics/derivers.d.ts +24 -0
  5. package/dist/diagnostics/derivers.d.ts.map +1 -0
  6. package/dist/diagnostics/derivers.js +146 -0
  7. package/dist/diagnostics/index.d.ts +3 -0
  8. package/dist/diagnostics/index.d.ts.map +1 -0
  9. package/dist/diagnostics/index.js +2 -0
  10. package/dist/diagnostics/report.d.ts +7 -0
  11. package/dist/diagnostics/report.d.ts.map +1 -0
  12. package/dist/diagnostics/report.js +84 -0
  13. package/dist/environment-reset.d.ts +51 -0
  14. package/dist/environment-reset.d.ts.map +1 -0
  15. package/dist/environment-reset.js +83 -0
  16. package/dist/execute-run.d.ts.map +1 -1
  17. package/dist/execute-run.js +121 -69
  18. package/dist/format-harness-error.d.ts +5 -0
  19. package/dist/format-harness-error.d.ts.map +1 -0
  20. package/dist/format-harness-error.js +43 -0
  21. package/dist/harness-session.d.ts +26 -1
  22. package/dist/harness-session.d.ts.map +1 -1
  23. package/dist/harness-session.js +260 -87
  24. package/dist/run.d.ts.map +1 -1
  25. package/dist/run.js +21 -4
  26. package/package.json +8 -7
  27. package/src/crash-monitor.ts +28 -0
  28. package/src/diagnostics/derivers.ts +170 -0
  29. package/src/diagnostics/index.ts +2 -0
  30. package/src/diagnostics/report.ts +124 -0
  31. package/src/environment-reset.ts +138 -0
  32. package/src/execute-run.ts +136 -77
  33. package/src/format-harness-error.ts +73 -0
  34. package/src/harness-session.ts +369 -131
  35. package/src/run.ts +23 -4
  36. package/tsconfig.json +3 -0
  37. package/tsconfig.lib.json +3 -0
  38. package/dist/__tests__/bridge.test.d.ts +0 -2
  39. package/dist/__tests__/bridge.test.d.ts.map +0 -1
  40. package/dist/__tests__/bridge.test.js +0 -163
  41. package/dist/__tests__/client-log-handler.test.d.ts +0 -2
  42. package/dist/__tests__/client-log-handler.test.d.ts.map +0 -1
  43. package/dist/__tests__/client-log-handler.test.js +0 -306
  44. package/dist/__tests__/crash-monitor.test.d.ts +0 -2
  45. package/dist/__tests__/crash-monitor.test.d.ts.map +0 -1
  46. package/dist/__tests__/crash-monitor.test.js +0 -166
  47. package/dist/__tests__/crash-supervisor.test.d.ts +0 -2
  48. package/dist/__tests__/crash-supervisor.test.d.ts.map +0 -1
  49. package/dist/__tests__/crash-supervisor.test.js +0 -151
  50. package/dist/__tests__/errors.test.d.ts +0 -2
  51. package/dist/__tests__/errors.test.d.ts.map +0 -1
  52. package/dist/__tests__/errors.test.js +0 -98
  53. package/dist/__tests__/execute-run.test.d.ts +0 -2
  54. package/dist/__tests__/execute-run.test.d.ts.map +0 -1
  55. package/dist/__tests__/execute-run.test.js +0 -393
  56. package/dist/__tests__/fixtures/mock-platform-runner.d.ts +0 -7
  57. package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +0 -1
  58. package/dist/__tests__/fixtures/mock-platform-runner.js +0 -6
  59. package/dist/__tests__/harness-cache.test.d.ts +0 -2
  60. package/dist/__tests__/harness-cache.test.d.ts.map +0 -1
  61. package/dist/__tests__/harness-cache.test.js +0 -51
  62. package/dist/__tests__/harness-session.test.d.ts +0 -2
  63. package/dist/__tests__/harness-session.test.d.ts.map +0 -1
  64. package/dist/__tests__/harness-session.test.js +0 -74
  65. package/dist/__tests__/harness.test.d.ts +0 -2
  66. package/dist/__tests__/harness.test.d.ts.map +0 -1
  67. package/dist/__tests__/harness.test.js +0 -636
  68. package/dist/__tests__/hook-queue.test.d.ts +0 -2
  69. package/dist/__tests__/hook-queue.test.d.ts.map +0 -1
  70. package/dist/__tests__/hook-queue.test.js +0 -56
  71. package/dist/__tests__/index.test.d.ts +0 -2
  72. package/dist/__tests__/index.test.d.ts.map +0 -1
  73. package/dist/__tests__/index.test.js +0 -148
  74. package/dist/__tests__/metro-port.test.d.ts +0 -2
  75. package/dist/__tests__/metro-port.test.d.ts.map +0 -1
  76. package/dist/__tests__/metro-port.test.js +0 -52
  77. package/dist/__tests__/resource-lock.test.d.ts +0 -2
  78. package/dist/__tests__/resource-lock.test.d.ts.map +0 -1
  79. package/dist/__tests__/resource-lock.test.js +0 -138
  80. package/dist/__tests__/test-file-platform-filter.test.d.ts +0 -2
  81. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +0 -1
  82. package/dist/__tests__/test-file-platform-filter.test.js +0 -47
  83. package/dist/cli-args.test.d.ts +0 -2
  84. package/dist/cli-args.test.d.ts.map +0 -1
  85. package/dist/cli-args.test.js +0 -1
  86. package/dist/tsconfig.lib.tsbuildinfo +0 -1
  87. package/src/__tests__/bridge.test.ts +0 -219
  88. package/src/__tests__/client-log-handler.test.ts +0 -364
  89. package/src/__tests__/crash-monitor.test.ts +0 -239
  90. package/src/__tests__/errors.test.ts +0 -157
  91. package/src/__tests__/execute-run.test.ts +0 -526
  92. package/src/__tests__/harness-session.test.ts +0 -93
  93. package/src/__tests__/hook-queue.test.ts +0 -77
  94. package/src/__tests__/index.test.ts +0 -219
  95. package/src/__tests__/metro-port.test.ts +0 -61
  96. package/src/__tests__/resource-lock.test.ts +0 -179
  97. package/src/__tests__/test-file-platform-filter.test.ts +0 -83
  98. package/tsconfig.tsbuildinfo +0 -1
@@ -8,6 +8,7 @@ import { randomUUID } from 'node:crypto';
8
8
  import path from 'node:path';
9
9
  import { type HarnessSession, type HarnessRunState } from './harness-session.js';
10
10
  import { runHarnessTestFile } from './run.js';
11
+ import { resolveResetStrategyKind } from './environment-reset.js';
11
12
  import {
12
13
  NativeCrashError,
13
14
  RuntimeDisconnectError,
@@ -22,11 +23,17 @@ import type {
22
23
  TestRunnerEvents,
23
24
  TestRunnerTestFinishedEvent,
24
25
  TestRunnerTestStartedEvent,
26
+ TestSuiteResult,
25
27
  } from '@react-native-harness/bridge';
26
28
  import {
27
29
  createPlatformSkippedTestResult,
28
30
  shouldRunHarnessTestFile,
29
31
  } from './test-file-platform-filter.js';
32
+ import { formatHarnessErrorMessage } from './format-harness-error.js';
33
+ import { logger } from '@react-native-harness/tools';
34
+ import { printSummary, writeTraceFile } from './diagnostics/index.js';
35
+
36
+ const diagnosticsLogger = logger.child('diagnostics');
30
37
 
31
38
  type EmitTestEvent = <Name extends keyof TestEvents>(
32
39
  eventName: Name,
@@ -86,8 +93,10 @@ const emitHarnessTestFinished = async (
86
93
  emitEvent: EmitTestEvent,
87
94
  event: TestRunnerTestFinishedEvent,
88
95
  ): Promise<void> => {
89
- const failureMessage = event.error?.message;
90
96
  const codeFrame = event.error?.codeFrame;
97
+ const failureMessage = formatHarnessErrorMessage(event.error, {
98
+ testStartedAt: event.startedAt,
99
+ });
91
100
  const location = codeFrame?.location
92
101
  ? { column: codeFrame.location.column, line: codeFrame.location.row }
93
102
  : null;
@@ -95,9 +104,7 @@ const emitHarnessTestFinished = async (
95
104
  ancestorTitles: event.ancestorTitles,
96
105
  duration: event.duration,
97
106
  failureDetails: [],
98
- failureMessages: failureMessage
99
- ? [`${failureMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`]
100
- : [],
107
+ failureMessages: failureMessage ? [failureMessage] : [],
101
108
  fullName: event.fullName,
102
109
  location,
103
110
  numPassingAsserts: event.status === 'passed' ? 1 : 0,
@@ -114,6 +121,18 @@ const isHarnessCaseEvent = (
114
121
  ): event is TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent =>
115
122
  event.type === 'test-started' || event.type === 'test-finished';
116
123
 
124
+ const TIMEOUT_ERROR_NAMES = new Set([
125
+ 'SuiteHookTimeoutError',
126
+ 'TestCaseTimeoutError',
127
+ ]);
128
+
129
+ const hasRuntimeTimeout = (result: TestSuiteResult): boolean =>
130
+ (result.error ? TIMEOUT_ERROR_NAMES.has(result.error.name) : false) ||
131
+ result.tests.some((test) =>
132
+ test.error ? TIMEOUT_ERROR_NAMES.has(test.error.name) : false,
133
+ ) ||
134
+ result.suites.some(hasRuntimeTimeout);
135
+
117
136
  export const executeRun = async (
118
137
  session: HarnessSession,
119
138
  tests: Array<Test>,
@@ -122,6 +141,8 @@ export const executeRun = async (
122
141
  globalConfig: Config.GlobalConfig,
123
142
  ): Promise<void> => {
124
143
  const runId = randomUUID();
144
+ const diagnostics = session.diagnostics.child({ runId });
145
+ const runSpan = diagnostics.start('run.total', { runId });
125
146
  const startTime = Date.now();
126
147
  const watchMode = globalConfig.watch || globalConfig.watchAll;
127
148
  const rootDir = globalConfig.rootDir ?? process.cwd();
@@ -145,6 +166,7 @@ export const executeRun = async (
145
166
  testFiles,
146
167
  watchMode,
147
168
  coverageEnabled: globalConfig.collectCoverage,
169
+ completed: false,
148
170
  summary,
149
171
  status: summary.failed > 0 ? 'failed' : 'passed',
150
172
  ...overrides,
@@ -168,12 +190,15 @@ export const executeRun = async (
168
190
  host: session.config.host?.trim() || undefined,
169
191
  });
170
192
 
171
- const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
193
+ const resetStrategyKind = resolveResetStrategyKind(
194
+ session.config.resetEnvironmentBetweenTestFiles,
195
+ );
172
196
  const platformId = session.context.platform.platformId;
173
197
  const knownPlatformIds = new Set(
174
198
  session.config.runners.map((runner) => runner.platformId),
175
199
  );
176
200
  let isFirstTest = true;
201
+ let shouldRestartAfterTimeout = false;
177
202
  let runError: unknown;
178
203
 
179
204
  try {
@@ -200,22 +225,86 @@ export const executeRun = async (
200
225
  };
201
226
 
202
227
  await session.callHook('test-file:started', { runId, file: relativeTestPath });
228
+ const fileSpan = diagnostics.start('run.file', { file: relativeTestPath });
229
+ let fileStatus: 'passed' | 'failed' | 'skipped' | 'todo' = 'failed';
230
+
231
+ try {
232
+ if (
233
+ !shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)
234
+ ) {
235
+ try {
236
+ await emitEvent('test-file-start', test);
237
+ const skippedResult = createPlatformSkippedTestResult(test.path);
238
+ applyJestResultToSummary(summary, skippedResult);
239
+ updateRunState();
240
+ fileStatus = 'skipped';
241
+ await emitTestFileFinished({
242
+ status: 'skipped',
243
+ duration: Date.now() - fileStartedAt,
244
+ result: null,
245
+ });
246
+ await emitEvent('test-file-success', test, skippedResult);
247
+ } catch (err) {
248
+ fileStatus = 'failed';
249
+ if (!emittedTestFileFinished) {
250
+ await emitTestFileFinished({
251
+ status: 'failed',
252
+ duration: Date.now() - fileStartedAt,
253
+ result: null,
254
+ });
255
+ }
256
+ updateRunState({ error: err });
257
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
258
+ }
259
+ continue;
260
+ }
203
261
 
204
- if (
205
- !shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)
206
- ) {
207
262
  try {
263
+ if (shouldRestartAfterTimeout) {
264
+ // Recovery from a runtime timeout always uses the (stronger)
265
+ // process restart, never the configured reset strategy.
266
+ await session.restartApp(test.path);
267
+ shouldRestartAfterTimeout = false;
268
+ } else if (resetStrategyKind !== null && !isFirstTest) {
269
+ await session.resetEnvironment(test.path);
270
+ }
271
+ isFirstTest = false;
272
+
273
+ session.flushClientLogs();
208
274
  await emitEvent('test-file-start', test);
209
- const skippedResult = createPlatformSkippedTestResult(test.path);
210
- applyJestResultToSummary(summary, skippedResult);
275
+ await session.ensureAppReady(test.path);
276
+
277
+ // Crash detection is handled inside session.runTestFile; NativeCrashError
278
+ // propagates here if a crash wins the race.
279
+ const result = await runHarnessTestFile({
280
+ testPath: test.path,
281
+ session,
282
+ globalConfig,
283
+ projectConfig: test.context.config,
284
+ });
285
+
286
+ applyJestResultToSummary(summary, result.jestResult);
287
+ const clientLogs = session.flushClientLogs();
288
+ if (clientLogs.length > 0) {
289
+ result.jestResult.console = clientLogs;
290
+ }
211
291
  updateRunState();
292
+ fileStatus = result.harnessResult.status;
212
293
  await emitTestFileFinished({
213
- status: 'skipped',
214
- duration: Date.now() - fileStartedAt,
215
- result: null,
294
+ status: result.harnessResult.status,
295
+ duration: result.duration,
296
+ result: result.harnessResult,
216
297
  });
217
- await emitEvent('test-file-success', test, skippedResult);
298
+ const didRuntimeTimeout = hasRuntimeTimeout(result.harnessResult);
299
+ shouldRestartAfterTimeout = didRuntimeTimeout;
300
+ await caseEventChain;
301
+ await emitEvent('test-file-success', test, result.jestResult);
302
+ if (didRuntimeTimeout) {
303
+ await session.restartApp(test.path);
304
+ shouldRestartAfterTimeout = false;
305
+ }
218
306
  } catch (err) {
307
+ fileStatus = 'failed';
219
308
  if (!emittedTestFileFinished) {
220
309
  await emitTestFileFinished({
221
310
  status: 'failed',
@@ -223,72 +312,29 @@ export const executeRun = async (
223
312
  result: null,
224
313
  });
225
314
  }
226
- updateRunState({ error: err });
227
- await emitEvent('test-file-failure', test, buildTestFailure(err));
228
- }
229
- continue;
230
- }
231
-
232
- try {
233
- if (shouldResetEnv && !isFirstTest) {
234
- await session.restartApp(test.path);
235
- }
236
- isFirstTest = false;
237
-
238
- session.flushClientLogs();
239
- await emitEvent('test-file-start', test);
240
- await session.ensureAppReady(test.path);
241
-
242
- // Crash detection is handled inside session.runTestFile; NativeCrashError
243
- // propagates here if a crash wins the race.
244
- const result = await runHarnessTestFile({
245
- testPath: test.path,
246
- session,
247
- globalConfig,
248
- projectConfig: test.context.config,
249
- });
250
315
 
251
- applyJestResultToSummary(summary, result.jestResult);
252
- const clientLogs = session.flushClientLogs();
253
- if (clientLogs.length > 0) {
254
- result.jestResult.console = clientLogs;
255
- }
256
- updateRunState();
257
- await emitTestFileFinished({
258
- status: result.harnessResult.status,
259
- duration: result.duration,
260
- result: result.harnessResult,
261
- });
262
- await caseEventChain;
263
- await emitEvent('test-file-success', test, result.jestResult);
264
- } catch (err) {
265
- if (!emittedTestFileFinished) {
266
- await emitTestFileFinished({
267
- status: 'failed',
268
- duration: Date.now() - fileStartedAt,
269
- result: null,
270
- });
271
- }
316
+ const isRuntimeFailure =
317
+ err instanceof NativeCrashError ||
318
+ err instanceof RuntimeDisconnectError ||
319
+ err instanceof StartupStallError ||
320
+ err instanceof AppBridgeDisconnectedError ||
321
+ err instanceof DeviceNotRespondingError;
272
322
 
273
- const isRuntimeFailure =
274
- err instanceof NativeCrashError ||
275
- err instanceof RuntimeDisconnectError ||
276
- err instanceof StartupStallError ||
277
- err instanceof AppBridgeDisconnectedError ||
278
- err instanceof DeviceNotRespondingError;
323
+ if (isRuntimeFailure) {
324
+ summary.failed += 1;
325
+ updateRunState();
326
+ }
279
327
 
280
- if (isRuntimeFailure) {
281
- summary.failed += 1;
282
- updateRunState();
283
- }
328
+ if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
329
+ session.resetCrashState();
330
+ }
284
331
 
285
- if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
286
- session.resetCrashState();
332
+ updateRunState({ error: isRuntimeFailure ? undefined : err });
333
+ await caseEventChain;
334
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
287
335
  }
288
-
289
- updateRunState({ error: isRuntimeFailure ? undefined : err });
290
- await caseEventChain;
291
- await emitEvent('test-file-failure', test, buildTestFailure(err));
336
+ } finally {
337
+ fileSpan.end({ status: fileStatus });
292
338
  }
293
339
  }
294
340
  } catch (err) {
@@ -296,18 +342,31 @@ export const executeRun = async (
296
342
  if (!(err instanceof CancelRun)) throw err;
297
343
  } finally {
298
344
  const runState = updateRunState(
299
- runError != null ? { error: runError, status: 'failed' } : {},
345
+ runError != null ? { completed: true, error: runError, status: 'failed' } : { completed: true },
300
346
  );
347
+ const finalStatus = runState.status ?? (runError != null ? 'failed' : 'passed');
301
348
  await session.callHook('run:finished', {
302
349
  runId,
303
350
  startTime,
304
351
  duration: Date.now() - startTime,
305
352
  testFiles,
306
353
  summary,
307
- status: runState.status ?? (runError != null ? 'failed' : 'passed'),
354
+ status: finalStatus,
308
355
  ...(runError != null ? { error: runError } : {}),
309
356
  });
310
357
  await caseEventChain;
311
358
  unsubscribe();
359
+ runSpan.end({ runId, status: finalStatus });
360
+
361
+ if (session.diagnostics.enabled) {
362
+ const entries = session.diagnostics.flush();
363
+ try {
364
+ printSummary(entries, (line) => diagnosticsLogger.info(line));
365
+ const tracePath = await writeTraceFile(entries, { projectRoot: rootDir, runId });
366
+ diagnosticsLogger.info(`wrote diagnostics trace to ${tracePath}`);
367
+ } catch (reportError) {
368
+ diagnosticsLogger.warn('failed to write diagnostics report: %s', reportError);
369
+ }
370
+ }
312
371
  }
313
372
  };
@@ -0,0 +1,73 @@
1
+ import type { SerializedError } from '@react-native-harness/bridge';
2
+
3
+ const formatPendingPromiseStack = (stack: string): string =>
4
+ stack
5
+ .split('\n')
6
+ .map((line) => ` ${line}`)
7
+ .join('\n');
8
+
9
+ const formatPendingPromises = (
10
+ pendingPromises: NonNullable<
11
+ NonNullable<SerializedError['diagnostics']>['pendingPromises']
12
+ >,
13
+ testStartedAt?: number,
14
+ ): string | null => {
15
+ if (pendingPromises.total === 0) {
16
+ return null;
17
+ }
18
+
19
+ const lines = [`Pending promises at timeout: ${pendingPromises.total}`];
20
+
21
+ if (pendingPromises.items.length < pendingPromises.total) {
22
+ lines.push(
23
+ `Showing ${pendingPromises.items.length} of ${pendingPromises.total} pending promises.`,
24
+ );
25
+ }
26
+
27
+ for (const promise of pendingPromises.items) {
28
+ const age =
29
+ testStartedAt !== undefined
30
+ ? `, created ${Math.max(0, promise.createdAt - testStartedAt)}ms after test start`
31
+ : '';
32
+
33
+ lines.push('');
34
+ lines.push(`Promise #${promise.id}${age}:`);
35
+
36
+ if (promise.stack) {
37
+ lines.push(formatPendingPromiseStack(promise.stack));
38
+ } else {
39
+ lines.push(' <stack unavailable>');
40
+ }
41
+ }
42
+
43
+ return lines.join('\n');
44
+ };
45
+
46
+ export const formatHarnessErrorMessage = (
47
+ error: SerializedError | undefined,
48
+ options: {
49
+ testStartedAt?: number;
50
+ } = {},
51
+ ): string | undefined => {
52
+ if (!error) {
53
+ return undefined;
54
+ }
55
+
56
+ const parts = [error.message];
57
+ const pendingPromiseDetails = error.diagnostics?.pendingPromises
58
+ ? formatPendingPromises(
59
+ error.diagnostics.pendingPromises,
60
+ options.testStartedAt,
61
+ )
62
+ : null;
63
+
64
+ if (pendingPromiseDetails) {
65
+ parts.push(pendingPromiseDetails);
66
+ }
67
+
68
+ if (error.codeFrame) {
69
+ parts.push(error.codeFrame.content);
70
+ }
71
+
72
+ return parts.join('\n\n');
73
+ };