@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.
- package/dist/__tests__/bridge.test.js +23 -2
- package/dist/__tests__/crash-monitor.test.js +146 -192
- package/dist/__tests__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +190 -26
- package/dist/__tests__/harness-session.test.js +63 -239
- package/dist/__tests__/index.test.js +59 -10
- package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
- package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
- package/dist/__tests__/test-file-platform-filter.test.js +47 -0
- package/dist/crash-monitor.d.ts +5 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +116 -102
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -2
- package/dist/execute-run.d.ts +4 -2
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +85 -7
- package/dist/harness-session.d.ts +21 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +134 -39
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/test-file-platform-filter.d.ts +5 -0
- package/dist/test-file-platform-filter.d.ts.map +1 -0
- package/dist/test-file-platform-filter.js +40 -0
- package/dist/toTestResult.d.ts +1 -0
- package/dist/toTestResult.d.ts.map +1 -1
- package/dist/toTestResult.js +2 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/__tests__/bridge.test.ts +37 -2
- package/src/__tests__/crash-monitor.test.ts +182 -231
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +235 -34
- package/src/__tests__/harness-session.test.ts +93 -0
- package/src/__tests__/index.test.ts +89 -11
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +192 -131
- package/src/errors.ts +62 -1
- package/src/execute-run.ts +125 -11
- package/src/harness-session.ts +177 -42
- package/src/index.ts +40 -11
- package/src/run.ts +1 -0
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/jest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"jest-util": "^30.2.0",
|
|
40
40
|
"tslib": "^2.3.0",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"@react-native-harness/
|
|
43
|
-
"@react-native-harness/
|
|
44
|
-
"@react-native-harness/
|
|
45
|
-
"@react-native-harness/
|
|
46
|
-
"@react-native-harness/
|
|
47
|
-
"@react-native-harness/
|
|
42
|
+
"@react-native-harness/plugins": "1.4.0-rc.1",
|
|
43
|
+
"@react-native-harness/platforms": "1.4.0-rc.1",
|
|
44
|
+
"@react-native-harness/bridge": "1.4.0-rc.1",
|
|
45
|
+
"@react-native-harness/config": "1.4.0-rc.1",
|
|
46
|
+
"@react-native-harness/tools": "1.4.0-rc.1",
|
|
47
|
+
"@react-native-harness/bundler-metro": "1.4.0-rc.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/yargs": "^17.0.32",
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration test pairing createHarnessBridge (CLI side) with
|
|
3
3
|
* connectToHarness (app side). Tests the full connection lifecycle and
|
|
4
|
-
* RPC round-trip without
|
|
4
|
+
* RPC round-trip without coupling tests to the transport internals.
|
|
5
5
|
*/
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import type { HarnessBridge } from '@react-native-harness/bridge/server';
|
|
8
8
|
import { createHarnessBridge } from '@react-native-harness/bridge/server';
|
|
9
9
|
import { connectToHarness } from '@react-native-harness/bridge/client';
|
|
10
10
|
import type { HarnessContext } from '@react-native-harness/bridge';
|
|
11
|
+
import type { TestSuiteResult } from '@react-native-harness/bridge';
|
|
11
12
|
|
|
12
13
|
const makeContext = (): HarnessContext => ({
|
|
13
14
|
platform: {
|
|
@@ -151,6 +152,38 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
151
152
|
expect(result.tests[0].name).toBe('passes');
|
|
152
153
|
handle.disconnect();
|
|
153
154
|
});
|
|
155
|
+
|
|
156
|
+
it('rejects pending app calls when a newer client replaces the session', async () => {
|
|
157
|
+
const firstHandle = await connect({
|
|
158
|
+
runTests: async (): Promise<TestSuiteResult> =>
|
|
159
|
+
await new Promise<TestSuiteResult>(() => undefined),
|
|
160
|
+
});
|
|
161
|
+
firstHandle.reportReady(device);
|
|
162
|
+
|
|
163
|
+
const firstConnection = await bridge.nextConnection();
|
|
164
|
+
const pendingRun = firstConnection.runTests('example.ts', {
|
|
165
|
+
runner: '/runner.js',
|
|
166
|
+
});
|
|
167
|
+
const pendingRunAssertion = expect(pendingRun).rejects.toThrow(
|
|
168
|
+
'The app bridge was replaced by a newer app connection.',
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const secondHandle = await connect();
|
|
172
|
+
secondHandle.reportReady({
|
|
173
|
+
...device,
|
|
174
|
+
model: 'iPhone 17 Pro Replacement',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await pendingRunAssertion;
|
|
178
|
+
|
|
179
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
180
|
+
|
|
181
|
+
expect(bridge.connection?.device.model).toBe(
|
|
182
|
+
'iPhone 17 Pro Replacement',
|
|
183
|
+
);
|
|
184
|
+
secondHandle.disconnect();
|
|
185
|
+
firstHandle.disconnect();
|
|
186
|
+
});
|
|
154
187
|
});
|
|
155
188
|
|
|
156
189
|
describe('bridge events', () => {
|
|
@@ -178,7 +211,9 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
178
211
|
const pending = bridge.nextConnection();
|
|
179
212
|
bridge.dispose();
|
|
180
213
|
|
|
181
|
-
await expect(pending).rejects.toThrow(
|
|
214
|
+
await expect(pending).rejects.toThrow(
|
|
215
|
+
'The app bridge was disposed before the test file finished running.',
|
|
216
|
+
);
|
|
182
217
|
});
|
|
183
218
|
});
|
|
184
219
|
});
|
|
@@ -1,288 +1,239 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
AppSession,
|
|
5
|
+
AppSessionEvent,
|
|
6
|
+
AppSessionListener,
|
|
7
|
+
AppSessionLog,
|
|
8
|
+
AppSessionState,
|
|
8
9
|
} from '@react-native-harness/platforms';
|
|
9
10
|
import {
|
|
10
11
|
createCrashMonitor,
|
|
11
12
|
CrashWatchCancelledError,
|
|
12
13
|
} from '../crash-monitor.js';
|
|
13
|
-
import { NativeCrashError } from '../errors.js';
|
|
14
|
+
import { NativeCrashError, RuntimeDisconnectError } from '../errors.js';
|
|
14
15
|
|
|
15
16
|
const noop = () => undefined;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
dispose: vi.fn(
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
const waitForClassification = () =>
|
|
18
|
+
new Promise((resolve) => setTimeout(resolve, 1600));
|
|
19
|
+
|
|
20
|
+
const createAppSessionMock = (
|
|
21
|
+
initialState: AppSessionState = { status: 'running' },
|
|
22
|
+
logs: AppSessionLog[] = [],
|
|
23
|
+
getCrashDetails?: AppSession['getCrashDetails']
|
|
24
|
+
) => {
|
|
25
|
+
let state = initialState;
|
|
26
|
+
let listener: AppSessionListener | null = null;
|
|
27
|
+
|
|
28
|
+
const session: AppSession = {
|
|
29
|
+
dispose: vi.fn(async () => undefined),
|
|
30
|
+
getState: vi.fn(async () => state),
|
|
31
|
+
getLogs: vi.fn(() => logs),
|
|
32
|
+
addListener: vi.fn((l: AppSessionListener) => {
|
|
33
|
+
listener = l;
|
|
31
34
|
}),
|
|
32
|
-
removeListener: vi.fn((l:
|
|
33
|
-
if (
|
|
35
|
+
removeListener: vi.fn((l: AppSessionListener) => {
|
|
36
|
+
if (listener === l) listener = null;
|
|
34
37
|
}),
|
|
35
38
|
};
|
|
36
39
|
|
|
40
|
+
if (getCrashDetails) {
|
|
41
|
+
session.getCrashDetails = getCrashDetails;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
session,
|
|
46
|
+
setState: (nextState: AppSessionState) => {
|
|
47
|
+
state = nextState;
|
|
48
|
+
},
|
|
49
|
+
emit: (event: AppSessionEvent) => listener?.(event),
|
|
40
50
|
};
|
|
41
51
|
};
|
|
42
52
|
|
|
43
|
-
const createPlatformRunnerMock = (
|
|
44
|
-
isRunning = false,
|
|
45
|
-
crashDetails: AppCrashDetails | null = null,
|
|
46
|
-
) =>
|
|
47
|
-
({
|
|
48
|
-
isAppRunning: vi.fn(async () => isRunning),
|
|
49
|
-
getCrashDetails: vi.fn(async () => crashDetails),
|
|
50
|
-
startApp: vi.fn(resolveUndefined),
|
|
51
|
-
restartApp: vi.fn(resolveUndefined),
|
|
52
|
-
stopApp: vi.fn(resolveUndefined),
|
|
53
|
-
dispose: vi.fn(resolveUndefined),
|
|
54
|
-
createAppMonitor: vi.fn(),
|
|
55
|
-
}) as unknown as HarnessPlatformRunner;
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Tests
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
53
|
describe('createCrashMonitor', () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
66
|
-
|
|
67
|
-
expect(cm.isAlive()).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('becomes alive when the app starts', () => {
|
|
71
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
72
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
73
|
-
|
|
74
|
-
emit({ type: 'app_started' });
|
|
75
|
-
|
|
76
|
-
expect(cm.isAlive()).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('becomes not alive after a confirmed crash', async () => {
|
|
80
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
81
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
54
|
+
it('starts not alive and becomes alive when an app session is attached', () => {
|
|
55
|
+
const cm = createCrashMonitor();
|
|
56
|
+
const { session } = createAppSessionMock();
|
|
82
57
|
|
|
83
|
-
|
|
84
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
85
|
-
watch.promise.catch(noop);
|
|
58
|
+
expect(cm.isAlive()).toBe(false);
|
|
86
59
|
|
|
87
|
-
|
|
88
|
-
await watch.promise.catch(noop);
|
|
60
|
+
cm.setAppSession(session);
|
|
89
61
|
|
|
90
|
-
|
|
91
|
-
});
|
|
62
|
+
expect(cm.isAlive()).toBe(true);
|
|
92
63
|
});
|
|
93
64
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const watch = cm.watch('/test/example.ts', 'execution');
|
|
100
|
-
watch.promise.catch(noop);
|
|
101
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
102
|
-
|
|
103
|
-
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('attributes the crash to the file and phase passed to watch()', async () => {
|
|
107
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
108
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
109
|
-
|
|
110
|
-
const watch = cm.watch('/test/example.ts', 'startup');
|
|
111
|
-
watch.promise.catch(noop);
|
|
112
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
113
|
-
|
|
114
|
-
const error = await watch.promise.catch((e: NativeCrashError) => e);
|
|
115
|
-
expect(error.testFilePath).toBe('/test/example.ts');
|
|
116
|
-
expect(error.details.phase).toBe('startup');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
|
|
120
|
-
const { monitor } = createAppMonitorMock();
|
|
121
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
122
|
-
|
|
123
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
124
|
-
watch.cancel();
|
|
125
|
-
|
|
126
|
-
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
65
|
+
it('rejects a watch with NativeCrashError when the app session exits', async () => {
|
|
66
|
+
const { session, emit } = createAppSessionMock({
|
|
67
|
+
status: 'exited',
|
|
68
|
+
occurredAt: Date.now(),
|
|
69
|
+
reason: 'observed-exit',
|
|
127
70
|
});
|
|
71
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
72
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
73
|
+
watch.promise.catch(noop);
|
|
128
74
|
|
|
129
|
-
|
|
130
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
131
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
132
|
-
|
|
133
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
134
|
-
watch.promise.catch(noop);
|
|
135
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
75
|
+
emit({ type: 'app_exited' });
|
|
136
76
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
77
|
+
const error = await watch.promise.catch((err: NativeCrashError) => err);
|
|
78
|
+
expect(error).toBeInstanceOf(NativeCrashError);
|
|
79
|
+
expect(error.testFilePath).toBe('/test/example.ts');
|
|
80
|
+
expect(error.details.phase).toBe('execution');
|
|
81
|
+
expect(cm.isAlive()).toBe(false);
|
|
141
82
|
});
|
|
142
83
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
150
|
-
watch.promise.catch(noop);
|
|
151
|
-
emit({ type: 'app_exited', isConfirmed: false });
|
|
84
|
+
it('classifies bridge disconnect plus exited app as NativeCrashError', async () => {
|
|
85
|
+
const { session, setState } = createAppSessionMock();
|
|
86
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
87
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
88
|
+
watch.promise.catch(noop);
|
|
152
89
|
|
|
153
|
-
|
|
90
|
+
cm.handleBridgeDisconnect();
|
|
91
|
+
setState({
|
|
92
|
+
status: 'exited',
|
|
93
|
+
occurredAt: Date.now(),
|
|
94
|
+
reason: 'process-gone',
|
|
154
95
|
});
|
|
155
96
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const runner = createPlatformRunnerMock(true /* still running */);
|
|
159
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
|
|
160
|
-
|
|
161
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
162
|
-
const settled = vi.fn();
|
|
163
|
-
watch.promise.then(settled, settled);
|
|
164
|
-
|
|
165
|
-
emit({ type: 'app_exited', isConfirmed: false });
|
|
166
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
167
|
-
|
|
168
|
-
expect(settled).not.toHaveBeenCalled();
|
|
169
|
-
watch.cancel();
|
|
170
|
-
});
|
|
97
|
+
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
98
|
+
});
|
|
171
99
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
100
|
+
it('classifies bridge disconnect plus running app as RuntimeDisconnectError', async () => {
|
|
101
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
102
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
103
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
104
|
+
watch.promise.catch(noop);
|
|
175
105
|
|
|
176
|
-
|
|
177
|
-
watch.promise.catch(noop);
|
|
178
|
-
emit({ type: 'possible_crash', isConfirmed: true });
|
|
106
|
+
cm.handleBridgeDisconnect();
|
|
179
107
|
|
|
180
|
-
|
|
181
|
-
});
|
|
108
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
182
109
|
});
|
|
183
110
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
111
|
+
it('attaches matching session log evidence to native crash details', async () => {
|
|
112
|
+
const occurredAt = Date.now();
|
|
113
|
+
const logs = [
|
|
114
|
+
{ line: 'ordinary app log', occurredAt },
|
|
115
|
+
{ line: 'MyApp[123] fatal error: boom', occurredAt },
|
|
116
|
+
];
|
|
117
|
+
const { session, setState } = createAppSessionMock(
|
|
118
|
+
{ status: 'running' },
|
|
119
|
+
logs
|
|
120
|
+
);
|
|
121
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
122
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
123
|
+
watch.promise.catch(noop);
|
|
124
|
+
|
|
125
|
+
cm.handleBridgeDisconnect();
|
|
126
|
+
setState({ status: 'exited', occurredAt, reason: 'process-gone' });
|
|
127
|
+
|
|
128
|
+
const error = await watch.promise.catch((err: NativeCrashError) => err);
|
|
129
|
+
expect(error).toBeInstanceOf(NativeCrashError);
|
|
130
|
+
expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
|
|
203
131
|
});
|
|
204
132
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
133
|
+
it('asks the app session to extract native crash details for the current test', async () => {
|
|
134
|
+
const occurredAt = Date.now();
|
|
135
|
+
const getCrashDetails = vi.fn(async () => ({
|
|
136
|
+
artifactType: 'logcat' as const,
|
|
137
|
+
artifactPath: '/tmp/.harness/crash-reports/crash-logcat.txt',
|
|
138
|
+
processName: 'com.harnessplayground',
|
|
139
|
+
pid: 7777,
|
|
140
|
+
}));
|
|
141
|
+
const { session, setState } = createAppSessionMock(
|
|
142
|
+
{ status: 'running' },
|
|
143
|
+
[],
|
|
144
|
+
getCrashDetails
|
|
145
|
+
);
|
|
146
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
147
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
148
|
+
watch.promise.catch(noop);
|
|
149
|
+
|
|
150
|
+
cm.handleBridgeDisconnect();
|
|
151
|
+
setState({
|
|
152
|
+
status: 'exited',
|
|
153
|
+
occurredAt,
|
|
154
|
+
pid: 7777,
|
|
155
|
+
reason: 'process-gone',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const error = await watch.promise.catch((err: NativeCrashError) => err);
|
|
159
|
+
|
|
160
|
+
expect(getCrashDetails).toHaveBeenCalledWith({
|
|
161
|
+
occurredAt: expect.any(Number),
|
|
162
|
+
pid: 7777,
|
|
163
|
+
processName: undefined,
|
|
164
|
+
testFilePath: '/test/example.ts',
|
|
165
|
+
});
|
|
166
|
+
expect(error.details.artifactPath).toBe(
|
|
167
|
+
'/tmp/.harness/crash-reports/crash-logcat.txt'
|
|
168
|
+
);
|
|
169
|
+
expect(error.message).toContain(
|
|
170
|
+
`Harness extracted the crash log: ${path.relative(
|
|
171
|
+
process.cwd(),
|
|
172
|
+
'/tmp/.harness/crash-reports/crash-logcat.txt'
|
|
173
|
+
)}`
|
|
174
|
+
);
|
|
175
|
+
});
|
|
218
176
|
|
|
219
|
-
|
|
220
|
-
|
|
177
|
+
it('falls back to log details when native crash extraction fails', async () => {
|
|
178
|
+
const occurredAt = Date.now();
|
|
179
|
+
const getCrashDetails = vi.fn(async () => {
|
|
180
|
+
throw new Error('copy failed');
|
|
221
181
|
});
|
|
182
|
+
const { session, setState } = createAppSessionMock(
|
|
183
|
+
{ status: 'running' },
|
|
184
|
+
[{ line: 'MyApp[123] fatal error: boom', occurredAt }],
|
|
185
|
+
getCrashDetails
|
|
186
|
+
);
|
|
187
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
188
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
189
|
+
watch.promise.catch(noop);
|
|
222
190
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
226
|
-
|
|
227
|
-
await cm.stop();
|
|
228
|
-
await cm.start();
|
|
191
|
+
cm.handleBridgeDisconnect();
|
|
192
|
+
setState({ status: 'exited', occurredAt, reason: 'process-gone' });
|
|
229
193
|
|
|
230
|
-
|
|
231
|
-
watch.promise.catch(noop);
|
|
232
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
194
|
+
const error = await watch.promise.catch((err: NativeCrashError) => err);
|
|
233
195
|
|
|
234
|
-
|
|
235
|
-
|
|
196
|
+
expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
|
|
197
|
+
expect(error.message).toContain("Harness couldn't extract the crash log.");
|
|
236
198
|
});
|
|
237
199
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
200
|
+
it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
|
|
201
|
+
const cm = createCrashMonitor();
|
|
202
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
242
203
|
|
|
243
|
-
|
|
244
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
245
|
-
watch.promise.catch(noop);
|
|
204
|
+
watch.cancel();
|
|
246
205
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// The watcher was cleared; a crash fired now should not reach the old watch.
|
|
251
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
252
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
253
|
-
|
|
254
|
-
// Old promise is still pending (we can verify by cancel resolving it).
|
|
255
|
-
watch.cancel();
|
|
256
|
-
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
257
|
-
});
|
|
206
|
+
await expect(watch.promise).rejects.toBeInstanceOf(
|
|
207
|
+
CrashWatchCancelledError
|
|
208
|
+
);
|
|
258
209
|
});
|
|
259
210
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
211
|
+
it('ignores session events while stopped', async () => {
|
|
212
|
+
const { session, emit } = createAppSessionMock({
|
|
213
|
+
status: 'exited',
|
|
214
|
+
occurredAt: Date.now(),
|
|
215
|
+
});
|
|
216
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
217
|
+
await cm.stop();
|
|
267
218
|
|
|
268
|
-
|
|
219
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
220
|
+
const settled = vi.fn();
|
|
221
|
+
watch.promise.then(settled, settled);
|
|
269
222
|
|
|
270
|
-
|
|
271
|
-
|
|
223
|
+
emit({ type: 'app_exited' });
|
|
224
|
+
await waitForClassification();
|
|
272
225
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
277
|
-
});
|
|
226
|
+
expect(settled).not.toHaveBeenCalled();
|
|
227
|
+
watch.cancel();
|
|
228
|
+
});
|
|
278
229
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
230
|
+
it('removes the app session listener on dispose', async () => {
|
|
231
|
+
const { session } = createAppSessionMock();
|
|
232
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
282
233
|
|
|
283
|
-
|
|
234
|
+
await cm.dispose();
|
|
284
235
|
|
|
285
|
-
|
|
286
|
-
|
|
236
|
+
expect(session.removeListener).toHaveBeenCalledOnce();
|
|
237
|
+
expect(cm.isAlive()).toBe(false);
|
|
287
238
|
});
|
|
288
239
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AppBridgeDisconnectedError } from '@react-native-harness/bridge/server';
|
|
2
4
|
import { NativeCrashError, PlatformReadyTimeoutError } from '../errors.js';
|
|
3
5
|
|
|
4
6
|
describe('PlatformReadyTimeoutError', () => {
|
|
@@ -10,6 +12,62 @@ describe('PlatformReadyTimeoutError', () => {
|
|
|
10
12
|
});
|
|
11
13
|
|
|
12
14
|
describe('NativeCrashError', () => {
|
|
15
|
+
it('reports the extracted crash log path when available', () => {
|
|
16
|
+
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
17
|
+
phase: 'execution',
|
|
18
|
+
artifactPath: path.join(
|
|
19
|
+
process.cwd(),
|
|
20
|
+
'.harness',
|
|
21
|
+
'crash-reports',
|
|
22
|
+
'crash.ips'
|
|
23
|
+
),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(error.message).toContain(
|
|
27
|
+
`Harness extracted the crash log: ${path.join(
|
|
28
|
+
'.harness',
|
|
29
|
+
'crash-reports',
|
|
30
|
+
'crash.ips'
|
|
31
|
+
)}`
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('lists enrichment artifact paths when available', () => {
|
|
36
|
+
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
37
|
+
phase: 'execution',
|
|
38
|
+
artifactPath: path.join(
|
|
39
|
+
process.cwd(),
|
|
40
|
+
'.harness',
|
|
41
|
+
'crash-reports',
|
|
42
|
+
'logcat.txt'
|
|
43
|
+
),
|
|
44
|
+
enrichmentArtifacts: [
|
|
45
|
+
{
|
|
46
|
+
artifactType: 'dropbox-native-crash',
|
|
47
|
+
artifactPath: path.join(
|
|
48
|
+
process.cwd(),
|
|
49
|
+
'.harness',
|
|
50
|
+
'crash-reports',
|
|
51
|
+
'dropbox-native-crash.txt'
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(error.message).toContain('Additional crash artifacts:');
|
|
58
|
+
expect(error.message).toContain(
|
|
59
|
+
path.join('.harness', 'crash-reports', 'dropbox-native-crash.txt')
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reports crash log extraction failure when no artifact was pulled', () => {
|
|
64
|
+
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
65
|
+
phase: 'execution',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(error.message).toContain("Harness couldn't extract the crash log.");
|
|
69
|
+
});
|
|
70
|
+
|
|
13
71
|
it('formats the extracted stack trace in the error message', () => {
|
|
14
72
|
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
15
73
|
phase: 'execution',
|
|
@@ -80,3 +138,20 @@ describe('NativeCrashError', () => {
|
|
|
80
138
|
);
|
|
81
139
|
});
|
|
82
140
|
});
|
|
141
|
+
|
|
142
|
+
describe('AppBridgeDisconnectedError', () => {
|
|
143
|
+
it('explains likely causes without exposing an internal stack', () => {
|
|
144
|
+
const error = new AppBridgeDisconnectedError('app-disconnected');
|
|
145
|
+
|
|
146
|
+
expect(error.message).toBe(
|
|
147
|
+
'The app bridge disconnected during test execution. This can happen if the app was killed, crashed, reloaded, or restarted while the test file was running.'
|
|
148
|
+
);
|
|
149
|
+
expect(error.stack).toBe(`AppBridgeDisconnectedError: ${error.message}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('uses a reconnection-specific message when a newer app connection replaces the old one', () => {
|
|
153
|
+
expect(new AppBridgeDisconnectedError('app-replaced').message).toBe(
|
|
154
|
+
'The app bridge was replaced by a newer app connection. This can happen when the app reloads, restarts, or reconnects while a test file is still running.'
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
});
|