@react-native-harness/jest 1.0.0 → 1.1.0-rc.2

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 (80) hide show
  1. package/dist/__tests__/crash-supervisor.test.d.ts +2 -0
  2. package/dist/__tests__/crash-supervisor.test.d.ts.map +1 -0
  3. package/dist/__tests__/crash-supervisor.test.js +151 -0
  4. package/dist/__tests__/errors.test.d.ts +2 -0
  5. package/dist/__tests__/errors.test.d.ts.map +1 -0
  6. package/dist/__tests__/errors.test.js +54 -0
  7. package/dist/__tests__/harness-cache.test.d.ts +2 -0
  8. package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
  9. package/dist/__tests__/harness-cache.test.js +51 -0
  10. package/dist/__tests__/harness.test.d.ts +2 -0
  11. package/dist/__tests__/harness.test.d.ts.map +1 -0
  12. package/dist/__tests__/harness.test.js +399 -0
  13. package/dist/__tests__/index.test.d.ts +2 -0
  14. package/dist/__tests__/index.test.d.ts.map +1 -0
  15. package/dist/__tests__/index.test.js +42 -0
  16. package/dist/cli-args.d.ts +1 -0
  17. package/dist/cli-args.d.ts.map +1 -1
  18. package/dist/cli-args.js +5 -0
  19. package/dist/crash-supervisor.d.ts +21 -0
  20. package/dist/crash-supervisor.d.ts.map +1 -0
  21. package/dist/crash-supervisor.js +210 -0
  22. package/dist/errors.d.ts +9 -4
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +43 -8
  25. package/dist/harness.d.ts +30 -8
  26. package/dist/harness.d.ts.map +1 -1
  27. package/dist/harness.js +440 -98
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +205 -57
  30. package/dist/logs.d.ts +1 -0
  31. package/dist/logs.d.ts.map +1 -1
  32. package/dist/logs.js +3 -0
  33. package/dist/run.d.ts +7 -1
  34. package/dist/run.d.ts.map +1 -1
  35. package/dist/run.js +13 -8
  36. package/dist/setup.d.ts.map +1 -1
  37. package/dist/setup.js +18 -6
  38. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  39. package/package.json +7 -6
  40. package/src/__tests__/crash-supervisor.test.ts +200 -0
  41. package/src/__tests__/errors.test.ts +70 -0
  42. package/src/__tests__/harness-cache.test.ts +74 -0
  43. package/src/__tests__/harness.test.ts +547 -0
  44. package/src/__tests__/index.test.ts +61 -0
  45. package/src/cli-args.ts +6 -0
  46. package/src/crash-supervisor.ts +321 -0
  47. package/src/env.d.ts +1 -1
  48. package/src/errors.ts +68 -8
  49. package/src/harness.ts +566 -120
  50. package/src/index.ts +276 -66
  51. package/src/logs.ts +4 -0
  52. package/src/run.ts +20 -10
  53. package/src/setup.ts +22 -6
  54. package/tsconfig.json +3 -3
  55. package/tsconfig.lib.json +6 -6
  56. package/dist/cli-args.test.d.ts +0 -2
  57. package/dist/cli-args.test.d.ts.map +0 -1
  58. package/dist/cli-args.test.js +0 -1
  59. package/dist/crash-monitor.d.ts +0 -16
  60. package/dist/crash-monitor.d.ts.map +0 -1
  61. package/dist/crash-monitor.js +0 -75
  62. package/dist/event-mapper.d.ts +0 -16
  63. package/dist/event-mapper.d.ts.map +0 -1
  64. package/dist/event-mapper.js +0 -120
  65. package/dist/global-setup.d.ts +0 -3
  66. package/dist/global-setup.d.ts.map +0 -1
  67. package/dist/global-setup.js +0 -60
  68. package/dist/global-teardown.d.ts +0 -3
  69. package/dist/global-teardown.d.ts.map +0 -1
  70. package/dist/global-teardown.js +0 -11
  71. package/dist/harness-reporter.d.ts +0 -8
  72. package/dist/harness-reporter.d.ts.map +0 -1
  73. package/dist/harness-reporter.js +0 -12
  74. package/dist/jest-options.d.ts +0 -2
  75. package/dist/jest-options.d.ts.map +0 -1
  76. package/dist/jest-options.js +0 -1
  77. package/dist/types.d.ts +0 -17
  78. package/dist/types.d.ts.map +0 -1
  79. package/dist/types.js +0 -1
  80. package/src/crash-monitor.ts +0 -109
