@react-native-harness/jest 1.3.0 → 1.4.0-rc.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 (49) hide show
  1. package/dist/__tests__/bridge.test.js +23 -2
  2. package/dist/__tests__/crash-monitor.test.js +146 -192
  3. package/dist/__tests__/errors.test.js +39 -0
  4. package/dist/__tests__/execute-run.test.js +190 -26
  5. package/dist/__tests__/harness-session.test.js +63 -239
  6. package/dist/__tests__/index.test.js +59 -10
  7. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  8. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  9. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  10. package/dist/crash-monitor.d.ts +5 -4
  11. package/dist/crash-monitor.d.ts.map +1 -1
  12. package/dist/crash-monitor.js +116 -102
  13. package/dist/errors.d.ts +10 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +41 -2
  16. package/dist/execute-run.d.ts +4 -2
  17. package/dist/execute-run.d.ts.map +1 -1
  18. package/dist/execute-run.js +85 -7
  19. package/dist/harness-session.d.ts +21 -1
  20. package/dist/harness-session.d.ts.map +1 -1
  21. package/dist/harness-session.js +134 -39
  22. package/dist/index.d.ts +7 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +24 -2
  25. package/dist/run.d.ts.map +1 -1
  26. package/dist/run.js +1 -0
  27. package/dist/test-file-platform-filter.d.ts +5 -0
  28. package/dist/test-file-platform-filter.d.ts.map +1 -0
  29. package/dist/test-file-platform-filter.js +40 -0
  30. package/dist/toTestResult.d.ts +1 -0
  31. package/dist/toTestResult.d.ts.map +1 -1
  32. package/dist/toTestResult.js +2 -2
  33. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  34. package/package.json +7 -7
  35. package/src/__tests__/bridge.test.ts +37 -2
  36. package/src/__tests__/crash-monitor.test.ts +182 -231
  37. package/src/__tests__/errors.test.ts +75 -0
  38. package/src/__tests__/execute-run.test.ts +235 -34
  39. package/src/__tests__/harness-session.test.ts +93 -0
  40. package/src/__tests__/index.test.ts +89 -11
  41. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  42. package/src/crash-monitor.ts +192 -131
  43. package/src/errors.ts +62 -1
  44. package/src/execute-run.ts +125 -11
  45. package/src/harness-session.ts +177 -42
  46. package/src/index.ts +40 -11
  47. package/src/run.ts +1 -0
  48. package/src/test-file-platform-filter.ts +57 -0
  49. package/src/toTestResult.ts +3 -2
package/src/errors.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HarnessError } from '@react-native-harness/tools';
1
+ import { formatPath, HarnessError } from '@react-native-harness/tools';
2
2
  import type { AppCrashDetails } from '@react-native-harness/platforms';
