@react-native-harness/jest 1.3.0 → 1.4.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 (98) hide show
  1. package/dist/__tests__/bridge.test.js +29 -4
  2. package/dist/__tests__/crash-monitor.test.js +194 -176
  3. package/dist/__tests__/environment-reset.test.d.ts +2 -0
  4. package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
  5. package/dist/__tests__/environment-reset.test.js +238 -0
  6. package/dist/__tests__/errors.test.js +39 -0
  7. package/dist/__tests__/execute-run.test.js +439 -29
  8. package/dist/__tests__/format-harness-error.test.d.ts +2 -0
  9. package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
  10. package/dist/__tests__/format-harness-error.test.js +27 -0
  11. package/dist/__tests__/harness-cache.test.js +62 -41
  12. package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
  13. package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
  14. package/dist/__tests__/harness-session-signal.test.js +298 -0
  15. package/dist/__tests__/harness-session.test.js +217 -234
  16. package/dist/__tests__/index.test.js +62 -11
  17. package/dist/__tests__/metro-port.test.js +2 -0
  18. package/dist/__tests__/run.test.d.ts +2 -0
  19. package/dist/__tests__/run.test.d.ts.map +1 -0
  20. package/dist/__tests__/run.test.js +48 -0
  21. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  22. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  23. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  24. package/dist/crash-monitor.d.ts +12 -4
  25. package/dist/crash-monitor.d.ts.map +1 -1
  26. package/dist/crash-monitor.js +135 -102
  27. package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
  28. package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
  29. package/dist/diagnostics/__tests__/derivers.test.js +248 -0
  30. package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
  31. package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
  32. package/dist/diagnostics/__tests__/report.test.js +97 -0
  33. package/dist/diagnostics/derivers.d.ts +24 -0
  34. package/dist/diagnostics/derivers.d.ts.map +1 -0
  35. package/dist/diagnostics/derivers.js +146 -0
  36. package/dist/diagnostics/index.d.ts +3 -0
  37. package/dist/diagnostics/index.d.ts.map +1 -0
  38. package/dist/diagnostics/index.js +2 -0
  39. package/dist/diagnostics/report.d.ts +7 -0
  40. package/dist/diagnostics/report.d.ts.map +1 -0
  41. package/dist/diagnostics/report.js +84 -0
  42. package/dist/environment-reset.d.ts +51 -0
  43. package/dist/environment-reset.d.ts.map +1 -0
  44. package/dist/environment-reset.js +83 -0
  45. package/dist/errors.d.ts +10 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +41 -2
  48. package/dist/execute-run.d.ts +4 -2
  49. package/dist/execute-run.d.ts.map +1 -1
  50. package/dist/execute-run.js +179 -49
  51. package/dist/format-harness-error.d.ts +5 -0
  52. package/dist/format-harness-error.d.ts.map +1 -0
  53. package/dist/format-harness-error.js +43 -0
  54. package/dist/harness-session.d.ts +46 -1
  55. package/dist/harness-session.d.ts.map +1 -1
  56. package/dist/harness-session.js +386 -118
  57. package/dist/index.d.ts +7 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +24 -2
  60. package/dist/run.d.ts.map +1 -1
  61. package/dist/run.js +22 -4
  62. package/dist/test-file-platform-filter.d.ts +5 -0
  63. package/dist/test-file-platform-filter.d.ts.map +1 -0
  64. package/dist/test-file-platform-filter.js +40 -0
  65. package/dist/toTestResult.d.ts +1 -0
  66. package/dist/toTestResult.d.ts.map +1 -1
  67. package/dist/toTestResult.js +2 -2
  68. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  69. package/package.json +8 -7
  70. package/src/__tests__/bridge.test.ts +45 -5
  71. package/src/__tests__/crash-monitor.test.ts +234 -199
  72. package/src/__tests__/environment-reset.test.ts +308 -0
  73. package/src/__tests__/errors.test.ts +75 -0
  74. package/src/__tests__/execute-run.test.ts +561 -37
  75. package/src/__tests__/format-harness-error.test.ts +32 -0
  76. package/src/__tests__/harness-cache.test.ts +91 -0
  77. package/src/__tests__/harness-session.test.ts +303 -0
  78. package/src/__tests__/index.test.ts +92 -12
  79. package/src/__tests__/metro-port.test.ts +2 -0
  80. package/src/__tests__/run.test.ts +74 -0
  81. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  82. package/src/crash-monitor.ts +220 -131
  83. package/src/diagnostics/__tests__/derivers.test.ts +300 -0
  84. package/src/diagnostics/__tests__/report.test.ts +128 -0
  85. package/src/diagnostics/derivers.ts +170 -0
  86. package/src/diagnostics/index.ts +2 -0
  87. package/src/diagnostics/report.ts +124 -0
  88. package/src/environment-reset.ts +138 -0
  89. package/src/errors.ts +62 -1
  90. package/src/execute-run.ts +230 -57
  91. package/src/format-harness-error.ts +73 -0
  92. package/src/harness-session.ts +535 -162
  93. package/src/index.ts +40 -11
  94. package/src/run.ts +24 -4
  95. package/src/test-file-platform-filter.ts +57 -0
  96. package/src/toTestResult.ts +3 -2
  97. package/tsconfig.json +3 -0
  98. package/tsconfig.lib.json +3 -0
