@react-native-harness/jest 1.0.0 → 1.1.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crash-supervisor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crash-supervisor.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crash-supervisor.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createCrashSupervisor } from '../crash-supervisor.js';
3
+ const createMockAppMonitor = (options) => {
4
+ const listeners = new Set();
5
+ const noop = async () => undefined;
6
+ const appMonitor = {
7
+ start: noop,
8
+ stop: noop,
9
+ dispose: noop,
10
+ addListener: (listener) => {
11
+ listeners.add(listener);
12
+ },
13
+ removeListener: (listener) => {
14
+ listeners.delete(listener);
15
+ },
16
+ };
17
+ if (options?.getCrashDetails) {
18
+ appMonitor.getCrashDetails = options.getCrashDetails;
19
+ }
20
+ const emit = (event) => {
21
+ for (const listener of listeners) {
22
+ listener(event);
23
+ }
24
+ };
25
+ return {
26
+ appMonitor,
27
+ emit,
28
+ };
29
+ };
30
+ const createPlatformRunner = ({ isAppRunning, getCrashDetails, }) => ({
31
+ startApp: async () => undefined,
32
+ restartApp: async () => undefined,
33
+ stopApp: async () => undefined,
34
+ dispose: async () => undefined,
35
+ isAppRunning,
36
+ createAppMonitor: () => {
37
+ throw new Error('Not used in unit tests');
38
+ },
39
+ getCrashDetails,
40
+ });
41
+ describe('crash-supervisor', () => {
42
+ it('rejects the active test when the app exits during launch', async () => {
43
+ const monitor = createMockAppMonitor();
44
+ const supervisor = createCrashSupervisor({
45
+ appMonitor: monitor.appMonitor,
46
+ platformRunner: createPlatformRunner({
47
+ isAppRunning: vi.fn().mockResolvedValue(false),
48
+ }),
49
+ });
50
+ supervisor.beginLaunch('/tmp/startup.harness.ts');
51
+ const crashPromise = supervisor.waitForCrash('/tmp/startup.harness.ts');
52
+ const expectation = expect(crashPromise).rejects.toMatchObject({
53
+ name: 'NativeCrashError',
54
+ phase: 'startup',
55
+ details: {
56
+ signal: 'SIGSEGV',
57
+ processName: 'com.harnessplayground',
58
+ },
59
+ });
60
+ monitor.emit({
61
+ type: 'app_exited',
62
+ source: 'logs',
63
+ crashDetails: {
64
+ source: 'logs',
65
+ signal: 'SIGSEGV',
66
+ processName: 'com.harnessplayground',
67
+ rawLines: ['Fatal signal 11 (SIGSEGV)'],
68
+ },
69
+ });
70
+ await expectation;
71
+ await supervisor.dispose();
72
+ });
73
+ it('treats polling exit events as immediately confirmed crashes', async () => {
74
+ const monitor = createMockAppMonitor();
75
+ const isAppRunning = vi.fn().mockResolvedValue(true);
76
+ const supervisor = createCrashSupervisor({
77
+ appMonitor: monitor.appMonitor,
78
+ platformRunner: createPlatformRunner({
79
+ isAppRunning,
80
+ }),
81
+ });
82
+ supervisor.beginLaunch('/tmp/polling-exit.harness.ts');
83
+ const crashPromise = supervisor.waitForCrash('/tmp/polling-exit.harness.ts');
84
+ monitor.emit({ type: 'app_exited', source: 'polling' });
85
+ await expect(crashPromise).rejects.toMatchObject({
86
+ name: 'NativeCrashError',
87
+ phase: 'startup',
88
+ });
89
+ expect(isAppRunning).not.toHaveBeenCalled();
90
+ await supervisor.dispose();
91
+ });
92
+ it('does not reject when monitoring is stopped', async () => {
93
+ const monitor = createMockAppMonitor();
94
+ const supervisor = createCrashSupervisor({
95
+ appMonitor: monitor.appMonitor,
96
+ platformRunner: createPlatformRunner({
97
+ isAppRunning: vi.fn().mockResolvedValue(false),
98
+ }),
99
+ });
100
+ supervisor.beginTestRun('/tmp/restart.harness.ts');
101
+ await supervisor.stop();
102
+ const reject = vi.fn();
103
+ void supervisor.waitForCrash('/tmp/restart.harness.ts').catch(reject);
104
+ monitor.emit({ type: 'app_exited', source: 'polling' });
105
+ await Promise.resolve();
106
+ expect(reject).not.toHaveBeenCalled();
107
+ await supervisor.dispose();
108
+ });
109
+ it('prefers crash details from the started app monitor over the runner', async () => {
110
+ const monitorGetCrashDetails = vi.fn().mockResolvedValue({
111
+ source: 'logs',
112
+ summary: 'full crash block',
113
+ rawLines: ['full crash block'],
114
+ });
115
+ const runnerGetCrashDetails = vi.fn().mockResolvedValue({
116
+ source: 'logs',
117
+ summary: 'runner details',
118
+ rawLines: ['runner details'],
119
+ });
120
+ const monitor = createMockAppMonitor({
121
+ getCrashDetails: monitorGetCrashDetails,
122
+ });
123
+ const supervisor = createCrashSupervisor({
124
+ appMonitor: monitor.appMonitor,
125
+ platformRunner: createPlatformRunner({
126
+ isAppRunning: vi.fn().mockResolvedValue(false),
127
+ getCrashDetails: runnerGetCrashDetails,
128
+ }),
129
+ });
130
+ supervisor.beginTestRun('/tmp/monitor-details.harness.ts');
131
+ const crashPromise = supervisor.waitForCrash('/tmp/monitor-details.harness.ts');
132
+ monitor.emit({
133
+ type: 'possible_crash',
134
+ source: 'logs',
135
+ crashDetails: {
136
+ source: 'logs',
137
+ processName: 'com.harnessplayground',
138
+ pid: 1234,
139
+ rawLines: ['partial line'],
140
+ },
141
+ });
142
+ await expect(crashPromise).rejects.toMatchObject({
143
+ details: {
144
+ summary: 'full crash block',
145
+ },
146
+ });
147
+ expect(monitorGetCrashDetails).toHaveBeenCalled();
148
+ expect(runnerGetCrashDetails).not.toHaveBeenCalled();
149
+ await supervisor.dispose();
150
+ });
151
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { NativeCrashError } from '../errors.js';
3
+ describe('NativeCrashError', () => {
4
+ it('formats the extracted stack trace in the error message', () => {
5
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
6
+ phase: 'execution',
7
+ processName: 'HarnessPlayground',
8
+ pid: 1234,
9
+ signal: 'SIGTRAP',
10
+ exceptionType: 'EXC_BREAKPOINT',
11
+ stackTrace: [
12
+ '0 AppDelegate.crashIfRequested() (AppDelegate.swift:31)',
13
+ '1 AppDelegate.application(_:didFinishLaunchingWithOptions:) (AppDelegate.swift:56)',
14
+ ],
15
+ });
16
+ expect(error.message).toContain(' 0 AppDelegate.crashIfRequested() (AppDelegate.swift:31)');
17
+ expect(error.message).toContain(' 1 AppDelegate.application(_:didFinishLaunchingWithOptions:) (AppDelegate.swift:56)');
18
+ });
19
+ it('omits single-line iOS summaries from the rendered error message', () => {
20
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
21
+ phase: 'startup',
22
+ artifactType: 'ios-crash-report',
23
+ summary: '2026-03-12 13:46:18.154 Df HarnessPlayground[18007:65e716] [com.apple.dt.xctest:Default] notify_get_state check indicated test daemon not ready.',
24
+ processName: 'HarnessPlayground',
25
+ pid: 18007,
26
+ signal: 'SIGABRT',
27
+ exceptionType: 'EXC_CRASH',
28
+ });
29
+ expect(error.message).not.toContain('notify_get_state check indicated test daemon not ready');
30
+ expect(error.message).toContain('Signal: SIGABRT');
31
+ expect(error.message).toContain('Exception: EXC_CRASH');
32
+ expect(error.message).toContain('Process: HarnessPlayground (pid 18007)');
33
+ });
34
+ it('does not duplicate stack frames when the summary already contains a crash block', () => {
35
+ const frame = '03-13 07:59:44.943 20373 20373 E AndroidRuntime: \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)';
36
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
37
+ phase: 'startup',
38
+ summary: [
39
+ '--------- beginning of crash',
40
+ '03-13 07:59:44.943 20373 20373 E AndroidRuntime: FATAL EXCEPTION: main',
41
+ frame,
42
+ ].join('\n'),
43
+ stackTrace: [frame],
44
+ });
45
+ expect(error.message.match(/MainActivity\.onCreate\(MainActivity\.kt:38\)/g)).toHaveLength(1);
46
+ });
47
+ it('collapses the native crash stack header so jest does not reprint multiline messages', () => {
48
+ const error = new NativeCrashError('/tmp/crash.harness.ts', {
49
+ phase: 'startup',
50
+ summary: ['line one', 'line two'].join('\n'),
51
+ });
52
+ expect(error.stack).toBe('NativeCrashError: The native app crashed while preparing to run this test file.');
53
+ });
54
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=harness.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/harness.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,106 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { waitForAppReady } from '../harness.js';
4
+ import { createCrashSupervisor } from '../crash-supervisor.js';
5
+ const createBridgeServer = () => {
6
+ const emitter = new EventEmitter();
7
+ return {
8
+ serverBridge: {
9
+ on: emitter.on.bind(emitter),
10
+ once: emitter.once.bind(emitter),
11
+ off: emitter.off.bind(emitter),
12
+ },
13
+ emitReady: () => {
14
+ emitter.emit('ready');
15
+ },
16
+ };
17
+ };
18
+ const createAppMonitor = () => {
19
+ const listeners = new Set();
20
+ return {
21
+ start: async () => undefined,
22
+ stop: async () => undefined,
23
+ dispose: async () => undefined,
24
+ addListener: (listener) => {
25
+ listeners.add(listener);
26
+ },
27
+ removeListener: (listener) => {
28
+ listeners.delete(listener);
29
+ },
30
+ };
31
+ };
32
+ const createPlatformRunner = (restartApp) => ({
33
+ startApp: async () => undefined,
34
+ restartApp,
35
+ stopApp: async () => undefined,
36
+ dispose: async () => undefined,
37
+ isAppRunning: async () => true,
38
+ createAppMonitor: createAppMonitor,
39
+ });
40
+ describe('waitForAppReady', () => {
41
+ it('passes launch options to the initial launch', async () => {
42
+ const { serverBridge, emitReady } = createBridgeServer();
43
+ const restartApp = vi.fn().mockResolvedValue(undefined);
44
+ const platformInstance = createPlatformRunner(restartApp);
45
+ const crashSupervisor = createCrashSupervisor({
46
+ appMonitor: createAppMonitor(),
47
+ platformRunner: platformInstance,
48
+ });
49
+ const promise = waitForAppReady({
50
+ serverBridge,
51
+ platformInstance,
52
+ bridgeTimeout: 5000,
53
+ testFilePath: '/tmp/test.harness.ts',
54
+ crashSupervisor,
55
+ appLaunchOptions: {
56
+ extras: {
57
+ mode: 'startup',
58
+ },
59
+ },
60
+ });
61
+ await Promise.resolve();
62
+ expect(restartApp).toHaveBeenCalledWith({
63
+ extras: {
64
+ mode: 'startup',
65
+ },
66
+ });
67
+ emitReady();
68
+ await promise;
69
+ await crashSupervisor.dispose();
70
+ });
71
+ it('does not retry launch when the app never becomes ready', async () => {
72
+ vi.useFakeTimers();
73
+ const { serverBridge } = createBridgeServer();
74
+ const restartApp = vi.fn().mockResolvedValue(undefined);
75
+ const platformInstance = createPlatformRunner(restartApp);
76
+ const crashSupervisor = createCrashSupervisor({
77
+ appMonitor: createAppMonitor(),
78
+ platformRunner: platformInstance,
79
+ });
80
+ const promise = waitForAppReady({
81
+ serverBridge,
82
+ platformInstance,
83
+ bridgeTimeout: 1000,
84
+ testFilePath: '/tmp/test.harness.ts',
85
+ crashSupervisor,
86
+ appLaunchOptions: {
87
+ extras: {
88
+ mode: 'startup',
89
+ },
90
+ },
91
+ });
92
+ await Promise.resolve();
93
+ await vi.advanceTimersByTimeAsync(1000);
94
+ await expect(promise).rejects.toMatchObject({
95
+ name: 'AbortError',
96
+ });
97
+ expect(restartApp).toHaveBeenCalledTimes(1);
98
+ expect(restartApp).toHaveBeenCalledWith({
99
+ extras: {
100
+ mode: 'startup',
101
+ },
102
+ });
103
+ await crashSupervisor.dispose();
104
+ vi.useRealTimers();
105
+ });
106
+ });
@@ -0,0 +1,21 @@
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
@@ -0,0 +1 @@
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;AAQzC,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,eA6O3B,CAAC"}
@@ -0,0 +1,209 @@
1
+ import { NativeCrashError, } from './errors.js';
2
+ import { logger } from '@react-native-harness/tools';
3
+ const getCrashPhase = (state) => state === 'running' ? 'execution' : 'startup';
4
+ const mergeCrashDetails = (phase, initialDetails, enrichedDetails, fallbackSummary) => ({
5
+ phase,
6
+ source: enrichedDetails?.source ?? initialDetails?.source,
7
+ summary: enrichedDetails?.summary ?? initialDetails?.summary ?? fallbackSummary,
8
+ signal: enrichedDetails?.signal ?? initialDetails?.signal,
9
+ exceptionType: enrichedDetails?.exceptionType ?? initialDetails?.exceptionType,
10
+ processName: enrichedDetails?.processName ?? initialDetails?.processName,
11
+ pid: enrichedDetails?.pid ?? initialDetails?.pid,
12
+ stackTrace: enrichedDetails?.stackTrace ?? initialDetails?.stackTrace,
13
+ rawLines: enrichedDetails?.rawLines ?? initialDetails?.rawLines,
14
+ artifactType: enrichedDetails?.artifactType ?? initialDetails?.artifactType,
15
+ artifactPath: enrichedDetails?.artifactPath ?? initialDetails?.artifactPath,
16
+ });
17
+ export const createCrashSupervisor = ({ appMonitor, platformRunner, }) => {
18
+ let state = 'idle';
19
+ let activeTestFilePath = null;
20
+ let crashRejectors = new Set();
21
+ let disposed = false;
22
+ let monitoring = true;
23
+ let isResolvingCrash = false;
24
+ const getCrashDetailsProvider = () => {
25
+ if ('getCrashDetails' in appMonitor) {
26
+ return appMonitor;
27
+ }
28
+ if (platformRunner.getCrashDetails) {
29
+ return platformRunner;
30
+ }
31
+ return null;
32
+ };
33
+ const rejectCrashWaiters = (testFilePath, details) => {
34
+ const error = new NativeCrashError(testFilePath, details);
35
+ for (const reject of crashRejectors) {
36
+ reject(error);
37
+ }
38
+ crashRejectors = new Set();
39
+ };
40
+ const handleCrash = async (reason, details) => {
41
+ if (isResolvingCrash ||
42
+ (state !== 'launching' && state !== 'running' && state !== 'ready')) {
43
+ return;
44
+ }
45
+ if (!activeTestFilePath) {
46
+ logger.debug(`Ignoring crash signal without active test: ${reason}`);
47
+ return;
48
+ }
49
+ isResolvingCrash = true;
50
+ logger.debug(`Native crash detected during ${activeTestFilePath} (state: ${state}, reason: ${reason || '(none)'})`);
51
+ for (const line of details?.rawLines ?? []) {
52
+ logger.debug(line);
53
+ }
54
+ const phase = getCrashPhase(state);
55
+ const testFilePath = activeTestFilePath;
56
+ state = 'idle';
57
+ try {
58
+ const enrichedDetails = await getCrashDetailsProvider()?.getCrashDetails?.({
59
+ processName: details?.processName,
60
+ pid: details?.pid,
61
+ occurredAt: Date.now(),
62
+ });
63
+ const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
64
+ logger.debug('Crash details:', {
65
+ phase: mergedDetails.phase,
66
+ source: mergedDetails.source,
67
+ summary: mergedDetails.summary,
68
+ signal: mergedDetails.signal,
69
+ exceptionType: mergedDetails.exceptionType,
70
+ processName: mergedDetails.processName,
71
+ pid: mergedDetails.pid,
72
+ });
73
+ rejectCrashWaiters(testFilePath, mergedDetails);
74
+ }
75
+ finally {
76
+ isResolvingCrash = false;
77
+ }
78
+ };
79
+ const confirmCrash = async (reason, details) => {
80
+ if (disposed || !monitoring || state === 'disposing') {
81
+ return;
82
+ }
83
+ try {
84
+ const isRunning = await platformRunner.isAppRunning();
85
+ if (!isRunning) {
86
+ handleCrash(reason, details);
87
+ }
88
+ }
89
+ catch (error) {
90
+ logger.debug('Crash confirmation failed', error);
91
+ }
92
+ };
93
+ const appMonitorListener = (event) => {
94
+ if (disposed || !monitoring) {
95
+ return;
96
+ }
97
+ if (event.type === 'app_started') {
98
+ return;
99
+ }
100
+ if (event.type === 'app_exited') {
101
+ const details = {
102
+ source: event.crashDetails?.source ?? event.source,
103
+ summary: event.crashDetails?.summary,
104
+ signal: event.crashDetails?.signal,
105
+ exceptionType: event.crashDetails?.exceptionType,
106
+ processName: event.crashDetails?.processName,
107
+ pid: event.crashDetails?.pid ?? event.pid,
108
+ stackTrace: event.crashDetails?.stackTrace,
109
+ rawLines: event.crashDetails?.rawLines ??
110
+ (event.line ? [event.line] : undefined),
111
+ };
112
+ if (event.isConfirmed ?? event.source === 'polling') {
113
+ void handleCrash('', details);
114
+ }
115
+ else {
116
+ void confirmCrash('', details);
117
+ }
118
+ return;
119
+ }
120
+ if (event.type === 'possible_crash') {
121
+ const details = {
122
+ source: event.crashDetails?.source ?? event.source,
123
+ summary: event.crashDetails?.summary,
124
+ signal: event.crashDetails?.signal,
125
+ exceptionType: event.crashDetails?.exceptionType,
126
+ processName: event.crashDetails?.processName,
127
+ pid: event.crashDetails?.pid ?? event.pid,
128
+ stackTrace: event.crashDetails?.stackTrace,
129
+ rawLines: event.crashDetails?.rawLines ??
130
+ (event.line ? [event.line] : undefined),
131
+ };
132
+ if (event.isConfirmed) {
133
+ void handleCrash(`possible crash signal (${event.source ?? 'unknown'})`, details);
134
+ }
135
+ else {
136
+ void confirmCrash(`possible crash signal (${event.source ?? 'unknown'})`, details);
137
+ }
138
+ }
139
+ };
140
+ appMonitor.addListener(appMonitorListener);
141
+ const setActiveTestFile = (testFilePath) => {
142
+ activeTestFilePath = testFilePath;
143
+ };
144
+ const beginLaunch = (testFilePath) => {
145
+ activeTestFilePath = testFilePath;
146
+ state = 'launching';
147
+ };
148
+ const markReady = () => {
149
+ if (state !== 'disposing') {
150
+ state = 'ready';
151
+ }
152
+ };
153
+ const beginTestRun = (testFilePath) => {
154
+ activeTestFilePath = testFilePath;
155
+ state = 'running';
156
+ };
157
+ const stop = async () => {
158
+ monitoring = false;
159
+ await appMonitor.stop();
160
+ };
161
+ const start = async () => {
162
+ monitoring = true;
163
+ await appMonitor.start();
164
+ };
165
+ const waitForCrash = (testFilePath) => new Promise((_, reject) => {
166
+ if (disposed) {
167
+ reject(new NativeCrashError(testFilePath, {
168
+ phase: 'startup',
169
+ source: 'polling',
170
+ summary: 'Crash supervisor disposed while waiting for app startup.',
171
+ }));
172
+ return;
173
+ }
174
+ crashRejectors.add(reject);
175
+ });
176
+ const isReady = () => state === 'ready' || state === 'running';
177
+ const cancelCrashWaiters = () => {
178
+ crashRejectors = new Set();
179
+ };
180
+ const reset = () => {
181
+ cancelCrashWaiters();
182
+ isResolvingCrash = false;
183
+ if (state !== 'disposing') {
184
+ state = 'idle';
185
+ }
186
+ };
187
+ const dispose = async () => {
188
+ disposed = true;
189
+ monitoring = false;
190
+ state = 'disposing';
191
+ crashRejectors = new Set();
192
+ isResolvingCrash = false;
193
+ appMonitor.removeListener(appMonitorListener);
194
+ await appMonitor.dispose();
195
+ };
196
+ return {
197
+ setActiveTestFile,
198
+ beginLaunch,
199
+ markReady,
200
+ beginTestRun,
201
+ stop,
202
+ start,
203
+ waitForCrash,
204
+ isReady,
205
+ cancelCrashWaiters,
206
+ reset,
207
+ dispose,
208
+ };
209
+ };
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HarnessError } from '@react-native-harness/tools';
2
+ import type { AppCrashDetails } from '@react-native-harness/platforms';
2
3
  export declare class NoRunnerSpecifiedError extends HarnessError {
3
4
  constructor();
4
5
  }
