@react-native-harness/jest 1.2.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__/fixtures/mock-platform-runner.d.ts +7 -0
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
- package/dist/__tests__/harness-session.test.d.ts +2 -0
- package/dist/__tests__/harness-session.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session.test.js +74 -0
- 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
|
@@ -1,7 +1,7 @@
|
|
|
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 { createHarnessBridge } from '@react-native-harness/bridge/server';
|
|
@@ -117,6 +117,27 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
117
117
|
expect(result.tests[0].name).toBe('passes');
|
|
118
118
|
handle.disconnect();
|
|
119
119
|
});
|
|
120
|
+
it('rejects pending app calls when a newer client replaces the session', async () => {
|
|
121
|
+
const firstHandle = await connect({
|
|
122
|
+
runTests: async () => await new Promise(() => undefined),
|
|
123
|
+
});
|
|
124
|
+
firstHandle.reportReady(device);
|
|
125
|
+
const firstConnection = await bridge.nextConnection();
|
|
126
|
+
const pendingRun = firstConnection.runTests('example.ts', {
|
|
127
|
+
runner: '/runner.js',
|
|
128
|
+
});
|
|
129
|
+
const pendingRunAssertion = expect(pendingRun).rejects.toThrow('The app bridge was replaced by a newer app connection.');
|
|
130
|
+
const secondHandle = await connect();
|
|
131
|
+
secondHandle.reportReady({
|
|
132
|
+
...device,
|
|
133
|
+
model: 'iPhone 17 Pro Replacement',
|
|
134
|
+
});
|
|
135
|
+
await pendingRunAssertion;
|
|
136
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
137
|
+
expect(bridge.connection?.device.model).toBe('iPhone 17 Pro Replacement');
|
|
138
|
+
secondHandle.disconnect();
|
|
139
|
+
firstHandle.disconnect();
|
|
140
|
+
});
|
|
120
141
|
});
|
|
121
142
|
describe('bridge events', () => {
|
|
122
143
|
it('emitEvent on app side fires the event listener on bridge', async () => {
|
|
@@ -136,7 +157,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
136
157
|
it('rejects pending nextConnection() waiters', async () => {
|
|
137
158
|
const pending = bridge.nextConnection();
|
|
138
159
|
bridge.dispose();
|
|
139
|
-
await expect(pending).rejects.toThrow('
|
|
160
|
+
await expect(pending).rejects.toThrow('The app bridge was disposed before the test file finished running.');
|
|
140
161
|
});
|
|
141
162
|
});
|
|
142
163
|
});
|
|
@@ -1,212 +1,166 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { createCrashMonitor, CrashWatchCancelledError, } from '../crash-monitor.js';
|
|
3
|
-
import { NativeCrashError } from '../errors.js';
|
|
4
|
+
import { NativeCrashError, RuntimeDisconnectError } from '../errors.js';
|
|
4
5
|
const noop = () => undefined;
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
stop: vi.fn(resolveUndefined),
|
|
14
|
-
dispose: vi.fn(resolveUndefined),
|
|
6
|
+
const waitForClassification = () => new Promise((resolve) => setTimeout(resolve, 1600));
|
|
7
|
+
const createAppSessionMock = (initialState = { status: 'running' }, logs = [], getCrashDetails) => {
|
|
8
|
+
let state = initialState;
|
|
9
|
+
let listener = null;
|
|
10
|
+
const session = {
|
|
11
|
+
dispose: vi.fn(async () => undefined),
|
|
12
|
+
getState: vi.fn(async () => state),
|
|
13
|
+
getLogs: vi.fn(() => logs),
|
|
15
14
|
addListener: vi.fn((l) => {
|
|
16
|
-
|
|
15
|
+
listener = l;
|
|
17
16
|
}),
|
|
18
17
|
removeListener: vi.fn((l) => {
|
|
19
|
-
if (
|
|
20
|
-
|
|
18
|
+
if (listener === l)
|
|
19
|
+
listener = null;
|
|
21
20
|
}),
|
|
22
21
|
};
|
|
22
|
+
if (getCrashDetails) {
|
|
23
|
+
session.getCrashDetails = getCrashDetails;
|
|
24
|
+
}
|
|
23
25
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
session,
|
|
27
|
+
setState: (nextState) => {
|
|
28
|
+
state = nextState;
|
|
29
|
+
},
|
|
30
|
+
emit: (event) => listener?.(event),
|
|
26
31
|
};
|
|
27
32
|
};
|
|
28
|
-
const createPlatformRunnerMock = (isRunning = false, crashDetails = null) => ({
|
|
29
|
-
isAppRunning: vi.fn(async () => isRunning),
|
|
30
|
-
getCrashDetails: vi.fn(async () => crashDetails),
|
|
31
|
-
startApp: vi.fn(resolveUndefined),
|
|
32
|
-
restartApp: vi.fn(resolveUndefined),
|
|
33
|
-
stopApp: vi.fn(resolveUndefined),
|
|
34
|
-
dispose: vi.fn(resolveUndefined),
|
|
35
|
-
createAppMonitor: vi.fn(),
|
|
36
|
-
});
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Tests
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
33
|
describe('createCrashMonitor', () => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
it('becomes alive when the app starts', () => {
|
|
48
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
49
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
50
|
-
emit({ type: 'app_started' });
|
|
51
|
-
expect(cm.isAlive()).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
it('becomes not alive after a confirmed crash', async () => {
|
|
54
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
55
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
56
|
-
emit({ type: 'app_started' });
|
|
57
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
58
|
-
watch.promise.catch(noop);
|
|
59
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
60
|
-
await watch.promise.catch(noop);
|
|
61
|
-
expect(cm.isAlive()).toBe(false);
|
|
62
|
-
});
|
|
34
|
+
it('starts not alive and becomes alive when an app session is attached', () => {
|
|
35
|
+
const cm = createCrashMonitor();
|
|
36
|
+
const { session } = createAppSessionMock();
|
|
37
|
+
expect(cm.isAlive()).toBe(false);
|
|
38
|
+
cm.setAppSession(session);
|
|
39
|
+
expect(cm.isAlive()).toBe(true);
|
|
63
40
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const error = await watch.promise.catch((e) => e);
|
|
80
|
-
expect(error.testFilePath).toBe('/test/example.ts');
|
|
81
|
-
expect(error.details.phase).toBe('startup');
|
|
82
|
-
});
|
|
83
|
-
it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
|
|
84
|
-
const { monitor } = createAppMonitorMock();
|
|
85
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
86
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
87
|
-
watch.cancel();
|
|
88
|
-
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
89
|
-
});
|
|
90
|
-
it('subsequent cancel() after crash is a no-op', async () => {
|
|
91
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
92
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
93
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
94
|
-
watch.promise.catch(noop);
|
|
95
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
96
|
-
await watch.promise.catch(noop);
|
|
97
|
-
// Second cancel should not throw or cause issues.
|
|
98
|
-
expect(() => watch.cancel()).not.toThrow();
|
|
99
|
-
});
|
|
41
|
+
it('rejects a watch with NativeCrashError when the app session exits', async () => {
|
|
42
|
+
const { session, emit } = createAppSessionMock({
|
|
43
|
+
status: 'exited',
|
|
44
|
+
occurredAt: Date.now(),
|
|
45
|
+
reason: 'observed-exit',
|
|
46
|
+
});
|
|
47
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
48
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
49
|
+
watch.promise.catch(noop);
|
|
50
|
+
emit({ type: 'app_exited' });
|
|
51
|
+
const error = await watch.promise.catch((err) => err);
|
|
52
|
+
expect(error).toBeInstanceOf(NativeCrashError);
|
|
53
|
+
expect(error.testFilePath).toBe('/test/example.ts');
|
|
54
|
+
expect(error.details.phase).toBe('execution');
|
|
55
|
+
expect(cm.isAlive()).toBe(false);
|
|
100
56
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const runner = createPlatformRunnerMock(true /* still running */);
|
|
114
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
|
|
115
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
116
|
-
const settled = vi.fn();
|
|
117
|
-
watch.promise.then(settled, settled);
|
|
118
|
-
emit({ type: 'app_exited', isConfirmed: false });
|
|
119
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
120
|
-
expect(settled).not.toHaveBeenCalled();
|
|
121
|
-
watch.cancel();
|
|
122
|
-
});
|
|
123
|
-
it('fires on possible_crash when confirmed', async () => {
|
|
124
|
-
const { monitor, emit } = createAppMonitorMock();
|
|
125
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
126
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
127
|
-
watch.promise.catch(noop);
|
|
128
|
-
emit({ type: 'possible_crash', isConfirmed: true });
|
|
129
|
-
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
130
|
-
});
|
|
57
|
+
it('classifies bridge disconnect plus exited app as NativeCrashError', async () => {
|
|
58
|
+
const { session, setState } = createAppSessionMock();
|
|
59
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
60
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
61
|
+
watch.promise.catch(noop);
|
|
62
|
+
cm.handleBridgeDisconnect();
|
|
63
|
+
setState({
|
|
64
|
+
status: 'exited',
|
|
65
|
+
occurredAt: Date.now(),
|
|
66
|
+
reason: 'process-gone',
|
|
67
|
+
});
|
|
68
|
+
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
131
69
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: runner });
|
|
141
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
142
|
-
watch.promise.catch(noop);
|
|
143
|
-
emit({ type: 'app_exited', isConfirmed: true, crashDetails: { pid: 1234 } });
|
|
144
|
-
const error = await watch.promise.catch((e) => e);
|
|
145
|
-
expect(error.details.processName).toBe('MyApp');
|
|
146
|
-
expect(error.details.signal).toBe('SIGSEGV');
|
|
147
|
-
expect(error.details.pid).toBe(1234);
|
|
148
|
-
});
|
|
70
|
+
it('classifies bridge disconnect plus running app as RuntimeDisconnectError', async () => {
|
|
71
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
72
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
73
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
74
|
+
watch.promise.catch(noop);
|
|
75
|
+
cm.handleBridgeDisconnect();
|
|
76
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
149
77
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const cm = createCrashMonitor({ appMonitor: monitor, platformRunner: createPlatformRunnerMock() });
|
|
166
|
-
await cm.stop();
|
|
167
|
-
await cm.start();
|
|
168
|
-
const watch = cm.watch('test.ts', 'execution');
|
|
169
|
-
watch.promise.catch(noop);
|
|
170
|
-
emit({ type: 'app_exited', isConfirmed: true });
|
|
171
|
-
await expect(watch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
172
|
-
});
|
|
78
|
+
it('attaches matching session log evidence to native crash details', async () => {
|
|
79
|
+
const occurredAt = Date.now();
|
|
80
|
+
const logs = [
|
|
81
|
+
{ line: 'ordinary app log', occurredAt },
|
|
82
|
+
{ line: 'MyApp[123] fatal error: boom', occurredAt },
|
|
83
|
+
];
|
|
84
|
+
const { session, setState } = createAppSessionMock({ status: 'running' }, logs);
|
|
85
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
86
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
87
|
+
watch.promise.catch(noop);
|
|
88
|
+
cm.handleBridgeDisconnect();
|
|
89
|
+
setState({ status: 'exited', occurredAt, reason: 'process-gone' });
|
|
90
|
+
const error = await watch.promise.catch((err) => err);
|
|
91
|
+
expect(error).toBeInstanceOf(NativeCrashError);
|
|
92
|
+
expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
|
|
173
93
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
94
|
+
it('asks the app session to extract native crash details for the current test', async () => {
|
|
95
|
+
const occurredAt = Date.now();
|
|
96
|
+
const getCrashDetails = vi.fn(async () => ({
|
|
97
|
+
artifactType: 'logcat',
|
|
98
|
+
artifactPath: '/tmp/.harness/crash-reports/crash-logcat.txt',
|
|
99
|
+
processName: 'com.harnessplayground',
|
|
100
|
+
pid: 7777,
|
|
101
|
+
}));
|
|
102
|
+
const { session, setState } = createAppSessionMock({ status: 'running' }, [], getCrashDetails);
|
|
103
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
104
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
105
|
+
watch.promise.catch(noop);
|
|
106
|
+
cm.handleBridgeDisconnect();
|
|
107
|
+
setState({
|
|
108
|
+
status: 'exited',
|
|
109
|
+
occurredAt,
|
|
110
|
+
pid: 7777,
|
|
111
|
+
reason: 'process-gone',
|
|
112
|
+
});
|
|
113
|
+
const error = await watch.promise.catch((err) => err);
|
|
114
|
+
expect(getCrashDetails).toHaveBeenCalledWith({
|
|
115
|
+
occurredAt: expect.any(Number),
|
|
116
|
+
pid: 7777,
|
|
117
|
+
processName: undefined,
|
|
118
|
+
testFilePath: '/test/example.ts',
|
|
119
|
+
});
|
|
120
|
+
expect(error.details.artifactPath).toBe('/tmp/.harness/crash-reports/crash-logcat.txt');
|
|
121
|
+
expect(error.message).toContain(`Harness extracted the crash log: ${path.relative(process.cwd(), '/tmp/.harness/crash-reports/crash-logcat.txt')}`);
|
|
190
122
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
123
|
+
it('falls back to log details when native crash extraction fails', async () => {
|
|
124
|
+
const occurredAt = Date.now();
|
|
125
|
+
const getCrashDetails = vi.fn(async () => {
|
|
126
|
+
throw new Error('copy failed');
|
|
127
|
+
});
|
|
128
|
+
const { session, setState } = createAppSessionMock({ status: 'running' }, [{ line: 'MyApp[123] fatal error: boom', occurredAt }], getCrashDetails);
|
|
129
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
130
|
+
const watch = cm.watch('/test/example.ts', 'execution');
|
|
131
|
+
watch.promise.catch(noop);
|
|
132
|
+
cm.handleBridgeDisconnect();
|
|
133
|
+
setState({ status: 'exited', occurredAt, reason: 'process-gone' });
|
|
134
|
+
const error = await watch.promise.catch((err) => err);
|
|
135
|
+
expect(error.details.rawLines).toEqual(['MyApp[123] fatal error: boom']);
|
|
136
|
+
expect(error.message).toContain("Harness couldn't extract the crash log.");
|
|
137
|
+
});
|
|
138
|
+
it('settles the promise with CrashWatchCancelledError on cancel()', async () => {
|
|
139
|
+
const cm = createCrashMonitor();
|
|
140
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
141
|
+
watch.cancel();
|
|
142
|
+
await expect(watch.promise).rejects.toBeInstanceOf(CrashWatchCancelledError);
|
|
143
|
+
});
|
|
144
|
+
it('ignores session events while stopped', async () => {
|
|
145
|
+
const { session, emit } = createAppSessionMock({
|
|
146
|
+
status: 'exited',
|
|
147
|
+
occurredAt: Date.now(),
|
|
148
|
+
});
|
|
149
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
150
|
+
await cm.stop();
|
|
151
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
152
|
+
const settled = vi.fn();
|
|
153
|
+
watch.promise.then(settled, settled);
|
|
154
|
+
emit({ type: 'app_exited' });
|
|
155
|
+
await waitForClassification();
|
|
156
|
+
expect(settled).not.toHaveBeenCalled();
|
|
157
|
+
watch.cancel();
|
|
158
|
+
});
|
|
159
|
+
it('removes the app session listener on dispose', async () => {
|
|
160
|
+
const { session } = createAppSessionMock();
|
|
161
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
162
|
+
await cm.dispose();
|
|
163
|
+
expect(session.removeListener).toHaveBeenCalledOnce();
|
|
164
|
+
expect(cm.isAlive()).toBe(false);
|
|
211
165
|
});
|
|
212
166
|
});
|
|
@@ -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
|
describe('PlatformReadyTimeoutError', () => {
|
|
4
6
|
it('includes the configured timeout and config hint', () => {
|
|
@@ -6,6 +8,33 @@ describe('PlatformReadyTimeoutError', () => {
|
|
|
6
8
|
});
|
|
7
9
|
});
|
|
8
10
|
describe('NativeCrashError', () => {
|
|
11
|
+
it('reports the extracted crash log path when available', () => {
|
|
12
|
+
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
13
|
+
phase: 'execution',
|
|
14
|
+
artifactPath: path.join(process.cwd(), '.harness', 'crash-reports', 'crash.ips'),
|
|
15
|
+
});
|
|
16
|
+
expect(error.message).toContain(`Harness extracted the crash log: ${path.join('.harness', 'crash-reports', 'crash.ips')}`);
|
|
17
|
+
});
|
|
18
|
+
it('lists enrichment artifact paths when available', () => {
|
|
19
|
+
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
20
|
+
phase: 'execution',
|
|
21
|
+
artifactPath: path.join(process.cwd(), '.harness', 'crash-reports', 'logcat.txt'),
|
|
22
|
+
enrichmentArtifacts: [
|
|
23
|
+
{
|
|
24
|
+
artifactType: 'dropbox-native-crash',
|
|
25
|
+
artifactPath: path.join(process.cwd(), '.harness', 'crash-reports', 'dropbox-native-crash.txt'),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
expect(error.message).toContain('Additional crash artifacts:');
|
|
30
|
+
expect(error.message).toContain(path.join('.harness', 'crash-reports', 'dropbox-native-crash.txt'));
|
|
31
|
+
});
|
|
32
|
+
it('reports crash log extraction failure when no artifact was pulled', () => {
|
|
33
|
+
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
34
|
+
phase: 'execution',
|
|
35
|
+
});
|
|
36
|
+
expect(error.message).toContain("Harness couldn't extract the crash log.");
|
|
37
|
+
});
|
|
9
38
|
it('formats the extracted stack trace in the error message', () => {
|
|
10
39
|
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
11
40
|
phase: 'execution',
|
|
@@ -57,3 +86,13 @@ describe('NativeCrashError', () => {
|
|
|
57
86
|
expect(error.stack).toBe('NativeCrashError: The native app crashed while preparing to run this test file.');
|
|
58
87
|
});
|
|
59
88
|
});
|
|
89
|
+
describe('AppBridgeDisconnectedError', () => {
|
|
90
|
+
it('explains likely causes without exposing an internal stack', () => {
|
|
91
|
+
const error = new AppBridgeDisconnectedError('app-disconnected');
|
|
92
|
+
expect(error.message).toBe('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.');
|
|
93
|
+
expect(error.stack).toBe(`AppBridgeDisconnectedError: ${error.message}`);
|
|
94
|
+
});
|
|
95
|
+
it('uses a reconnection-specific message when a newer app connection replaces the old one', () => {
|
|
96
|
+
expect(new AppBridgeDisconnectedError('app-replaced').message).toBe('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.');
|
|
97
|
+
});
|
|
98
|
+
});
|