@@ -0,0 +1,321 @@
1
+ import {
2
+ type AppMonitor,
3
+ type AppCrashDetails,
4
+ type AppMonitorEvent,
5
+ type AppMonitorListener,
6
+ type HarnessPlatformRunner,
7
+ } from '@react-native-harness/platforms';
8
+ import {
9
+ NativeCrashError,
10
+ type NativeCrashDetails,
11
+ type NativeCrashPhase,
12
+ } from './errors.js';
13
+ import { logger } from '@react-native-harness/tools';
14
+
15
+ const crashLogger = logger.child('crash');
16
+
17
+ export type CrashSupervisorState =
18
+ | 'idle'
19
+ | 'launching'
20
+ | 'ready'
21
+ | 'running'
22
+ | 'disposing';
23
+
24
+ export type CrashSupervisor = {
25
+ setActiveTestFile: (testFilePath: string | null) => void;
26
+ beginLaunch: (testFilePath: string) => void;
27
+ markReady: () => void;
28
+ beginTestRun: (testFilePath: string) => void;
29
+ stop: () => Promise<void>;
30
+ start: () => Promise<void>;
31
+ waitForCrash: (testFilePath: string) => Promise<never>;
32
+ isReady: () => boolean;
33
+ cancelCrashWaiters: () => void;
34
+ reset: () => void;
35
+ dispose: () => Promise<void>;
36
+ };
37
+
38
+ export type CrashSupervisorOptions = {
39
+ appMonitor: AppMonitor;
40
+ platformRunner: HarnessPlatformRunner;
41
+ };
42
+
43
+ type CrashDetailsProvider = {
44
+ getCrashDetails?: (
45
+ options: {
46
+ processName?: string;
47
+ pid?: number;
48
+ occurredAt: number;
49
+ }
50
+ ) => Promise<AppCrashDetails | null>;
51
+ };
52
+
53
+ const getCrashPhase = (state: CrashSupervisorState): NativeCrashPhase =>
54
+ state === 'running' ? 'execution' : 'startup';
55
+
56
+ const mergeCrashDetails = (
57
+ phase: NativeCrashPhase,
58
+ initialDetails?: AppCrashDetails,
59
+ enrichedDetails?: AppCrashDetails | null,
60
+ fallbackSummary?: string
61
+ ): NativeCrashDetails => ({
62
+ phase,
63
+ source: enrichedDetails?.source ?? initialDetails?.source,
64
+ summary:
65
+ enrichedDetails?.summary ?? initialDetails?.summary ?? fallbackSummary,
66
+ signal: enrichedDetails?.signal ?? initialDetails?.signal,
67
+ exceptionType: enrichedDetails?.exceptionType ?? initialDetails?.exceptionType,
68
+ processName: enrichedDetails?.processName ?? initialDetails?.processName,
69
+ pid: enrichedDetails?.pid ?? initialDetails?.pid,
70
+ stackTrace: enrichedDetails?.stackTrace ?? initialDetails?.stackTrace,
71
+ rawLines: enrichedDetails?.rawLines ?? initialDetails?.rawLines,
72
+ artifactType: enrichedDetails?.artifactType ?? initialDetails?.artifactType,
73
+ artifactPath: enrichedDetails?.artifactPath ?? initialDetails?.artifactPath,
74
+ });
75
+
76
+ export const createCrashSupervisor = ({
77
+ appMonitor,
78
+ platformRunner,
79
+ }: CrashSupervisorOptions): CrashSupervisor => {
80
+ let state: CrashSupervisorState = 'idle';
81
+ let activeTestFilePath: string | null = null;
82
+ let crashRejectors = new Set<(error: NativeCrashError) => void>();
83
+ let disposed = false;
84
+ let monitoring = true;
85
+ let isResolvingCrash = false;
86
+
87
+ const getCrashDetailsProvider = (): CrashDetailsProvider | null => {
88
+ if ('getCrashDetails' in appMonitor) {
89
+ return appMonitor as AppMonitor & CrashDetailsProvider;
90
+ }
91
+
92
+ if (platformRunner.getCrashDetails) {
93
+ return platformRunner;
94
+ }
95
+
96
+ return null;
97
+ };
98
+
99
+ const rejectCrashWaiters = (testFilePath: string, details: NativeCrashDetails) => {
100
+ const error = new NativeCrashError(testFilePath, details);
101
+
102
+ for (const reject of crashRejectors) {
103
+ reject(error);
104
+ }
105
+
106
+ crashRejectors = new Set();
107
+ };
108
+
109
+ const handleCrash = async (reason: string, details?: AppCrashDetails) => {
110
+ if (
111
+ isResolvingCrash ||
112
+ (state !== 'launching' && state !== 'running' && state !== 'ready')
113
+ ) {
114
+ return;
115
+ }
116
+
117
+ if (!activeTestFilePath) {
118
+ crashLogger.debug('ignoring crash signal without active test: %s', reason);
119
+ return;
120
+ }
121
+
122
+ isResolvingCrash = true;
123
+ crashLogger.debug(
124
+ 'native crash detected during %s (state=%s reason=%s)',
125
+ activeTestFilePath,
126
+ state,
127
+ reason || '(none)'
128
+ );
129
+
130
+ for (const line of details?.rawLines ?? []) {
131
+ crashLogger.debug('%s', line);
132
+ }
133
+
134
+ const phase = getCrashPhase(state);
135
+ const testFilePath = activeTestFilePath;
136
+ state = 'idle';
137
+
138
+ try {
139
+ const enrichedDetails = await getCrashDetailsProvider()?.getCrashDetails?.({
140
+ processName: details?.processName,
141
+ pid: details?.pid,
142
+ occurredAt: Date.now(),
143
+ });
144
+
145
+ const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
146
+ crashLogger.debug('crash details: %o', {
147
+ phase: mergedDetails.phase,
148
+ source: mergedDetails.source,
149
+ summary: mergedDetails.summary,
150
+ signal: mergedDetails.signal,
151
+ exceptionType: mergedDetails.exceptionType,
152
+ processName: mergedDetails.processName,
153
+ pid: mergedDetails.pid,
154
+ });
155
+ rejectCrashWaiters(testFilePath, mergedDetails);
156
+ } finally {
157
+ isResolvingCrash = false;
158
+ }
159
+ };
160
+
161
+ const confirmCrash = async (reason: string, details?: AppCrashDetails) => {
162
+ if (disposed || !monitoring || state === 'disposing') {
163
+ return;
164
+ }
165
+
166
+ try {
167
+ const isRunning = await platformRunner.isAppRunning();
168
+
169
+ if (!isRunning) {
170
+ void handleCrash(reason, details);
171
+ }
172
+ } catch (error) {
173
+ crashLogger.debug('crash confirmation failed', error);
174
+ }
175
+ };
176
+
177
+ const appMonitorListener: AppMonitorListener = (event: AppMonitorEvent) => {
178
+ if (disposed || !monitoring) {
179
+ return;
180
+ }
181
+
182
+ if (event.type === 'app_started') {
183
+ return;
184
+ }
185
+
186
+ if (event.type === 'app_exited') {
187
+ const details = {
188
+ source: event.crashDetails?.source ?? event.source,
189
+ summary: event.crashDetails?.summary,
190
+ signal: event.crashDetails?.signal,
191
+ exceptionType: event.crashDetails?.exceptionType,
192
+ processName: event.crashDetails?.processName,
193
+ pid: event.crashDetails?.pid ?? event.pid,
194
+ stackTrace: event.crashDetails?.stackTrace,
195
+ rawLines:
196
+ event.crashDetails?.rawLines ??
197
+ (event.line ? [event.line] : undefined),
198
+ };
199
+
200
+ if (event.isConfirmed ?? event.source === 'polling') {
201
+ void handleCrash('', details);
202
+ } else {
203
+ void confirmCrash('', details);
204
+ }
205
+ return;
206
+ }
207
+
208
+ if (event.type === 'possible_crash') {
209
+ const details = {
210
+ source: event.crashDetails?.source ?? event.source,
211
+ summary: event.crashDetails?.summary,
212
+ signal: event.crashDetails?.signal,
213
+ exceptionType: event.crashDetails?.exceptionType,
214
+ processName: event.crashDetails?.processName,
215
+ pid: event.crashDetails?.pid ?? event.pid,
216
+ stackTrace: event.crashDetails?.stackTrace,
217
+ rawLines:
218
+ event.crashDetails?.rawLines ??
219
+ (event.line ? [event.line] : undefined),
220
+ };
221
+
222
+ if (event.isConfirmed) {
223
+ void handleCrash(
224
+ `possible crash signal (${event.source ?? 'unknown'})`,
225
+ details
226
+ );
227
+ } else {
228
+ void confirmCrash(
229
+ `possible crash signal (${event.source ?? 'unknown'})`,
230
+ details
231
+ );
232
+ }
233
+ }
234
+ };
235
+
236
+ appMonitor.addListener(appMonitorListener);
237
+
238
+ const setActiveTestFile = (testFilePath: string | null) => {
239
+ activeTestFilePath = testFilePath;
240
+ };
241
+
242
+ const beginLaunch = (testFilePath: string) => {
243
+ activeTestFilePath = testFilePath;
244
+ state = 'launching';
245
+ };
246
+
247
+ const markReady = () => {
248
+ if (state !== 'disposing') {
249
+ state = 'ready';
250
+ }
251
+ };
252
+
253
+ const beginTestRun = (testFilePath: string) => {
254
+ activeTestFilePath = testFilePath;
255
+ state = 'running';
256
+ };
257
+
258
+ const stop = async () => {
259
+ monitoring = false;
260
+ await appMonitor.stop();
261
+ };
262
+
263
+ const start = async () => {
264
+ monitoring = true;
265
+ await appMonitor.start();
266
+ };
267
+
268
+ const waitForCrash = (testFilePath: string): Promise<never> =>
269
+ new Promise<never>((_, reject) => {
270
+ if (disposed) {
271
+ reject(
272
+ new NativeCrashError(testFilePath, {
273
+ phase: 'startup',
274
+ source: 'polling',
275
+ summary: 'Crash supervisor disposed while waiting for app startup.',
276
+ })
277
+ );
278
+ return;
279
+ }
280
+
281
+ crashRejectors.add(reject);
282
+ });
283
+
284
+ const isReady = () => state === 'ready' || state === 'running';
285
+
286
+ const cancelCrashWaiters = () => {
287
+ crashRejectors = new Set();
288
+ };
289
+
290
+ const reset = () => {
291
+ cancelCrashWaiters();
292
+ isResolvingCrash = false;
293
+ if (state !== 'disposing') {
294
+ state = 'idle';
295
+ }
296
+ };
297
+
298
+ const dispose = async () => {
299
+ disposed = true;
300
+ monitoring = false;
301
+ state = 'disposing';
302
+ crashRejectors = new Set();
303
+ isResolvingCrash = false;
304
+ appMonitor.removeListener(appMonitorListener);
305
+ await appMonitor.dispose();
306
+ };
307
+
308
+ return {
309
+ setActiveTestFile,
310
+ beginLaunch,
311
+ markReady,
312
+ beginTestRun,
313
+ stop,
314
+ start,
315
+ waitForCrash,
316
+ isReady,
317
+ cancelCrashWaiters,
318
+ reset,
319
+ dispose,
320
+ };
321
+ };
package/src/env.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Harness } from '@react-native-harness/cli/external';
1
+ import type { Harness } from './harness.js';
2
2
  import type { Config as HarnessConfig } from '@react-native-harness/config';
