@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.
- package/dist/__tests__/bridge.test.js +29 -4
- package/dist/__tests__/crash-monitor.test.js +194 -176
- package/dist/__tests__/environment-reset.test.d.ts +2 -0
- package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
- package/dist/__tests__/environment-reset.test.js +238 -0
- package/dist/__tests__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +439 -29
- package/dist/__tests__/format-harness-error.test.d.ts +2 -0
- package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
- package/dist/__tests__/format-harness-error.test.js +27 -0
- package/dist/__tests__/harness-cache.test.js +62 -41
- package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
- package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session-signal.test.js +298 -0
- package/dist/__tests__/harness-session.test.js +217 -234
- package/dist/__tests__/index.test.js +62 -11
- package/dist/__tests__/metro-port.test.js +2 -0
- package/dist/__tests__/run.test.d.ts +2 -0
- package/dist/__tests__/run.test.d.ts.map +1 -0
- package/dist/__tests__/run.test.js +48 -0
- 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 +12 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +135 -102
- package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/derivers.test.js +248 -0
- package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/report.test.js +97 -0
- package/dist/diagnostics/derivers.d.ts +24 -0
- package/dist/diagnostics/derivers.d.ts.map +1 -0
- package/dist/diagnostics/derivers.js +146 -0
- package/dist/diagnostics/index.d.ts +3 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/report.d.ts +7 -0
- package/dist/diagnostics/report.d.ts.map +1 -0
- package/dist/diagnostics/report.js +84 -0
- package/dist/environment-reset.d.ts +51 -0
- package/dist/environment-reset.d.ts.map +1 -0
- package/dist/environment-reset.js +83 -0
- 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 +179 -49
- package/dist/format-harness-error.d.ts +5 -0
- package/dist/format-harness-error.d.ts.map +1 -0
- package/dist/format-harness-error.js +43 -0
- package/dist/harness-session.d.ts +46 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +386 -118
- 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 +22 -4
- 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 +8 -7
- package/src/__tests__/bridge.test.ts +45 -5
- package/src/__tests__/crash-monitor.test.ts +234 -199
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +561 -37
- package/src/__tests__/format-harness-error.test.ts +32 -0
- package/src/__tests__/harness-cache.test.ts +91 -0
- package/src/__tests__/harness-session.test.ts +303 -0
- package/src/__tests__/index.test.ts +92 -12
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +220 -131
- package/src/diagnostics/__tests__/derivers.test.ts +300 -0
- package/src/diagnostics/__tests__/report.test.ts +128 -0
- package/src/diagnostics/derivers.ts +170 -0
- package/src/diagnostics/index.ts +2 -0
- package/src/diagnostics/report.ts +124 -0
- package/src/environment-reset.ts +138 -0
- package/src/errors.ts +62 -1
- package/src/execute-run.ts +230 -57
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +535 -162
- package/src/index.ts +40 -11
- package/src/run.ts +24 -4
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
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",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -39,12 +39,13 @@
|
|
|
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/platforms": "1.
|
|
42
|
+
"@react-native-harness/cache": "1.4.0",
|
|
43
|
+
"@react-native-harness/bundler-metro": "1.4.0",
|
|
44
|
+
"@react-native-harness/config": "1.4.0",
|
|
45
|
+
"@react-native-harness/bridge": "1.4.0",
|
|
46
|
+
"@react-native-harness/plugins": "1.4.0",
|
|
47
|
+
"@react-native-harness/platforms": "1.4.0",
|
|
48
|
+
"@react-native-harness/tools": "1.4.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@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: {
|
|
@@ -36,8 +37,12 @@ afterEach(async () => {
|
|
|
36
37
|
await new Promise((r) => setTimeout(r, 10));
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
const connect = (
|
|
40
|
-
connectToHarness
|
|
40
|
+
const connect = (
|
|
41
|
+
callbacks: Parameters<typeof connectToHarness>[1] = {
|
|
42
|
+
runTests: vi.fn(),
|
|
43
|
+
resetEnvironment: vi.fn(),
|
|
44
|
+
},
|
|
45
|
+
) => connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
|
|
41
46
|
|
|
42
47
|
const device = {
|
|
43
48
|
platform: 'ios' as const,
|
|
@@ -139,7 +144,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
139
144
|
const runTestsCb = vi.fn(async () => suiteResult);
|
|
140
145
|
|
|
141
146
|
const connectionPromise = bridge.nextConnection();
|
|
142
|
-
const handle = await connect({ runTests: runTestsCb });
|
|
147
|
+
const handle = await connect({ runTests: runTestsCb, resetEnvironment: vi.fn() });
|
|
143
148
|
handle.reportReady(device);
|
|
144
149
|
|
|
145
150
|
const conn = await connectionPromise;
|
|
@@ -151,6 +156,39 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
151
156
|
expect(result.tests[0].name).toBe('passes');
|
|
152
157
|
handle.disconnect();
|
|
153
158
|
});
|
|
159
|
+
|
|
160
|
+
it('rejects pending app calls when a newer client replaces the session', async () => {
|
|
161
|
+
const firstHandle = await connect({
|
|
162
|
+
runTests: async (): Promise<TestSuiteResult> =>
|
|
163
|
+
await new Promise<TestSuiteResult>(() => undefined),
|
|
164
|
+
resetEnvironment: vi.fn(),
|
|
165
|
+
});
|
|
166
|
+
firstHandle.reportReady(device);
|
|
167
|
+
|
|
168
|
+
const firstConnection = await bridge.nextConnection();
|
|
169
|
+
const pendingRun = firstConnection.runTests('example.ts', {
|
|
170
|
+
runner: '/runner.js',
|
|
171
|
+
});
|
|
172
|
+
const pendingRunAssertion = expect(pendingRun).rejects.toThrow(
|
|
173
|
+
'The app bridge was replaced by a newer app connection.',
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const secondHandle = await connect();
|
|
177
|
+
secondHandle.reportReady({
|
|
178
|
+
...device,
|
|
179
|
+
model: 'iPhone 17 Pro Replacement',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await pendingRunAssertion;
|
|
183
|
+
|
|
184
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
185
|
+
|
|
186
|
+
expect(bridge.connection?.device.model).toBe(
|
|
187
|
+
'iPhone 17 Pro Replacement',
|
|
188
|
+
);
|
|
189
|
+
secondHandle.disconnect();
|
|
190
|
+
firstHandle.disconnect();
|
|
191
|
+
});
|
|
154
192
|
});
|
|
155
193
|
|
|
156
194
|
describe('bridge events', () => {
|
|
@@ -178,7 +216,9 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
178
216
|
const pending = bridge.nextConnection();
|
|
179
217
|
bridge.dispose();
|
|
180
218
|
|
|
181
|
-
await expect(pending).rejects.toThrow(
|
|
219
|
+
await expect(pending).rejects.toThrow(
|
|
220
|
+
'The app bridge was disposed before the test file finished running.',
|
|
221
|
+
);
|
|
182
222
|
});
|
|
183
223
|
});
|
|
184
224
|
});
|
|
@@ -1,288 +1,323 @@
|
|
|
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() });
|
|
54
|
+
it('starts not alive and becomes alive when an app session is attached', () => {
|
|
55
|
+
const cm = createCrashMonitor();
|
|
56
|
+
const { session } = createAppSessionMock();
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
});
|
|
58
|
+
expect(cm.isAlive()).toBe(false);
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
72
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
60
|
+
cm.setAppSession(session);
|
|
73
61
|
|
|
74
|
-
|
|
62
|
+
expect(cm.isAlive()).toBe(true);
|
|
63
|
+
});
|
|
75
64
|
|
|
76
|
-
|
|
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',
|
|
77
70
|
});
|
|
71
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
72
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
73
|
+
watch.promise.catch(noop);
|
|
78
74
|
|
|
79
|
-
|
|
80
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
81
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
82
|
-
|
|
83
|
-
emit({ type: 'app_started' });
|
|
84
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
85
|
-
watch.promise.catch(noop);
|
|
75
|
+
emit({ type: 'app_exited' });
|
|
86
76
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
92
82
|
});
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
89
|
+
|
|
90
|
+
cm.handleBridgeDisconnect();
|
|
91
|
+
setState({
|
|
92
|
+
status: 'exited',
|
|
93
|
+
occurredAt: Date.now(),
|
|
94
|
+
reason: 'process-gone',
|
|
104
95
|
});
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
97
|
+
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
98
|
+
});
|
|
109
99
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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);
|
|
113
105
|
|
|
114
|
-
|
|
115
|
-
expect(error.testFilePath).toBe('/test/example.ts');
|
|
116
|
-
expect(error.details.phase).toBe('startup');
|
|
117
|
-
});
|
|
106
|
+
cm.handleBridgeDisconnect();
|
|
118
107
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
108
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
109
|
+
});
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
|
|
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']);
|
|
131
|
+
});
|
|
125
132
|
|
|
126
|
-
|
|
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',
|
|
127
156
|
});
|
|
128
157
|
|
|
129
|
-
|
|
130
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
131
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
158
|
+
const error = await watch.promise.catch((err: NativeCrashError) => err);
|
|
132
159
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Second cancel should not throw or cause issues.
|
|
139
|
-
expect(() => watch.cancel()).not.toThrow();
|
|
160
|
+
expect(getCrashDetails).toHaveBeenCalledWith({
|
|
161
|
+
occurredAt: expect.any(Number),
|
|
162
|
+
pid: 7777,
|
|
163
|
+
processName: undefined,
|
|
164
|
+
testFilePath: '/test/example.ts',
|
|
140
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
|
+
);
|
|
141
175
|
});
|
|
142
176
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
|
|
148
|
-
|
|
149
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
150
|
-
watch.promise.catch(noop);
|
|
151
|
-
emit({ type: 'app_exited', isConfirmed: false });
|
|
152
|
-
|
|
153
|
-
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
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');
|
|
154
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);
|
|
190
|
+
|
|
191
|
+
cm.handleBridgeDisconnect();
|
|
192
|
+
setState({ status: 'exited', occurredAt, reason: 'process-gone' });
|
|
193
|
+
|
|
194
|
+
const error = await watch.promise.catch((err: NativeCrashError) => err);
|
|
195
|
+
|
|
196
|
+
expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
|
|
197
|
+
expect(error.message).toContain("Harness couldn't extract the crash log.");
|
|
198
|
+
});
|
|
155
199
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
|
|
200
|
+
it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
|
|
201
|
+
const cm = createCrashMonitor();
|
|
202
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
160
203
|
|
|
161
|
-
|
|
162
|
-
const settled = vi.fn();
|
|
163
|
-
watch.promise.then(settled, settled);
|
|
204
|
+
watch.cancel();
|
|
164
205
|
|
|
165
|
-
|
|
166
|
-
|
|
206
|
+
await expect(watch.promise).rejects.toBeInstanceOf(
|
|
207
|
+
CrashWatchCancelledError
|
|
208
|
+
);
|
|
209
|
+
});
|
|
167
210
|
|
|
168
|
-
|
|
169
|
-
|
|
211
|
+
it('ignores session events while stopped', async () => {
|
|
212
|
+
const { session, emit } = createAppSessionMock({
|
|
213
|
+
status: 'exited',
|
|
214
|
+
occurredAt: Date.now(),
|
|
170
215
|
});
|
|
216
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
217
|
+
await cm.stop();
|
|
171
218
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
219
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
220
|
+
const settled = vi.fn();
|
|
221
|
+
watch.promise.then(settled, settled);
|
|
175
222
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
emit({ type: 'possible_crash', isConfirmed: true });
|
|
223
|
+
emit({ type: 'app_exited' });
|
|
224
|
+
await waitForClassification();
|
|
179
225
|
|
|
180
|
-
|
|
181
|
-
|
|
226
|
+
expect(settled).not.toHaveBeenCalled();
|
|
227
|
+
watch.cancel();
|
|
182
228
|
});
|
|
183
229
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const runner = createPlatformRunnerMock(false, {
|
|
188
|
-
processName: 'MyApp',
|
|
189
|
-
signal: 'SIGSEGV',
|
|
190
|
-
summary: 'Segmentation fault',
|
|
191
|
-
});
|
|
192
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
|
|
230
|
+
it('removes the app session listener on dispose', async () => {
|
|
231
|
+
const { session } = createAppSessionMock();
|
|
232
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
193
233
|
|
|
194
|
-
|
|
195
|
-
watch.promise.catch(noop);
|
|
196
|
-
emit({ type: 'app_exited', isConfirmed: true, crashDetails: { pid: 1234 } });
|
|
234
|
+
await cm.dispose();
|
|
197
235
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
expect(error.details.signal).toBe('SIGSEGV');
|
|
201
|
-
expect(error.details.pid).toBe(1234);
|
|
202
|
-
});
|
|
236
|
+
expect(session.removeListener).toHaveBeenCalledOnce();
|
|
237
|
+
expect(cm.isAlive()).toBe(false);
|
|
203
238
|
});
|
|
204
239
|
|
|
205
|
-
describe('
|
|
206
|
-
it('
|
|
207
|
-
const {
|
|
208
|
-
const cm = createCrashMonitor({
|
|
240
|
+
describe('expectDisconnect', () => {
|
|
241
|
+
it('suppresses handleBridgeDisconnect while the window is open', async () => {
|
|
242
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
243
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
244
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
245
|
+
watch.promise.catch(noop);
|
|
209
246
|
|
|
210
|
-
|
|
247
|
+
const closeWindow = cm.expectDisconnect();
|
|
248
|
+
cm.handleBridgeDisconnect();
|
|
249
|
+
await waitForClassification();
|
|
211
250
|
|
|
212
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
213
251
|
const settled = vi.fn();
|
|
214
252
|
watch.promise.then(settled, settled);
|
|
215
|
-
|
|
216
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
217
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
218
|
-
|
|
253
|
+
await Promise.resolve();
|
|
219
254
|
expect(settled).not.toHaveBeenCalled();
|
|
255
|
+
|
|
256
|
+
closeWindow();
|
|
220
257
|
watch.cancel();
|
|
221
258
|
});
|
|
222
259
|
|
|
223
|
-
it('resumes
|
|
224
|
-
const {
|
|
225
|
-
const cm = createCrashMonitor({
|
|
226
|
-
|
|
227
|
-
await cm.stop();
|
|
228
|
-
await cm.start();
|
|
229
|
-
|
|
260
|
+
it('resumes classifying disconnects once the window closes', async () => {
|
|
261
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
262
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
230
263
|
const watch = cm.watch('test.ts', 'execution');
|
|
231
264
|
watch.promise.catch(noop);
|
|
232
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
233
265
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
266
|
+
const closeWindow = cm.expectDisconnect();
|
|
267
|
+
closeWindow();
|
|
268
|
+
|
|
269
|
+
cm.handleBridgeDisconnect();
|
|
237
270
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
241
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
271
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
272
|
+
});
|
|
242
273
|
|
|
243
|
-
|
|
274
|
+
it('keeps the window open until every opener has closed it (nested windows)', async () => {
|
|
275
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
276
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
244
277
|
const watch = cm.watch('test.ts', 'execution');
|
|
245
278
|
watch.promise.catch(noop);
|
|
246
279
|
|
|
247
|
-
cm.
|
|
280
|
+
const closeFirst = cm.expectDisconnect();
|
|
281
|
+
const closeSecond = cm.expectDisconnect();
|
|
282
|
+
closeFirst();
|
|
248
283
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
252
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
284
|
+
cm.handleBridgeDisconnect();
|
|
285
|
+
await waitForClassification();
|
|
253
286
|
|
|
254
|
-
|
|
287
|
+
const settled = vi.fn();
|
|
288
|
+
watch.promise.then(settled, settled);
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
expect(settled).not.toHaveBeenCalled();
|
|
291
|
+
|
|
292
|
+
closeSecond();
|
|
255
293
|
watch.cancel();
|
|
256
|
-
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
257
294
|
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('dispose', () => {
|
|
261
|
-
it('ignores events after dispose', async () => {
|
|
262
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
263
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
264
295
|
|
|
296
|
+
it('ignores a redundant close() call', async () => {
|
|
297
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
298
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
265
299
|
const watch = cm.watch('test.ts', 'execution');
|
|
266
300
|
watch.promise.catch(noop);
|
|
267
301
|
|
|
268
|
-
|
|
302
|
+
const closeWindow = cm.expectDisconnect();
|
|
303
|
+
closeWindow();
|
|
304
|
+
closeWindow();
|
|
269
305
|
|
|
270
|
-
|
|
271
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
306
|
+
cm.handleBridgeDisconnect();
|
|
272
307
|
|
|
273
|
-
|
|
274
|
-
// The promise is still pending - cancel to settle it.
|
|
275
|
-
watch.cancel();
|
|
276
|
-
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
308
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
277
309
|
});
|
|
278
310
|
|
|
279
|
-
it('
|
|
280
|
-
const {
|
|
281
|
-
const cm = createCrashMonitor({
|
|
311
|
+
it('clears the expected-disconnect window on dispose', async () => {
|
|
312
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
313
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
314
|
+
cm.expectDisconnect();
|
|
282
315
|
|
|
283
316
|
await cm.dispose();
|
|
284
317
|
|
|
285
|
-
|
|
318
|
+
// A disconnect after dispose should not throw or hang; monitoring is
|
|
319
|
+
// fully torn down regardless of the window state.
|
|
320
|
+
expect(() => cm.handleBridgeDisconnect()).not.toThrow();
|
|
286
321
|
});
|
|
287
322
|
});
|
|
288
323
|
});
|