@react-native-harness/jest 1.4.0-rc.1 → 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 +6 -2
- package/dist/__tests__/crash-monitor.test.js +64 -0
- 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__/execute-run.test.js +250 -4
- 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 +161 -2
- package/dist/__tests__/index.test.js +3 -1
- 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/crash-monitor.d.ts +7 -0
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +19 -0
- 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/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +121 -69
- 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 +26 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +260 -87
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +21 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/bridge.test.ts +8 -3
- package/src/__tests__/crash-monitor.test.ts +84 -0
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/execute-run.test.ts +327 -4
- 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 +211 -1
- package/src/__tests__/index.test.ts +3 -1
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/crash-monitor.ts +28 -0
- 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/execute-run.ts +136 -77
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +369 -131
- package/src/run.ts +23 -4
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
|
@@ -28,7 +28,10 @@ afterEach(async () => {
|
|
|
28
28
|
// Allow the server to close cleanly.
|
|
29
29
|
await new Promise((r) => setTimeout(r, 10));
|
|
30
30
|
});
|
|
31
|
-
const connect = (callbacks = {
|
|
31
|
+
const connect = (callbacks = {
|
|
32
|
+
runTests: vi.fn(),
|
|
33
|
+
resetEnvironment: vi.fn(),
|
|
34
|
+
}) => connectToHarness(`ws://127.0.0.1:${bridgePort}`, callbacks);
|
|
32
35
|
const device = {
|
|
33
36
|
platform: 'ios',
|
|
34
37
|
manufacturer: 'Apple',
|
|
@@ -107,7 +110,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
107
110
|
};
|
|
108
111
|
const runTestsCb = vi.fn(async () => suiteResult);
|
|
109
112
|
const connectionPromise = bridge.nextConnection();
|
|
110
|
-
const handle = await connect({ runTests: runTestsCb });
|
|
113
|
+
const handle = await connect({ runTests: runTestsCb, resetEnvironment: vi.fn() });
|
|
111
114
|
handle.reportReady(device);
|
|
112
115
|
const conn = await connectionPromise;
|
|
113
116
|
const result = await conn.runTests('example.ts', {
|
|
@@ -120,6 +123,7 @@ describe('bridge: createHarnessBridge + connectToHarness', () => {
|
|
|
120
123
|
it('rejects pending app calls when a newer client replaces the session', async () => {
|
|
121
124
|
const firstHandle = await connect({
|
|
122
125
|
runTests: async () => await new Promise(() => undefined),
|
|
126
|
+
resetEnvironment: vi.fn(),
|
|
123
127
|
});
|
|
124
128
|
firstHandle.reportReady(device);
|
|
125
129
|
const firstConnection = await bridge.nextConnection();
|
|
@@ -163,4 +163,68 @@ describe('createCrashMonitor', () => {
|
|
|
163
163
|
expect(session.removeListener).toHaveBeenCalledOnce();
|
|
164
164
|
expect(cm.isAlive()).toBe(false);
|
|
165
165
|
});
|
|
166
|
+
describe('expectDisconnect', () => {
|
|
167
|
+
it('suppresses handleBridgeDisconnect while the window is open', async () => {
|
|
168
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
169
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
170
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
171
|
+
watch.promise.catch(noop);
|
|
172
|
+
const closeWindow = cm.expectDisconnect();
|
|
173
|
+
cm.handleBridgeDisconnect();
|
|
174
|
+
await waitForClassification();
|
|
175
|
+
const settled = vi.fn();
|
|
176
|
+
watch.promise.then(settled, settled);
|
|
177
|
+
await Promise.resolve();
|
|
178
|
+
expect(settled).not.toHaveBeenCalled();
|
|
179
|
+
closeWindow();
|
|
180
|
+
watch.cancel();
|
|
181
|
+
});
|
|
182
|
+
it('resumes classifying disconnects once the window closes', async () => {
|
|
183
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
184
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
185
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
186
|
+
watch.promise.catch(noop);
|
|
187
|
+
const closeWindow = cm.expectDisconnect();
|
|
188
|
+
closeWindow();
|
|
189
|
+
cm.handleBridgeDisconnect();
|
|
190
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
191
|
+
});
|
|
192
|
+
it('keeps the window open until every opener has closed it (nested windows)', async () => {
|
|
193
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
194
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
195
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
196
|
+
watch.promise.catch(noop);
|
|
197
|
+
const closeFirst = cm.expectDisconnect();
|
|
198
|
+
const closeSecond = cm.expectDisconnect();
|
|
199
|
+
closeFirst();
|
|
200
|
+
cm.handleBridgeDisconnect();
|
|
201
|
+
await waitForClassification();
|
|
202
|
+
const settled = vi.fn();
|
|
203
|
+
watch.promise.then(settled, settled);
|
|
204
|
+
await Promise.resolve();
|
|
205
|
+
expect(settled).not.toHaveBeenCalled();
|
|
206
|
+
closeSecond();
|
|
207
|
+
watch.cancel();
|
|
208
|
+
});
|
|
209
|
+
it('ignores a redundant close() call', async () => {
|
|
210
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
211
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
212
|
+
const watch = cm.watch('test.ts', 'execution');
|
|
213
|
+
watch.promise.catch(noop);
|
|
214
|
+
const closeWindow = cm.expectDisconnect();
|
|
215
|
+
closeWindow();
|
|
216
|
+
closeWindow();
|
|
217
|
+
cm.handleBridgeDisconnect();
|
|
218
|
+
await expect(watch.promise).rejects.toBeInstanceOf(RuntimeDisconnectError);
|
|
219
|
+
});
|
|
220
|
+
it('clears the expected-disconnect window on dispose', async () => {
|
|
221
|
+
const { session } = createAppSessionMock({ status: 'running' });
|
|
222
|
+
const cm = createCrashMonitor({ appSession: session });
|
|
223
|
+
cm.expectDisconnect();
|
|
224
|
+
await cm.dispose();
|
|
225
|
+
// A disconnect after dispose should not throw or hang; monitoring is
|
|
226
|
+
// fully torn down regardless of the window state.
|
|
227
|
+
expect(() => cm.handleBridgeDisconnect()).not.toThrow();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
166
230
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment-reset.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/environment-reset.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createProcessResetStrategy, createRuntimeResetStrategy, resolveResetStrategyKind, withEscalation, } from '../environment-reset.js';
|
|
3
|
+
describe('resolveResetStrategyKind', () => {
|
|
4
|
+
it('normalizes true to the process strategy', () => {
|
|
5
|
+
expect(resolveResetStrategyKind(true)).toBe('process');
|
|
6
|
+
});
|
|
7
|
+
it('normalizes false to null (no reset)', () => {
|
|
8
|
+
expect(resolveResetStrategyKind(false)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
it('passes through the process string value', () => {
|
|
11
|
+
expect(resolveResetStrategyKind('process')).toBe('process');
|
|
12
|
+
});
|
|
13
|
+
it('passes through the runtime string value', () => {
|
|
14
|
+
expect(resolveResetStrategyKind('runtime')).toBe('runtime');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('createProcessResetStrategy', () => {
|
|
18
|
+
it('has kind "process" and delegates to the injected restart function', async () => {
|
|
19
|
+
const restart = vi.fn(async () => undefined);
|
|
20
|
+
const strategy = createProcessResetStrategy({ restart });
|
|
21
|
+
expect(strategy.kind).toBe('process');
|
|
22
|
+
await strategy.reset({ testFilePath: '/tests/example.harness.ts' });
|
|
23
|
+
expect(restart).toHaveBeenCalledWith('/tests/example.harness.ts');
|
|
24
|
+
});
|
|
25
|
+
it('propagates restart failures', async () => {
|
|
26
|
+
const restart = vi.fn(async () => {
|
|
27
|
+
throw new Error('restart failed');
|
|
28
|
+
});
|
|
29
|
+
const strategy = createProcessResetStrategy({ restart });
|
|
30
|
+
await expect(strategy.reset({ testFilePath: '/tests/example.harness.ts' })).rejects.toThrow('restart failed');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('createRuntimeResetStrategy', () => {
|
|
34
|
+
const testFilePath = '/tests/example.harness.ts';
|
|
35
|
+
it('has kind "runtime" and resets via the connection then waits for reconnect', async () => {
|
|
36
|
+
const callOrder = [];
|
|
37
|
+
const closeWindow = vi.fn();
|
|
38
|
+
const connection = {
|
|
39
|
+
resetEnvironment: vi.fn(async () => {
|
|
40
|
+
callOrder.push('resetEnvironment');
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
const waitForReconnect = vi.fn(async () => {
|
|
44
|
+
callOrder.push('waitForReconnect-started');
|
|
45
|
+
});
|
|
46
|
+
const strategy = createRuntimeResetStrategy({
|
|
47
|
+
getConnection: () => connection,
|
|
48
|
+
expectDisconnect: () => {
|
|
49
|
+
callOrder.push('expectDisconnect');
|
|
50
|
+
return closeWindow;
|
|
51
|
+
},
|
|
52
|
+
waitForReconnect,
|
|
53
|
+
isDisconnectError: () => false,
|
|
54
|
+
timeoutMs: 1000,
|
|
55
|
+
});
|
|
56
|
+
expect(strategy.kind).toBe('runtime');
|
|
57
|
+
await strategy.reset({ testFilePath });
|
|
58
|
+
expect(callOrder).toEqual([
|
|
59
|
+
'expectDisconnect',
|
|
60
|
+
'waitForReconnect-started',
|
|
61
|
+
'resetEnvironment',
|
|
62
|
+
]);
|
|
63
|
+
expect(waitForReconnect).toHaveBeenCalledWith(expect.any(AbortSignal));
|
|
64
|
+
expect(closeWindow).toHaveBeenCalledOnce();
|
|
65
|
+
});
|
|
66
|
+
it('starts waiting for reconnect before firing the RPC', async () => {
|
|
67
|
+
const callOrder = [];
|
|
68
|
+
const connection = {
|
|
69
|
+
resetEnvironment: vi.fn(async () => {
|
|
70
|
+
callOrder.push('resetEnvironment');
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
const waitForReconnect = vi.fn(() => {
|
|
74
|
+
callOrder.push('waitForReconnect-called');
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
});
|
|
77
|
+
const strategy = createRuntimeResetStrategy({
|
|
78
|
+
getConnection: () => connection,
|
|
79
|
+
expectDisconnect: () => () => undefined,
|
|
80
|
+
waitForReconnect,
|
|
81
|
+
isDisconnectError: () => false,
|
|
82
|
+
timeoutMs: 1000,
|
|
83
|
+
});
|
|
84
|
+
await strategy.reset({ testFilePath });
|
|
85
|
+
expect(callOrder).toEqual(['waitForReconnect-called', 'resetEnvironment']);
|
|
86
|
+
});
|
|
87
|
+
it('throws when there is no active app connection', async () => {
|
|
88
|
+
const expectDisconnect = vi.fn();
|
|
89
|
+
const strategy = createRuntimeResetStrategy({
|
|
90
|
+
getConnection: () => null,
|
|
91
|
+
expectDisconnect,
|
|
92
|
+
waitForReconnect: vi.fn(async () => undefined),
|
|
93
|
+
isDisconnectError: () => false,
|
|
94
|
+
timeoutMs: 1000,
|
|
95
|
+
});
|
|
96
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('No active app connection');
|
|
97
|
+
expect(expectDisconnect).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
it('closes the expected-disconnect window even when the RPC call fails', async () => {
|
|
100
|
+
const closeWindow = vi.fn();
|
|
101
|
+
const connection = {
|
|
102
|
+
resetEnvironment: vi.fn(async () => {
|
|
103
|
+
throw new Error('rpc failed');
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
const strategy = createRuntimeResetStrategy({
|
|
107
|
+
getConnection: () => connection,
|
|
108
|
+
expectDisconnect: () => closeWindow,
|
|
109
|
+
waitForReconnect: vi.fn(async () => undefined),
|
|
110
|
+
isDisconnectError: () => false,
|
|
111
|
+
timeoutMs: 1000,
|
|
112
|
+
});
|
|
113
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
|
|
114
|
+
expect(closeWindow).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
it('closes the expected-disconnect window even when reconnect times out', async () => {
|
|
117
|
+
const closeWindow = vi.fn();
|
|
118
|
+
const connection = { resetEnvironment: vi.fn(async () => undefined) };
|
|
119
|
+
const strategy = createRuntimeResetStrategy({
|
|
120
|
+
getConnection: () => connection,
|
|
121
|
+
expectDisconnect: () => closeWindow,
|
|
122
|
+
waitForReconnect: vi.fn(async () => {
|
|
123
|
+
throw new Error('reconnect timed out');
|
|
124
|
+
}),
|
|
125
|
+
isDisconnectError: () => false,
|
|
126
|
+
timeoutMs: 1000,
|
|
127
|
+
});
|
|
128
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('reconnect timed out');
|
|
129
|
+
expect(closeWindow).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
it('treats a disconnect-flavored RPC rejection as reload-in-flight and succeeds', async () => {
|
|
132
|
+
const disconnectError = new Error('app bridge disconnected');
|
|
133
|
+
const connection = {
|
|
134
|
+
resetEnvironment: vi.fn(async () => {
|
|
135
|
+
throw disconnectError;
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
const waitForReconnect = vi.fn(async () => undefined);
|
|
139
|
+
const strategy = createRuntimeResetStrategy({
|
|
140
|
+
getConnection: () => connection,
|
|
141
|
+
expectDisconnect: () => () => undefined,
|
|
142
|
+
waitForReconnect,
|
|
143
|
+
isDisconnectError: (error) => error === disconnectError,
|
|
144
|
+
timeoutMs: 1000,
|
|
145
|
+
});
|
|
146
|
+
await expect(strategy.reset({ testFilePath })).resolves.toBeUndefined();
|
|
147
|
+
expect(waitForReconnect).toHaveBeenCalledOnce();
|
|
148
|
+
});
|
|
149
|
+
it('still throws on a non-disconnect RPC rejection', async () => {
|
|
150
|
+
const connection = {
|
|
151
|
+
resetEnvironment: vi.fn(async () => {
|
|
152
|
+
throw new Error('rpc failed');
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
const strategy = createRuntimeResetStrategy({
|
|
156
|
+
getConnection: () => connection,
|
|
157
|
+
expectDisconnect: () => () => undefined,
|
|
158
|
+
waitForReconnect: vi.fn(async () => undefined),
|
|
159
|
+
isDisconnectError: (error) => error instanceof Error && error.message === 'app bridge disconnected',
|
|
160
|
+
timeoutMs: 1000,
|
|
161
|
+
});
|
|
162
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
|
|
163
|
+
});
|
|
164
|
+
it('does not leave an unhandled rejection when the RPC fails before the reconnect wait settles', async () => {
|
|
165
|
+
let rejectReconnect;
|
|
166
|
+
const waitForReconnect = vi.fn(() => new Promise((_, reject) => {
|
|
167
|
+
rejectReconnect = reject;
|
|
168
|
+
}));
|
|
169
|
+
const connection = {
|
|
170
|
+
resetEnvironment: vi.fn(async () => {
|
|
171
|
+
throw new Error('rpc failed');
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
const unhandled = [];
|
|
175
|
+
const onUnhandledRejection = (reason) => {
|
|
176
|
+
unhandled.push(reason);
|
|
177
|
+
};
|
|
178
|
+
process.on('unhandledRejection', onUnhandledRejection);
|
|
179
|
+
try {
|
|
180
|
+
const strategy = createRuntimeResetStrategy({
|
|
181
|
+
getConnection: () => connection,
|
|
182
|
+
expectDisconnect: () => () => undefined,
|
|
183
|
+
waitForReconnect,
|
|
184
|
+
isDisconnectError: () => false,
|
|
185
|
+
timeoutMs: 1000,
|
|
186
|
+
});
|
|
187
|
+
await expect(strategy.reset({ testFilePath })).rejects.toThrow('rpc failed');
|
|
188
|
+
// Simulate the reconnect timeout firing after the strategy already
|
|
189
|
+
// threw; the strategy must have attached a rejection handler.
|
|
190
|
+
rejectReconnect(new Error('reconnect timed out'));
|
|
191
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
192
|
+
expect(unhandled).toEqual([]);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
process.off('unhandledRejection', onUnhandledRejection);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe('withEscalation', () => {
|
|
200
|
+
const testFilePath = '/tests/example.harness.ts';
|
|
201
|
+
const makeStrategy = (kind, reset) => ({ kind, reset });
|
|
202
|
+
it('reports the primary strategy kind', () => {
|
|
203
|
+
const primary = makeStrategy('runtime', async () => undefined);
|
|
204
|
+
const fallback = makeStrategy('process', async () => undefined);
|
|
205
|
+
expect(withEscalation(primary, fallback).kind).toBe('runtime');
|
|
206
|
+
});
|
|
207
|
+
it('does not call the fallback when the primary succeeds', async () => {
|
|
208
|
+
const fallbackReset = vi.fn(async () => undefined);
|
|
209
|
+
const primary = makeStrategy('runtime', async () => undefined);
|
|
210
|
+
const fallback = makeStrategy('process', fallbackReset);
|
|
211
|
+
const onEscalate = vi.fn();
|
|
212
|
+
await withEscalation(primary, fallback, { onEscalate }).reset({ testFilePath });
|
|
213
|
+
expect(fallbackReset).not.toHaveBeenCalled();
|
|
214
|
+
expect(onEscalate).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
it('escalates to the fallback when the primary fails', async () => {
|
|
217
|
+
const primaryError = new Error('runtime reset failed');
|
|
218
|
+
const fallbackReset = vi.fn(async () => undefined);
|
|
219
|
+
const primary = makeStrategy('runtime', async () => {
|
|
220
|
+
throw primaryError;
|
|
221
|
+
});
|
|
222
|
+
const fallback = makeStrategy('process', fallbackReset);
|
|
223
|
+
const onEscalate = vi.fn();
|
|
224
|
+
await withEscalation(primary, fallback, { onEscalate }).reset({ testFilePath });
|
|
225
|
+
expect(fallbackReset).toHaveBeenCalledWith({ testFilePath });
|
|
226
|
+
expect(onEscalate).toHaveBeenCalledWith(primaryError);
|
|
227
|
+
});
|
|
228
|
+
it('propagates the fallback error when both strategies fail', async () => {
|
|
229
|
+
const fallbackError = new Error('process reset also failed');
|
|
230
|
+
const primary = makeStrategy('runtime', async () => {
|
|
231
|
+
throw new Error('runtime reset failed');
|
|
232
|
+
});
|
|
233
|
+
const fallback = makeStrategy('process', async () => {
|
|
234
|
+
throw fallbackError;
|
|
235
|
+
});
|
|
236
|
+
await expect(withEscalation(primary, fallback).reset({ testFilePath })).rejects.toBe(fallbackError);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { NativeCrashError, StartupStallError } from '../errors.js';
|
|
3
3
|
import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
|
|
4
|
+
import { createDiagnostics } from '@react-native-harness/tools';
|
|
4
5
|
import { executeRun } from '../execute-run.js';
|
|
5
6
|
const resolveUndefined = async () => undefined;
|
|
6
7
|
// Mock the file-runner so we control what jestResult/harnessResult each test returns
|
|
@@ -9,6 +10,15 @@ const mockRunHarnessTestFile = vi.hoisted(() => vi.fn());
|
|
|
9
10
|
vi.mock('../run.js', () => ({
|
|
10
11
|
runHarnessTestFile: mockRunHarnessTestFile,
|
|
11
12
|
}));
|
|
13
|
+
// Mock the reporting module so diagnostics tests can inspect the entries that
|
|
14
|
+
// would have been reported without touching the real filesystem, and so
|
|
15
|
+
// asserting on them doesn't race with executeRun's own diagnostics.flush().
|
|
16
|
+
const mockPrintSummary = vi.hoisted(() => vi.fn());
|
|
17
|
+
const mockWriteTraceFile = vi.hoisted(() => vi.fn(async () => '/fake/trace.json'));
|
|
18
|
+
vi.mock('../diagnostics/index.js', () => ({
|
|
19
|
+
printSummary: mockPrintSummary,
|
|
20
|
+
writeTraceFile: mockWriteTraceFile,
|
|
21
|
+
}));
|
|
12
22
|
// ---------------------------------------------------------------------------
|
|
13
23
|
// Helpers
|
|
14
24
|
// ---------------------------------------------------------------------------
|
|
@@ -80,9 +90,11 @@ const makeSession = (overrides = {}) => ({
|
|
|
80
90
|
config: {},
|
|
81
91
|
},
|
|
82
92
|
},
|
|
93
|
+
diagnostics: createDiagnostics({ enabled: false }),
|
|
83
94
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
84
95
|
runTestFile: vi.fn(async () => makeHarnessResult()),
|
|
85
96
|
restartApp: vi.fn(resolveUndefined),
|
|
97
|
+
resetEnvironment: vi.fn(resolveUndefined),
|
|
86
98
|
resetCrashState: vi.fn(),
|
|
87
99
|
flushClientLogs: vi.fn(() => []),
|
|
88
100
|
callHook: vi.fn(resolveUndefined),
|
|
@@ -138,13 +150,57 @@ describe('executeRun', () => {
|
|
|
138
150
|
expect(hookNames).toContain('run:finished');
|
|
139
151
|
});
|
|
140
152
|
});
|
|
153
|
+
describe('diagnostics', () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
mockPrintSummary.mockClear();
|
|
156
|
+
mockWriteTraceFile.mockClear();
|
|
157
|
+
});
|
|
158
|
+
it('records run.total and run.file spans when diagnostics is enabled', async () => {
|
|
159
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
160
|
+
const session = makeSession({ diagnostics });
|
|
161
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
162
|
+
// executeRun flushes and hands entries to the (mocked) reporter in its
|
|
163
|
+
// finally block, so we inspect what was reported rather than flushing
|
|
164
|
+
// the diagnostics instance ourselves.
|
|
165
|
+
expect(mockPrintSummary).toHaveBeenCalledTimes(1);
|
|
166
|
+
const entries = mockPrintSummary.mock.calls[0]?.[0];
|
|
167
|
+
const runEntry = entries.find((e) => e.label === 'run.total');
|
|
168
|
+
const fileEntry = entries.find((e) => e.label === 'run.file');
|
|
169
|
+
expect(runEntry).toMatchObject({ status: 'ok', attrs: { status: 'passed' } });
|
|
170
|
+
expect(runEntry?.attrs?.runId).toBeTypeOf('string');
|
|
171
|
+
expect(fileEntry).toMatchObject({ status: 'ok', attrs: { file: '../a.ts', status: 'passed' } });
|
|
172
|
+
expect(fileEntry?.attrs?.runId).toBeTypeOf('string');
|
|
173
|
+
expect(mockWriteTraceFile).toHaveBeenCalledWith(entries, expect.objectContaining({ runId: expect.any(String) }));
|
|
174
|
+
});
|
|
175
|
+
it('still records run.total when a test throws', async () => {
|
|
176
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
177
|
+
const session = makeSession({
|
|
178
|
+
diagnostics,
|
|
179
|
+
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
180
|
+
});
|
|
181
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
182
|
+
const entries = mockPrintSummary.mock.calls[0]?.[0];
|
|
183
|
+
expect(entries.find((e) => e.label === 'run.total')).toBeDefined();
|
|
184
|
+
expect(entries.find((e) => e.label === 'run.file')).toMatchObject({
|
|
185
|
+
attrs: { status: 'failed' },
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it('is a no-op and does not generate a report when diagnostics is disabled', async () => {
|
|
189
|
+
const diagnostics = createDiagnostics({ enabled: false });
|
|
190
|
+
const session = makeSession({ diagnostics });
|
|
191
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
192
|
+
expect(diagnostics.flush()).toEqual([]);
|
|
193
|
+
expect(mockPrintSummary).not.toHaveBeenCalled();
|
|
194
|
+
expect(mockWriteTraceFile).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
141
197
|
describe('happy path', () => {
|
|
142
198
|
it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
|
|
143
199
|
const order = [];
|
|
144
200
|
const session = makeSession({
|
|
145
201
|
ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
|
|
146
202
|
});
|
|
147
|
-
const emitEvent = (async (eventName
|
|
203
|
+
const emitEvent = (async (eventName) => {
|
|
148
204
|
if (eventName === 'test-file-start') {
|
|
149
205
|
order.push('test-file-start');
|
|
150
206
|
}
|
|
@@ -178,6 +234,14 @@ describe('executeRun', () => {
|
|
|
178
234
|
expect(payload.summary.passed).toBe(6);
|
|
179
235
|
expect(payload.status).toBe('passed');
|
|
180
236
|
});
|
|
237
|
+
it('marks the final run state as completed', async () => {
|
|
238
|
+
const session = makeSession();
|
|
239
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
240
|
+
expect(session.setRunState).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
241
|
+
completed: true,
|
|
242
|
+
status: 'passed',
|
|
243
|
+
}));
|
|
244
|
+
});
|
|
181
245
|
it('attaches buffered client logs to the Jest result', async () => {
|
|
182
246
|
const clientLogs = [{ message: 'Loaded screen', origin: '', type: 'warn' }];
|
|
183
247
|
const session = makeSession({
|
|
@@ -257,6 +321,56 @@ describe('executeRun', () => {
|
|
|
257
321
|
['test-file-success', expect.anything(), expect.anything()],
|
|
258
322
|
]);
|
|
259
323
|
});
|
|
324
|
+
it('includes pending promise diagnostics in live test-case failures', async () => {
|
|
325
|
+
let testRunnerListener;
|
|
326
|
+
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
327
|
+
const session = makeSession({
|
|
328
|
+
onTestRunnerEvent: vi.fn((listener) => {
|
|
329
|
+
testRunnerListener = listener;
|
|
330
|
+
return () => undefined;
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
334
|
+
testRunnerListener?.({
|
|
335
|
+
type: 'test-finished',
|
|
336
|
+
file: 'example.ts',
|
|
337
|
+
suite: 'suite',
|
|
338
|
+
name: 'hangs',
|
|
339
|
+
ancestorTitles: ['suite'],
|
|
340
|
+
fullName: 'suite hangs',
|
|
341
|
+
startedAt: 100,
|
|
342
|
+
duration: 50,
|
|
343
|
+
status: 'failed',
|
|
344
|
+
error: {
|
|
345
|
+
name: 'TestCaseTimeoutError',
|
|
346
|
+
message: 'Test timed out after 50ms: suite hangs',
|
|
347
|
+
diagnostics: {
|
|
348
|
+
pendingPromises: {
|
|
349
|
+
total: 1,
|
|
350
|
+
items: [
|
|
351
|
+
{
|
|
352
|
+
id: 7,
|
|
353
|
+
createdAt: 110,
|
|
354
|
+
stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
return makeFileRunResult();
|
|
362
|
+
});
|
|
363
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
364
|
+
expect(emittedEvents).toContainEqual([
|
|
365
|
+
'test-case-result',
|
|
366
|
+
'example.ts',
|
|
367
|
+
expect.objectContaining({
|
|
368
|
+
failureMessages: [
|
|
369
|
+
expect.stringContaining('Pending promises at timeout: 1\n\nPromise #7, created 10ms after test start:'),
|
|
370
|
+
],
|
|
371
|
+
}),
|
|
372
|
+
]);
|
|
373
|
+
});
|
|
260
374
|
});
|
|
261
375
|
describe('runtime failures', () => {
|
|
262
376
|
it('passes StartupStallError to onFailure with an empty stack', async () => {
|
|
@@ -334,7 +448,7 @@ describe('executeRun', () => {
|
|
|
334
448
|
});
|
|
335
449
|
});
|
|
336
450
|
describe('resetEnvironmentBetweenTestFiles', () => {
|
|
337
|
-
it('calls
|
|
451
|
+
it('calls resetEnvironment before the second test but not the first', async () => {
|
|
338
452
|
const session = makeSession({
|
|
339
453
|
config: {
|
|
340
454
|
metroPort: 8081,
|
|
@@ -347,8 +461,139 @@ describe('executeRun', () => {
|
|
|
347
461
|
},
|
|
348
462
|
});
|
|
349
463
|
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
350
|
-
//
|
|
351
|
-
expect(session.
|
|
464
|
+
// resetEnvironment should be called for tests 2 and 3, not test 1.
|
|
465
|
+
expect(session.resetEnvironment).toHaveBeenCalledTimes(2);
|
|
466
|
+
expect(session.restartApp).not.toHaveBeenCalled();
|
|
467
|
+
});
|
|
468
|
+
it('calls resetEnvironment with the "runtime" strategy configured', async () => {
|
|
469
|
+
const session = makeSession({
|
|
470
|
+
config: {
|
|
471
|
+
metroPort: 8081,
|
|
472
|
+
resetEnvironmentBetweenTestFiles: 'runtime',
|
|
473
|
+
detectNativeCrashes: false,
|
|
474
|
+
runners: [
|
|
475
|
+
{ platformId: 'android', name: 'android' },
|
|
476
|
+
{ platformId: 'ios', name: 'ios' },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
481
|
+
expect(session.resetEnvironment).toHaveBeenCalledTimes(1);
|
|
482
|
+
expect(session.resetEnvironment).toHaveBeenCalledWith('/b.ts');
|
|
483
|
+
});
|
|
484
|
+
it('does not call resetEnvironment when resetEnvironmentBetweenTestFiles is false', async () => {
|
|
485
|
+
const session = makeSession({
|
|
486
|
+
config: {
|
|
487
|
+
metroPort: 8081,
|
|
488
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
489
|
+
detectNativeCrashes: false,
|
|
490
|
+
runners: [
|
|
491
|
+
{ platformId: 'android', name: 'android' },
|
|
492
|
+
{ platformId: 'ios', name: 'ios' },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
497
|
+
expect(session.resetEnvironment).not.toHaveBeenCalled();
|
|
498
|
+
});
|
|
499
|
+
it('restarts after a test case timeout before the next runnable file', async () => {
|
|
500
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
501
|
+
timedOutResult.tests = [
|
|
502
|
+
{
|
|
503
|
+
name: 'hangs',
|
|
504
|
+
status: 'failed',
|
|
505
|
+
duration: 10,
|
|
506
|
+
error: {
|
|
507
|
+
name: 'TestCaseTimeoutError',
|
|
508
|
+
message: 'Test timed out after 10ms: hangs',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
mockRunHarnessTestFile
|
|
513
|
+
.mockResolvedValueOnce(makeFileRunResult({
|
|
514
|
+
harnessResult: timedOutResult,
|
|
515
|
+
jestResult: makeJestResult({
|
|
516
|
+
numFailingTests: 1,
|
|
517
|
+
numPassingTests: 0,
|
|
518
|
+
}),
|
|
519
|
+
}))
|
|
520
|
+
.mockResolvedValueOnce(makeFileRunResult());
|
|
521
|
+
const session = makeSession({
|
|
522
|
+
config: {
|
|
523
|
+
metroPort: 8081,
|
|
524
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
525
|
+
detectNativeCrashes: false,
|
|
526
|
+
runners: [
|
|
527
|
+
{ platformId: 'android', name: 'android' },
|
|
528
|
+
{ platformId: 'ios', name: 'ios' },
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
533
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
534
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
535
|
+
});
|
|
536
|
+
it('restarts after a timeout in the last runnable file', async () => {
|
|
537
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
538
|
+
timedOutResult.tests = [
|
|
539
|
+
{
|
|
540
|
+
name: 'hangs',
|
|
541
|
+
status: 'failed',
|
|
542
|
+
duration: 10,
|
|
543
|
+
error: {
|
|
544
|
+
name: 'TestCaseTimeoutError',
|
|
545
|
+
message: 'Test timed out after 10ms: hangs',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
|
|
550
|
+
harnessResult: timedOutResult,
|
|
551
|
+
jestResult: makeJestResult({
|
|
552
|
+
numFailingTests: 1,
|
|
553
|
+
numPassingTests: 0,
|
|
554
|
+
}),
|
|
555
|
+
}));
|
|
556
|
+
const session = makeSession({
|
|
557
|
+
config: {
|
|
558
|
+
metroPort: 8081,
|
|
559
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
560
|
+
detectNativeCrashes: false,
|
|
561
|
+
runners: [
|
|
562
|
+
{ platformId: 'android', name: 'android' },
|
|
563
|
+
{ platformId: 'ios', name: 'ios' },
|
|
564
|
+
],
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig({ watch: true }));
|
|
568
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
569
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
570
|
+
});
|
|
571
|
+
it('restarts after a suite hook timeout', async () => {
|
|
572
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
573
|
+
timedOutResult.suites = [
|
|
574
|
+
{
|
|
575
|
+
name: 'suite',
|
|
576
|
+
tests: [],
|
|
577
|
+
suites: [],
|
|
578
|
+
status: 'failed',
|
|
579
|
+
duration: 10,
|
|
580
|
+
error: {
|
|
581
|
+
name: 'SuiteHookTimeoutError',
|
|
582
|
+
message: 'beforeAll hook timed out after 10ms in suite: suite',
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
];
|
|
586
|
+
mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
|
|
587
|
+
harnessResult: timedOutResult,
|
|
588
|
+
jestResult: makeJestResult({
|
|
589
|
+
numFailingTests: 1,
|
|
590
|
+
numPassingTests: 0,
|
|
591
|
+
}),
|
|
592
|
+
}));
|
|
593
|
+
const session = makeSession();
|
|
594
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
595
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
352
597
|
});
|
|
353
598
|
});
|
|
354
599
|
describe('platform-specific test files', () => {
|
|
@@ -387,6 +632,7 @@ describe('executeRun', () => {
|
|
|
387
632
|
makeTest('/project/kotlin.android.harness.ts'),
|
|
388
633
|
], makeWatcher(), vi.fn(), makeGlobalConfig());
|
|
389
634
|
expect(session.restartApp).not.toHaveBeenCalled();
|
|
635
|
+
expect(session.resetEnvironment).not.toHaveBeenCalled();
|
|
390
636
|
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
|
|
391
637
|
});
|
|
392
638
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-harness-error.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/format-harness-error.test.ts"],"names":[],"mappings":""}
|