@@ -9,12 +10,15 @@ export declare class RunnerNotFoundError extends HarnessError {
9
10
  export declare class InitializationTimeoutError extends HarnessError {
10
11
  constructor();
11
12
  }
12
- export declare class MaxAppRestartsError extends HarnessError {
13
- constructor(attempts: number);
14
- }
13
+ export type NativeCrashPhase = 'startup' | 'execution';
14
+ export type NativeCrashDetails = AppCrashDetails & {
15
+ phase: NativeCrashPhase;
16
+ };
15
17
  export declare class NativeCrashError extends HarnessError {
16
18
  readonly testFilePath: string;
19
+ readonly details: NativeCrashDetails;
17
20
  readonly lastKnownTest?: string | undefined;
18
- constructor(testFilePath: string, lastKnownTest?: string | undefined);
21
+ constructor(testFilePath: string, details: NativeCrashDetails, lastKnownTest?: string | undefined);
22
+ get phase(): NativeCrashPhase;
19
23
  }
20
24
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,mBAAoB,SAAQ,YAAY;gBACvC,QAAQ,EAAE,MAAM;CAO7B;AAED,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,aAAa,CAAC,EAAE,MAAM;gBADtB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,YAAA;CAKzC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAsDF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF"}
package/dist/errors.js CHANGED
@@ -19,20 +19,54 @@ export class InitializationTimeoutError extends HarnessError {
19
19
  this.name = 'InitializationTimeoutError';
20
20
  }
21
21
  }