3
3
 
4
4
  declare global {
package/src/errors.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { HarnessError } from '@react-native-harness/tools';
2
+ import type { AppCrashDetails } from '@react-native-harness/platforms';
3
+ export {
4
+ StartupStallError,
5
+ type StartupStallCode,
6
+ type StartupStallDetails,
7
+ } from '@react-native-harness/bundler-metro';
2
8
 
3
9
  export class NoRunnerSpecifiedError extends HarnessError {
4
10
  constructor() {
@@ -21,22 +27,76 @@ export class InitializationTimeoutError extends HarnessError {
21
27
  }
22
28
  }
23
29
 
24
- export class MaxAppRestartsError extends HarnessError {
25
- constructor(attempts: number) {
26
- super(
27
- `App failed to start after ${attempts} attempts. ` +
28
- `No bundling activity detected within timeout period.`
30
+ export type NativeCrashPhase = 'startup' | 'execution';
31
+
32
+ export type NativeCrashDetails = AppCrashDetails & {
33
+ phase: NativeCrashPhase;
34
+ };
35
+
36
+ const buildNativeCrashMessage = ({
37
+ phase,
38
+ summary,
39
+ signal,
40
+ exceptionType,
41
+ processName,
42
+ pid,
43
+ stackTrace,
44
+ artifactType,
45
+ }: NativeCrashDetails) => {
46
+ const lines = [
47
+ phase === 'startup'
48
+ ? 'The native app crashed while preparing to run this test file.'
49
+ : 'The native app crashed during test execution.',
50
+ ];
51
+ const hasCrashBlock = summary?.includes('\n') ?? false;
52
+ const shouldRenderSummary =
53
+ Boolean(summary) &&
54
+ !(
55
+ !hasCrashBlock &&
56
+ artifactType === 'ios-crash-report'
29
57
  );
30
- this.name = 'MaxAppRestartsError';
58
+
59
+ if (shouldRenderSummary && summary) {
60
+ lines.push('');
61
+ lines.push(summary);
62
+ }
63
+
64
+ if (!hasCrashBlock && signal) {
65
+ lines.push(`Signal: ${signal}`);
66
+ }
67
+
68
+ if (!hasCrashBlock && exceptionType) {
69
+ lines.push(`Exception: ${exceptionType}`);
31
70
  }
32
- }
71
+
72
+ if (!hasCrashBlock && processName && pid !== undefined) {
73
+ lines.push(`Process: ${processName} (pid ${pid})`);
74
+ } else if (!hasCrashBlock && processName) {
75
+ lines.push(`Process: ${processName}`);
76
+ } else if (!hasCrashBlock && pid !== undefined) {
77
+ lines.push(`PID: ${pid}`);
78
+ }
79
+
80
+ if (!hasCrashBlock && stackTrace && stackTrace.length > 0) {
81
+ lines.push('');
82
+ lines.push(...stackTrace.map((line) => ` ${line}`));
83
+ }
84
+
85
+ return lines.join('\n');
86
+ };
33
87
 
34
88
  export class NativeCrashError extends HarnessError {
35
89
  constructor(
36
90
  public readonly testFilePath: string,
91
+ public readonly details: NativeCrashDetails,
37
92
  public readonly lastKnownTest?: string
38
93
  ) {
39
- super('The native app crashed during test execution.');
94
+ super(buildNativeCrashMessage(details));
40
95
  this.name = 'NativeCrashError';
96
+ this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
97
+ }
98
+
99
+ get phase() {
100
+ return this.details.phase;
41
101
  }
42
102
  }