@@ -1,20 +1,44 @@
1
1
  import type {
2
- OnTestFailure,
3
- OnTestStart,
4
- OnTestSuccess,
5
2
  Test,
6
3
  TestWatcher,
7
4
  Config,
5
+ TestEvents,
8
6
  } from 'jest-runner';
9
7
  import { randomUUID } from 'node:crypto';
10
8
  import path from 'node:path';
11
9
  import { type HarnessSession, type HarnessRunState } from './harness-session.js';
12
10
  import { runHarnessTestFile } from './run.js';
11
+ import { resolveResetStrategyKind } from './environment-reset.js';
13
12
  import {
14
13
  NativeCrashError,
14
+ RuntimeDisconnectError,
15
15
  StartupStallError,
16
16
  } from './errors.js';
17
- import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
17
+ import {
18
+ AppBridgeDisconnectedError,
19
+ DeviceNotRespondingError,
20
+ } from '@react-native-harness/bridge/server';
21
+ import type { TestCaseResult } from '@jest/test-result';
22
+ import type {
23
+ TestRunnerEvents,
24
+ TestRunnerTestFinishedEvent,
25
+ TestRunnerTestStartedEvent,
26
+ TestSuiteResult,
27
+ } from '@react-native-harness/bridge';
28
+ import {
29
+ createPlatformSkippedTestResult,
30
+ shouldRunHarnessTestFile,
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');
37
+
38
+ type EmitTestEvent = <Name extends keyof TestEvents>(
39
+ eventName: Name,
40
+ ...eventData: TestEvents[Name]
41
+ ) => Promise<void>;
18
42
 
19
43
  const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
20
44
 
@@ -38,7 +62,9 @@ class CancelRun extends Error {
38
62
  const buildTestFailure = (err: unknown): { message: string; stack: string } => {
39
63
  if (
40
64
  err instanceof NativeCrashError ||
65
+ err instanceof RuntimeDisconnectError ||
41
66
  err instanceof StartupStallError ||
67
+ err instanceof AppBridgeDisconnectedError ||
42
68
  err instanceof DeviceNotRespondingError
43
69
  ) {
44
70
  return { message: (err as Error).message, stack: '' };
@@ -46,21 +72,92 @@ const buildTestFailure = (err: unknown): { message: string; stack: string } => {
46
72
  return err as { message: string; stack: string };
47
73
  };
48
74
 
75
+ const toJestMode = (
76
+ declarationMode?: 'only' | 'skip' | 'todo',
77
+ ): 'only' | 'skip' | 'todo' | undefined => declarationMode;
78
+
79
+ const emitHarnessTestStarted = async (
80
+ emitEvent: EmitTestEvent,
81
+ event: TestRunnerTestStartedEvent,
82
+ ): Promise<void> => {
83
+ await emitEvent('test-case-start', event.file, {
84
+ ancestorTitles: event.ancestorTitles,
85
+ fullName: event.fullName,
86
+ mode: toJestMode(event.declarationMode),
87
+ title: event.name,
88
+ startedAt: event.startedAt,
89
+ });
90
+ };
91
+
92
+ const emitHarnessTestFinished = async (
93
+ emitEvent: EmitTestEvent,
94
+ event: TestRunnerTestFinishedEvent,
95
+ ): Promise<void> => {
96
+ const codeFrame = event.error?.codeFrame;
97
+ const failureMessage = formatHarnessErrorMessage(event.error, {
98
+ testStartedAt: event.startedAt,
99
+ });
100
+ const location = codeFrame?.location
101
+ ? { column: codeFrame.location.column, line: codeFrame.location.row }
102
+ : null;
103
+ const testCaseResult: TestCaseResult = {
104
+ ancestorTitles: event.ancestorTitles,
105
+ duration: event.duration,
106
+ failureDetails: [],
107
+ failureMessages: failureMessage ? [failureMessage] : [],
108
+ fullName: event.fullName,
109
+ location,
110
+ numPassingAsserts: event.status === 'passed' ? 1 : 0,
111
+ startedAt: event.startedAt,
112
+ status: event.status,
113
+ title: event.name,
114
+ };
115
+
116
+ await emitEvent('test-case-result', event.file, testCaseResult);
117
+ };
118
+
119
+ const isHarnessCaseEvent = (
120
+ event: TestRunnerEvents,
121
+ ): event is TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent =>
122
+ event.type === 'test-started' || event.type === 'test-finished';
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
+
49
136
  export const executeRun = async (
50
137
  session: HarnessSession,
51
138
  tests: Array<Test>,
52
139
  watcher: TestWatcher,
53
- onStart: OnTestStart,
54
- onResult: OnTestSuccess,
55
- onFailure: OnTestFailure,
140
+ emitEvent: EmitTestEvent,
56
141
  globalConfig: Config.GlobalConfig,
57
142
  ): Promise<void> => {
58
143
  const runId = randomUUID();
144
+ const diagnostics = session.diagnostics.child({ runId });
145
+ const runSpan = diagnostics.start('run.total', { runId });
59
146
  const startTime = Date.now();
60
147
  const watchMode = globalConfig.watch || globalConfig.watchAll;
61
148
  const rootDir = globalConfig.rootDir ?? process.cwd();
62
149
  const testFiles = tests.map((t) => path.relative(rootDir, t.path));
63
150
  const summary = createRunSummary();
151
+ let caseEventChain = Promise.resolve();
152
+ const unsubscribe = session.onTestRunnerEvent((event) => {
153
+ if (isHarnessCaseEvent(event)) {
154
+ caseEventChain = caseEventChain.then(() =>
155
+ event.type === 'test-started'
156
+ ? emitHarnessTestStarted(emitEvent, event)
157
+ : emitHarnessTestFinished(emitEvent, event),
158
+ );
159
+ }
160
+ });
64
161
 
65
162
  const updateRunState = (overrides: Partial<HarnessRunState> = {}) => {
66
163
  const state: HarnessRunState = {
@@ -69,6 +166,7 @@ export const executeRun = async (
69
166
  testFiles,
70
167
  watchMode,
71
168
  coverageEnabled: globalConfig.collectCoverage,
169
+ completed: false,
72
170
  summary,
73
171
  status: summary.failed > 0 ? 'failed' : 'passed',
74
172
  ...overrides,
@@ -92,8 +190,15 @@ export const executeRun = async (
92
190
  host: session.config.host?.trim() || undefined,
93
191
  });
94
192
 
95
- const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
193
+ const resetStrategyKind = resolveResetStrategyKind(
194
+ session.config.resetEnvironmentBetweenTestFiles,
195
+ );
196
+ const platformId = session.context.platform.platformId;
197
+ const knownPlatformIds = new Set(
198
+ session.config.runners.map((runner) => runner.platformId),
199
+ );
96
200
  let isFirstTest = true;
201
+ let shouldRestartAfterTimeout = false;
97
202
  let runError: unknown;
98
203
 
99
204
  try {
@@ -120,63 +225,116 @@ export const executeRun = async (
120
225
  };
121
226
 
122
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';
123
230
 
124
231
  try {
125
- if (shouldResetEnv && !isFirstTest) {
126
- await session.restartApp(test.path);
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;
127
260
  }
128
- isFirstTest = false;
129
-
130
- session.flushClientLogs();
131
- await onStart(test);
132
- await session.ensureAppReady(test.path);
133
-
134
- // Crash detection is handled inside session.runTestFile; NativeCrashError
135
- // propagates here if a crash wins the race.
136
- const result = await runHarnessTestFile({
137
- testPath: test.path,
138
- session,
139
- globalConfig,
140
- projectConfig: test.context.config,
141
- });
142
261
 
143
- applyJestResultToSummary(summary, result.jestResult);
144
- const clientLogs = session.flushClientLogs();
145
- if (clientLogs.length > 0) {
146
- result.jestResult.console = clientLogs;
147
- }
148
- updateRunState();
149
- await emitTestFileFinished({
150
- status: result.harnessResult.status,
151
- duration: result.duration,
152
- result: result.harnessResult,
153
- });
154
- await onResult(test, result.jestResult);
155
- } catch (err) {
156
- if (!emittedTestFileFinished) {
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();
274
+ await emitEvent('test-file-start', test);
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
+ }
291
+ updateRunState();
292
+ fileStatus = result.harnessResult.status;
157
293
  await emitTestFileFinished({
158
- status: 'failed',
159
- duration: Date.now() - fileStartedAt,
160
- result: null,
294
+ status: result.harnessResult.status,
295
+ duration: result.duration,
296
+ result: result.harnessResult,
161
297
  });
162
- }
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
+ }
306
+ } catch (err) {
307
+ fileStatus = 'failed';
308
+ if (!emittedTestFileFinished) {
309
+ await emitTestFileFinished({
310
+ status: 'failed',
311
+ duration: Date.now() - fileStartedAt,
312
+ result: null,
313
+ });
314
+ }
163
315
 
164
- const isRuntimeFailure =
165
- err instanceof NativeCrashError ||
166
- err instanceof StartupStallError ||
167
- err instanceof DeviceNotRespondingError;
316
+ const isRuntimeFailure =
317
+ err instanceof NativeCrashError ||
318
+ err instanceof RuntimeDisconnectError ||
319
+ err instanceof StartupStallError ||
320
+ err instanceof AppBridgeDisconnectedError ||
321
+ err instanceof DeviceNotRespondingError;
168
322
 
169
- if (isRuntimeFailure) {
170
- summary.failed += 1;
171
- updateRunState();
172
- }
323
+ if (isRuntimeFailure) {
324
+ summary.failed += 1;
325
+ updateRunState();
326
+ }
173
327
 
174
- if (err instanceof NativeCrashError) {
175
- session.resetCrashState();
176
- }
328
+ if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
329
+ session.resetCrashState();
330
+ }
177
331
 
178
- updateRunState({ error: isRuntimeFailure ? undefined : err });
179
- onFailure(test, buildTestFailure(err));
332
+ updateRunState({ error: isRuntimeFailure ? undefined : err });
333
+ await caseEventChain;
334
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
335
+ }
336
+ } finally {
337
+ fileSpan.end({ status: fileStatus });
180
338
  }
181
339
  }
182
340
  } catch (err) {
@@ -184,16 +342,31 @@ export const executeRun = async (
184
342
  if (!(err instanceof CancelRun)) throw err;
185
343
  } finally {
186
344
  const runState = updateRunState(
187
- runError != null ? { error: runError, status: 'failed' } : {},
345
+ runError != null ? { completed: true, error: runError, status: 'failed' } : { completed: true },
188
346
  );
347
+ const finalStatus = runState.status ?? (runError != null ? 'failed' : 'passed');
189
348
  await session.callHook('run:finished', {
190
349
  runId,
191
350
  startTime,
192
351
  duration: Date.now() - startTime,
193
352
  testFiles,
194
353
  summary,
195
- status: runState.status ?? (runError != null ? 'failed' : 'passed'),
354
+ status: finalStatus,
196
355
  ...(runError != null ? { error: runError } : {}),
197
356
  });
357
+ await caseEventChain;
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
+ }
198
371
  }
199
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
+ };