22
- export class MaxAppRestartsError extends HarnessError {
23
- constructor(attempts) {
24
- super(`App failed to start after ${attempts} attempts. ` +
25
- `No bundling activity detected within timeout period.`);
26
- this.name = 'MaxAppRestartsError';
22
+ const buildNativeCrashMessage = ({ phase, summary, signal, exceptionType, processName, pid, stackTrace, artifactType, }) => {
23
+ const lines = [
24
+ phase === 'startup'
25
+ ? 'The native app crashed while preparing to run this test file.'
26
+ : 'The native app crashed during test execution.',
27
+ ];
28
+ const hasCrashBlock = summary?.includes('\n') ?? false;
29
+ const shouldRenderSummary = Boolean(summary) &&
30
+ !(!hasCrashBlock &&
31
+ artifactType === 'ios-crash-report');
32
+ if (shouldRenderSummary && summary) {
33
+ lines.push('');
34
+ lines.push(summary);
27
35
  }
28
- }
36
+ if (!hasCrashBlock && signal) {
37
+ lines.push(`Signal: ${signal}`);
38
+ }
39
+ if (!hasCrashBlock && exceptionType) {
40
+ lines.push(`Exception: ${exceptionType}`);
41
+ }
42
+ if (!hasCrashBlock && processName && pid !== undefined) {
43
+ lines.push(`Process: ${processName} (pid ${pid})`);
44
+ }
45
+ else if (!hasCrashBlock && processName) {
46
+ lines.push(`Process: ${processName}`);
47
+ }
48
+ else if (!hasCrashBlock && pid !== undefined) {
49
+ lines.push(`PID: ${pid}`);
50
+ }
51
+ if (!hasCrashBlock && stackTrace && stackTrace.length > 0) {
52
+ lines.push('');
53
+ lines.push(...stackTrace.map((line) => ` ${line}`));
54
+ }
55
+ return lines.join('\n');
56
+ };
29
57
  export class NativeCrashError extends HarnessError {
30
58
  testFilePath;
59
+ details;
31
60
  lastKnownTest;
32
- constructor(testFilePath, lastKnownTest) {
33
- super('The native app crashed during test execution.');
61
+ constructor(testFilePath, details, lastKnownTest) {
62
+ super(buildNativeCrashMessage(details));
34
63
  this.testFilePath = testFilePath;
64
+ this.details = details;
35
65
  this.lastKnownTest = lastKnownTest;
36
66
  this.name = 'NativeCrashError';
67
+ this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
68
+ }
69
+ get phase() {
70
+ return this.details.phase;
37
71
  }
38
72
  }