3
3
  export {
4
4
  StartupStallError,
@@ -55,6 +55,10 @@ export type NativeCrashDetails = AppCrashDetails & {
55
55
  phase: NativeCrashPhase;
56
56
  };
57
57
 
58
+ export type RuntimeDisconnectDetails = AppCrashDetails & {
59
+ phase: NativeCrashPhase;
60
+ };
61
+
58
62
  const buildNativeCrashMessage = ({
59
63
  phase,
60
64
  summary,
@@ -64,12 +68,28 @@ const buildNativeCrashMessage = ({
64
68
  pid,
65
69
  stackTrace,
66
70
  artifactType,
71
+ artifactPath,
72
+ enrichmentArtifacts,
67
73
  }: NativeCrashDetails) => {
68
74
  const lines = [
69
75
  phase === 'startup'
70
76
  ? 'The native app crashed while preparing to run this test file.'
71
77
  : 'The native app crashed during test execution.',
72
78
  ];
79
+
80
+ lines.push(
81
+ artifactPath
82
+ ? `Harness extracted the crash log: ${formatPath(artifactPath)}`
83
+ : "Harness couldn't extract the crash log."
84
+ );
85
+
86
+ if (enrichmentArtifacts && enrichmentArtifacts.length > 0) {
87
+ lines.push('Additional crash artifacts:');
88
+ for (const artifact of enrichmentArtifacts) {
89
+ lines.push(` - ${formatPath(artifact.artifactPath)}`);
90
+ }
91
+ }
92
+
73
93
  const hasCrashBlock = summary?.includes('\n') ?? false;
74
94
  const shouldRenderSummary =
75
95
  Boolean(summary) &&
@@ -119,3 +139,44 @@ export class NativeCrashError extends HarnessError {
119
139
  return this.details.phase;
120
140
  }
121
141
  }
142
+
143
+ const buildRuntimeDisconnectMessage = ({
144
+ phase,
145
+ summary,
146
+ rawLines,
147
+ }: RuntimeDisconnectDetails) => {
148
+ const lines = [
149
+ phase === 'startup'
150
+ ? 'The native runtime disconnected while preparing to run this test file.'
151
+ : 'The native runtime disconnected during test execution.',
152
+ ];
153
+
154
+ if (summary) {
155
+ lines.push('');
156
+ lines.push(summary);
157
+ }
158
+
159
+ if (rawLines && rawLines.length > 0 && summary !== rawLines.join('\n')) {
160
+ lines.push('');
161
+ lines.push(...rawLines);
162
+ }
163
+
164
+ return lines.join('\n');
165
+ };
166
+
167
+ export class RuntimeDisconnectError extends HarnessError {
168
+ constructor(
169
+ public readonly testFilePath: string,
170
+ public readonly details: RuntimeDisconnectDetails
171
+ ) {
172
+ super(buildRuntimeDisconnectMessage(details));
173
+ this.name = 'RuntimeDisconnectError';
174
+ this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
175
+ }
176
+
177
+ get phase() {
178
+ return this.details.phase;
179
+ }
180
+ }
181
+
182
+ export type HarnessRuntimeFailure = NativeCrashError | RuntimeDisconnectError;
@@ -1,10 +1,8 @@
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';
@@ -12,9 +10,28 @@ import { type HarnessSession, type HarnessRunState } from './harness-session.js'
12
10
  import { runHarnessTestFile } from './run.js';
13
11
  import {
14
12
  NativeCrashError,
13
+ RuntimeDisconnectError,
15
14
  StartupStallError,
16
15
  } from './errors.js';
17
- import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
16
+ import {
17
+ AppBridgeDisconnectedError,
18
+ DeviceNotRespondingError,
19
+ } from '@react-native-harness/bridge/server';
20
+ import type { TestCaseResult } from '@jest/test-result';
21
+ import type {
22
+ TestRunnerEvents,
23
+ TestRunnerTestFinishedEvent,
24
+ TestRunnerTestStartedEvent,
25
+ } from '@react-native-harness/bridge';
26
+ import {
27
+ createPlatformSkippedTestResult,
28
+ shouldRunHarnessTestFile,
29
+ } from './test-file-platform-filter.js';
30
+
31
+ type EmitTestEvent = <Name extends keyof TestEvents>(
32
+ eventName: Name,
33
+ ...eventData: TestEvents[Name]
34
+ ) => Promise<void>;
18
35
 
19
36
  const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
20
37
 
@@ -38,7 +55,9 @@ class CancelRun extends Error {
38
55
  const buildTestFailure = (err: unknown): { message: string; stack: string } => {
39
56
  if (
40
57
  err instanceof NativeCrashError ||
58
+ err instanceof RuntimeDisconnectError ||
41
59
  err instanceof StartupStallError ||
60
+ err instanceof AppBridgeDisconnectedError ||
42
61
  err instanceof DeviceNotRespondingError
43
62
  ) {
44
63
  return { message: (err as Error).message, stack: '' };
@@ -46,13 +65,60 @@ const buildTestFailure = (err: unknown): { message: string; stack: string } => {
46
65
  return err as { message: string; stack: string };
47
66
  };
48
67
 
68
+ const toJestMode = (
69
+ declarationMode?: 'only' | 'skip' | 'todo',
70
+ ): 'only' | 'skip' | 'todo' | undefined => declarationMode;
71
+
72
+ const emitHarnessTestStarted = async (
73
+ emitEvent: EmitTestEvent,
74
+ event: TestRunnerTestStartedEvent,
75
+ ): Promise<void> => {
76
+ await emitEvent('test-case-start', event.file, {
77
+ ancestorTitles: event.ancestorTitles,
78
+ fullName: event.fullName,
79
+ mode: toJestMode(event.declarationMode),
80
+ title: event.name,
81
+ startedAt: event.startedAt,
82
+ });
83
+ };
84
+
85
+ const emitHarnessTestFinished = async (
86
+ emitEvent: EmitTestEvent,
87
+ event: TestRunnerTestFinishedEvent,
88
+ ): Promise<void> => {
89
+ const failureMessage = event.error?.message;
90
+ const codeFrame = event.error?.codeFrame;
91
+ const location = codeFrame?.location
92
+ ? { column: codeFrame.location.column, line: codeFrame.location.row }
93
+ : null;
94
+ const testCaseResult: TestCaseResult = {
95
+ ancestorTitles: event.ancestorTitles,
96
+ duration: event.duration,
97
+ failureDetails: [],
98
+ failureMessages: failureMessage
99
+ ? [`${failureMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`]
100
+ : [],
101
+ fullName: event.fullName,
102
+ location,
103
+ numPassingAsserts: event.status === 'passed' ? 1 : 0,
104
+ startedAt: event.startedAt,
105
+ status: event.status,
106
+ title: event.name,
107
+ };
108
+
109
+ await emitEvent('test-case-result', event.file, testCaseResult);
110
+ };
111
+
112
+ const isHarnessCaseEvent = (
113
+ event: TestRunnerEvents,
114
+ ): event is TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent =>
115
+ event.type === 'test-started' || event.type === 'test-finished';
116
+
49
117
  export const executeRun = async (
50
118
  session: HarnessSession,
51
119
  tests: Array<Test>,
52
120
  watcher: TestWatcher,
53
- onStart: OnTestStart,
54
- onResult: OnTestSuccess,
55
- onFailure: OnTestFailure,
121
+ emitEvent: EmitTestEvent,
56
122
  globalConfig: Config.GlobalConfig,
57
123
  ): Promise<void> => {
58
124
  const runId = randomUUID();
@@ -61,6 +127,16 @@ export const executeRun = async (
61
127
  const rootDir = globalConfig.rootDir ?? process.cwd();
62
128
  const testFiles = tests.map((t) => path.relative(rootDir, t.path));
63
129
  const summary = createRunSummary();
130
+ let caseEventChain = Promise.resolve();
131
+ const unsubscribe = session.onTestRunnerEvent((event) => {
132
+ if (isHarnessCaseEvent(event)) {
133
+ caseEventChain = caseEventChain.then(() =>
134
+ event.type === 'test-started'
135
+ ? emitHarnessTestStarted(emitEvent, event)
136
+ : emitHarnessTestFinished(emitEvent, event),
137
+ );
138
+ }
139
+ });
64
140
 
65
141
  const updateRunState = (overrides: Partial<HarnessRunState> = {}) => {
66
142
  const state: HarnessRunState = {
@@ -93,6 +169,10 @@ export const executeRun = async (
93
169
  });
94
170
 
95
171
  const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
172
+ const platformId = session.context.platform.platformId;
173
+ const knownPlatformIds = new Set(
174
+ session.config.runners.map((runner) => runner.platformId),
175
+ );
96
176
  let isFirstTest = true;
97
177
  let runError: unknown;
98
178
 
@@ -121,6 +201,34 @@ export const executeRun = async (
121
201
 
122
202
  await session.callHook('test-file:started', { runId, file: relativeTestPath });
123
203
 
204
+ if (
205
+ !shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)
206
+ ) {
207
+ try {
208
+ await emitEvent('test-file-start', test);
209
+ const skippedResult = createPlatformSkippedTestResult(test.path);
210
+ applyJestResultToSummary(summary, skippedResult);
211
+ updateRunState();
212
+ await emitTestFileFinished({
213
+ status: 'skipped',
214
+ duration: Date.now() - fileStartedAt,
215
+ result: null,
216
+ });
217
+ await emitEvent('test-file-success', test, skippedResult);
218
+ } catch (err) {
219
+ if (!emittedTestFileFinished) {
220
+ await emitTestFileFinished({
221
+ status: 'failed',
222
+ duration: Date.now() - fileStartedAt,
223
+ result: null,
224
+ });
225
+ }
226
+ updateRunState({ error: err });
227
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
228
+ }
229
+ continue;
230
+ }
231
+
124
232
  try {
125
233
  if (shouldResetEnv && !isFirstTest) {
126
234
  await session.restartApp(test.path);
@@ -128,7 +236,7 @@ export const executeRun = async (
128
236
  isFirstTest = false;
129
237
 
130
238
  session.flushClientLogs();
131
- await onStart(test);
239
+ await emitEvent('test-file-start', test);
132
240
  await session.ensureAppReady(test.path);
133
241
 
134
242
  // Crash detection is handled inside session.runTestFile; NativeCrashError
@@ -151,7 +259,8 @@ export const executeRun = async (
151
259
  duration: result.duration,
152
260
  result: result.harnessResult,
153
261
  });
154
- await onResult(test, result.jestResult);
262
+ await caseEventChain;
263
+ await emitEvent('test-file-success', test, result.jestResult);
155
264
  } catch (err) {
156
265
  if (!emittedTestFileFinished) {
157
266
  await emitTestFileFinished({
@@ -163,7 +272,9 @@ export const executeRun = async (
163
272
 
164
273
  const isRuntimeFailure =
165
274
  err instanceof NativeCrashError ||
275
+ err instanceof RuntimeDisconnectError ||
166
276
  err instanceof StartupStallError ||
277
+ err instanceof AppBridgeDisconnectedError ||
167
278
  err instanceof DeviceNotRespondingError;
168
279
 
169
280
  if (isRuntimeFailure) {
@@ -171,12 +282,13 @@ export const executeRun = async (
171
282
  updateRunState();
172
283
  }
173
284
 
174
- if (err instanceof NativeCrashError) {
285
+ if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
175
286
  session.resetCrashState();
176
287
  }
177
288
 
178
289
  updateRunState({ error: isRuntimeFailure ? undefined : err });
179
- onFailure(test, buildTestFailure(err));
290
+ await caseEventChain;
291
+ await emitEvent('test-file-failure', test, buildTestFailure(err));
180
292
  }
181
293
  }
182
294
  } catch (err) {
@@ -195,5 +307,7 @@ export const executeRun = async (
195
307
  status: runState.status ?? (runError != null ? 'failed' : 'passed'),
196
308
  ...(runError != null ? { error: runError } : {}),
197
309
  });
310
+ await caseEventChain;
311
+ unsubscribe();
198
312
  }
199
313
  };