@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
|
@@ -1,250 +1,233 @@
|
|
|
1
|
-
import { afterEach,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const createCrashMonitorMock = vi.hoisted(() => vi.fn());
|
|
11
|
-
vi.mock('@react-native-harness/plugins', async () => {
|
|
12
|
-
const actual = await vi.importActual('@react-native-harness/plugins');
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
createHarnessPluginManager: vi.fn(() => ({
|
|
16
|
-
callHook: pluginManagerMocks.callHook,
|
|
17
|
-
})),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
vi.mock('@react-native-harness/config', async () => {
|
|
21
|
-
const actual = await vi.importActual('@react-native-harness/config');
|
|
22
|
-
return {
|
|
23
|
-
...actual,
|
|
24
|
-
getConfig: getConfigMock,
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
vi.mock('../metro-port.js', async () => {
|
|
28
|
-
const actual = await vi.importActual('../metro-port.js');
|
|
29
|
-
return {
|
|
30
|
-
...actual,
|
|
31
|
-
resolveHarnessMetroPort: resolveHarnessMetroPortMock,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
35
|
-
const actual = await vi.importActual('@react-native-harness/bundler-metro');
|
|
36
|
-
return {
|
|
37
|
-
...actual,
|
|
38
|
-
getMetroInstance: getMetroInstanceMock,
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
vi.mock('@react-native-harness/bridge/server', async () => {
|
|
42
|
-
const actual = await vi.importActual('@react-native-harness/bridge/server');
|
|
43
|
-
return {
|
|
44
|
-
...actual,
|
|
45
|
-
createHarnessBridge: createHarnessBridgeMock,
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
vi.mock('../crash-monitor.js', async () => {
|
|
49
|
-
const actual = await vi.importActual('../crash-monitor.js');
|
|
50
|
-
return {
|
|
51
|
-
...actual,
|
|
52
|
-
createCrashMonitor: createCrashMonitorMock,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
const createAppMonitor = () => ({
|
|
56
|
-
start: vi.fn(async () => undefined),
|
|
57
|
-
stop: vi.fn(async () => undefined),
|
|
58
|
-
dispose: vi.fn(async () => undefined),
|
|
59
|
-
addListener: vi.fn(),
|
|
60
|
-
removeListener: vi.fn(),
|
|
61
|
-
});
|
|
62
|
-
const makeBridge = () => ({
|
|
63
|
-
ws: {},
|
|
64
|
-
connection: null,
|
|
65
|
-
on: vi.fn(),
|
|
66
|
-
off: vi.fn(),
|
|
67
|
-
nextConnection: vi.fn(),
|
|
68
|
-
dispose: vi.fn(async () => undefined),
|
|
69
|
-
});
|
|
70
|
-
const makeMetroInstance = () => ({
|
|
71
|
-
events: {
|
|
72
|
-
addListener: vi.fn(),
|
|
73
|
-
removeListener: vi.fn(),
|
|
74
|
-
},
|
|
75
|
-
waitUntilHealthy: vi.fn(async () => 'packager-status:running'),
|
|
76
|
-
prewarm: vi.fn(async () => false),
|
|
77
|
-
dispose: vi.fn(async () => undefined),
|
|
78
|
-
});
|
|
79
|
-
const makeLockManager = () => ({
|
|
80
|
-
acquire: vi.fn(async () => ({
|
|
81
|
-
release: vi.fn(async () => undefined),
|
|
82
|
-
})),
|
|
83
|
-
});
|
|
84
|
-
const makeCrashMonitor = () => ({
|
|
85
|
-
watch: vi.fn(),
|
|
86
|
-
isAlive: vi.fn(() => false),
|
|
87
|
-
stop: vi.fn(async () => undefined),
|
|
88
|
-
start: vi.fn(async () => undefined),
|
|
89
|
-
reset: vi.fn(),
|
|
90
|
-
dispose: vi.fn(async () => undefined),
|
|
91
|
-
});
|
|
92
|
-
const makeDeviceState = () => ({
|
|
93
|
-
permissions: {
|
|
94
|
-
apply: vi.fn(async () => ({ mutation: null })),
|
|
95
|
-
revert: vi.fn(),
|
|
96
|
-
resetOutstanding: vi.fn(async () => undefined),
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getSignalExitCodeForRunState, waitForBridgeDisconnectOrTimeout, waitForNextConnected, waitForStartupCrash, withPlatformReadyTimeout, } from '../harness-session.js';
|
|
3
|
+
import { PlatformReadyTimeoutError } from '../errors.js';
|
|
4
|
+
const createConnection = () => ({
|
|
5
|
+
device: {
|
|
6
|
+
platform: 'ios',
|
|
7
|
+
manufacturer: 'Apple',
|
|
8
|
+
model: 'iPhone',
|
|
9
|
+
osVersion: '18.0',
|
|
97
10
|
},
|
|
11
|
+
runTests: vi.fn(),
|
|
12
|
+
resetEnvironment: vi.fn(),
|
|
98
13
|
});
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
name: 'ios',
|
|
120
|
-
platformId: 'ios',
|
|
121
|
-
runner: './__tests__/fixtures/mock-platform-runner.ts',
|
|
122
|
-
config: {},
|
|
14
|
+
const createBridge = (connection) => {
|
|
15
|
+
let currentConnection = connection;
|
|
16
|
+
let disconnectedListener = null;
|
|
17
|
+
return {
|
|
18
|
+
get connection() {
|
|
19
|
+
return currentConnection;
|
|
20
|
+
},
|
|
21
|
+
on: vi.fn((event, listener) => {
|
|
22
|
+
if (event === 'disconnected') {
|
|
23
|
+
disconnectedListener = listener;
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
off: vi.fn((event, listener) => {
|
|
27
|
+
if (event === 'disconnected' && disconnectedListener === listener) {
|
|
28
|
+
disconnectedListener = null;
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
disconnect: () => {
|
|
32
|
+
currentConnection = null;
|
|
33
|
+
disconnectedListener?.();
|
|
123
34
|
},
|
|
124
|
-
|
|
125
|
-
defaultRunner: 'ios',
|
|
126
|
-
metroPort: 8081,
|
|
127
|
-
bridgeTimeout: 1000,
|
|
128
|
-
platformReadyTimeout: 1000,
|
|
129
|
-
bundleStartTimeout: 1000,
|
|
130
|
-
maxAppRestarts: 1,
|
|
131
|
-
resetEnvironmentBetweenTestFiles: true,
|
|
132
|
-
detectNativeCrashes: false,
|
|
133
|
-
forwardClientLogs: false,
|
|
134
|
-
permissions: false,
|
|
135
|
-
plugins: [],
|
|
136
|
-
unstable__skipAlreadyIncludedModules: false,
|
|
137
|
-
unstable__enableMetroCache: false,
|
|
138
|
-
disableViewFlattening: false,
|
|
35
|
+
};
|
|
139
36
|
};
|
|
140
|
-
describe('
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
metroInstance = makeMetroInstance();
|
|
149
|
-
crashMonitor = makeCrashMonitor();
|
|
150
|
-
lockManager = makeLockManager();
|
|
151
|
-
getConfigMock.mockResolvedValue({ config: harnessConfig });
|
|
152
|
-
resolveHarnessMetroPortMock.mockResolvedValue({
|
|
153
|
-
config: harnessConfig,
|
|
154
|
-
initialMetroPort: 8081,
|
|
155
|
-
didFallback: false,
|
|
156
|
-
metroPortLease: { release: vi.fn(async () => undefined) },
|
|
37
|
+
describe('waitForBridgeDisconnectOrTimeout', () => {
|
|
38
|
+
it('returns true when the bridge disconnects before the timeout', async () => {
|
|
39
|
+
const connection = createConnection();
|
|
40
|
+
const bridge = createBridge(connection);
|
|
41
|
+
const waitPromise = waitForBridgeDisconnectOrTimeout({
|
|
42
|
+
bridge,
|
|
43
|
+
connection,
|
|
44
|
+
timeoutMs: 50,
|
|
157
45
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
createCrashMonitorMock.mockReturnValue(crashMonitor);
|
|
46
|
+
bridge.disconnect();
|
|
47
|
+
await expect(waitPromise).resolves.toBe(true);
|
|
161
48
|
});
|
|
162
|
-
|
|
163
|
-
|
|
49
|
+
it('returns false when the bridge stays connected through the timeout', async () => {
|
|
50
|
+
const connection = createConnection();
|
|
51
|
+
const bridge = createBridge(connection);
|
|
52
|
+
await expect(waitForBridgeDisconnectOrTimeout({
|
|
53
|
+
bridge,
|
|
54
|
+
connection,
|
|
55
|
+
timeoutMs: 10,
|
|
56
|
+
})).resolves.toBe(false);
|
|
164
57
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
});
|
|
179
|
-
platformRunner.stopApp = vi.fn(async () => {
|
|
180
|
-
callOrder.push('stopApp');
|
|
181
|
-
});
|
|
182
|
-
await session.restartApp('/test/example.ts');
|
|
183
|
-
expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
|
|
184
|
-
expect(deviceState.permissions.apply).not.toHaveBeenCalled();
|
|
185
|
-
expect(callOrder).toEqual(['resetOutstanding', 'stopApp']);
|
|
186
|
-
await session.dispose();
|
|
187
|
-
});
|
|
188
|
-
it('reapplies configured permissions after cleanup before restart between test files', async () => {
|
|
189
|
-
const deviceState = makeDeviceState();
|
|
190
|
-
const platformRunner = makePlatformRunner({
|
|
191
|
-
deviceState,
|
|
192
|
-
isAppRunning: vi.fn(async () => true),
|
|
193
|
-
});
|
|
194
|
-
bridge.connection = {};
|
|
195
|
-
crashMonitor.isAlive.mockReturnValue(true);
|
|
196
|
-
globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
|
|
197
|
-
const permissionsEnabledConfig = {
|
|
198
|
-
...harnessConfig,
|
|
199
|
-
permissions: true,
|
|
58
|
+
});
|
|
59
|
+
describe('waitForNextConnected', () => {
|
|
60
|
+
const createConnectableBridge = () => {
|
|
61
|
+
let listener = null;
|
|
62
|
+
const bridge = {
|
|
63
|
+
on: vi.fn((event, l) => {
|
|
64
|
+
if (event === 'connected')
|
|
65
|
+
listener = l;
|
|
66
|
+
}),
|
|
67
|
+
off: vi.fn((event, l) => {
|
|
68
|
+
if (event === 'connected' && listener === l)
|
|
69
|
+
listener = null;
|
|
70
|
+
}),
|
|
200
71
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
72
|
+
return { bridge, emitConnected: () => listener?.() };
|
|
73
|
+
};
|
|
74
|
+
it('resolves when the bridge emits a connected event', async () => {
|
|
75
|
+
const { bridge, emitConnected } = createConnectableBridge();
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const waitPromise = waitForNextConnected({ bridge, signal: controller.signal });
|
|
78
|
+
emitConnected();
|
|
79
|
+
await expect(waitPromise).resolves.toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
it('rejects when the signal aborts before a connected event', async () => {
|
|
82
|
+
const { bridge } = createConnectableBridge();
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const waitPromise = waitForNextConnected({ bridge, signal: controller.signal });
|
|
85
|
+
controller.abort(new DOMException('Aborted', 'AbortError'));
|
|
86
|
+
await expect(waitPromise).rejects.toThrow('Aborted');
|
|
87
|
+
});
|
|
88
|
+
it('rejects immediately when the signal is already aborted', async () => {
|
|
89
|
+
const { bridge } = createConnectableBridge();
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
controller.abort(new DOMException('Aborted', 'AbortError'));
|
|
92
|
+
await expect(waitForNextConnected({ bridge, signal: controller.signal })).rejects.toThrow('Aborted');
|
|
93
|
+
});
|
|
94
|
+
it('unsubscribes the connected listener once settled', async () => {
|
|
95
|
+
const { bridge, emitConnected } = createConnectableBridge();
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const waitPromise = waitForNextConnected({ bridge, signal: controller.signal });
|
|
98
|
+
emitConnected();
|
|
99
|
+
await waitPromise;
|
|
100
|
+
expect(bridge.off).toHaveBeenCalledOnce();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('waitForStartupCrash', () => {
|
|
104
|
+
it('does not install a startup crash watch when native crash detection is disabled', async () => {
|
|
105
|
+
const watch = vi.fn();
|
|
106
|
+
const crashMonitor = {
|
|
107
|
+
watch,
|
|
108
|
+
};
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const waitPromise = waitForStartupCrash({
|
|
111
|
+
crashMonitor,
|
|
112
|
+
detectNativeCrashes: false,
|
|
113
|
+
testFilePath: '/test.harness.ts',
|
|
114
|
+
signal: controller.signal,
|
|
213
115
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
116
|
+
controller.abort(new DOMException('Aborted', 'AbortError'));
|
|
117
|
+
await expect(waitPromise).rejects.toThrow('Aborted');
|
|
118
|
+
expect(watch).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
it('removes its abort listener on the session signal once the crash watch wins', async () => {
|
|
121
|
+
let rejectWatch;
|
|
122
|
+
const cancel = vi.fn();
|
|
123
|
+
const watch = vi.fn(() => ({
|
|
124
|
+
promise: new Promise((_, reject) => {
|
|
125
|
+
rejectWatch = reject;
|
|
126
|
+
}),
|
|
127
|
+
cancel,
|
|
128
|
+
}));
|
|
129
|
+
const crashMonitor = { watch };
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const addSpy = vi.spyOn(controller.signal, 'addEventListener');
|
|
132
|
+
const removeSpy = vi.spyOn(controller.signal, 'removeEventListener');
|
|
133
|
+
const waitPromise = waitForStartupCrash({
|
|
134
|
+
crashMonitor,
|
|
135
|
+
detectNativeCrashes: true,
|
|
136
|
+
testFilePath: '/test.harness.ts',
|
|
137
|
+
signal: controller.signal,
|
|
217
138
|
});
|
|
218
|
-
|
|
219
|
-
|
|
139
|
+
// The crash watch wins the race, not the session signal.
|
|
140
|
+
rejectWatch(new Error('native crash detected'));
|
|
141
|
+
await expect(waitPromise).rejects.toThrow('native crash detected');
|
|
142
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
143
|
+
// Regression: forgetting to cancel the losing waitForAbort() left its
|
|
144
|
+
// listener attached to this session-lifetime signal for the rest of
|
|
145
|
+
// the session — one leaked listener per test file.
|
|
146
|
+
expect(addSpy).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(removeSpy).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('withPlatformReadyTimeout', () => {
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
vi.useRealTimers();
|
|
153
|
+
});
|
|
154
|
+
it('passes the real session signal to work, unmodified by the timeout', async () => {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const work = vi.fn(async (signal) => {
|
|
157
|
+
expect(signal).toBe(controller.signal);
|
|
158
|
+
return 'ready';
|
|
220
159
|
});
|
|
221
|
-
await
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
160
|
+
await expect(withPlatformReadyTimeout({
|
|
161
|
+
timeout: 1_000,
|
|
162
|
+
signal: controller.signal,
|
|
163
|
+
work,
|
|
164
|
+
})).resolves.toBe('ready');
|
|
165
|
+
expect(work).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
it('throws PlatformReadyTimeoutError on timeout without aborting the session signal', async () => {
|
|
168
|
+
vi.useFakeTimers();
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
// Never resolves within the test; simulates a hung platform init.
|
|
171
|
+
const work = () => new Promise(() => undefined);
|
|
172
|
+
const resultPromise = withPlatformReadyTimeout({
|
|
173
|
+
timeout: 1_000,
|
|
174
|
+
signal: controller.signal,
|
|
175
|
+
work,
|
|
226
176
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const callOrder = [];
|
|
237
|
-
const platformRunner = makePlatformRunner({ deviceState });
|
|
238
|
-
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
239
|
-
callOrder.push('resetOutstanding');
|
|
177
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
178
|
+
await expect(resultPromise).rejects.toBeInstanceOf(PlatformReadyTimeoutError);
|
|
179
|
+
expect(controller.signal.aborted).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
it('propagates a genuine abort from the session signal as-is', async () => {
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const abortReason = new DOMException('Aborted', 'AbortError');
|
|
184
|
+
const work = (signal) => new Promise((_, reject) => {
|
|
185
|
+
signal.addEventListener('abort', () => reject(signal.reason));
|
|
240
186
|
});
|
|
241
|
-
|
|
242
|
-
|
|
187
|
+
const resultPromise = withPlatformReadyTimeout({
|
|
188
|
+
timeout: 1_000,
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
work,
|
|
243
191
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
192
|
+
controller.abort(abortReason);
|
|
193
|
+
await expect(resultPromise).rejects.toBe(abortReason);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('getSignalExitCodeForRunState', () => {
|
|
197
|
+
const createRunState = (overrides = {}) => ({
|
|
198
|
+
completed: true,
|
|
199
|
+
coverageEnabled: false,
|
|
200
|
+
runId: 'run-1',
|
|
201
|
+
startTime: 0,
|
|
202
|
+
status: 'passed',
|
|
203
|
+
summary: {
|
|
204
|
+
failed: 0,
|
|
205
|
+
passed: 1,
|
|
206
|
+
skipped: 0,
|
|
207
|
+
todo: 0,
|
|
208
|
+
},
|
|
209
|
+
testFiles: ['test.harness.ts'],
|
|
210
|
+
watchMode: false,
|
|
211
|
+
...overrides,
|
|
212
|
+
});
|
|
213
|
+
it('preserves a successful exit after a completed passing run', () => {
|
|
214
|
+
expect(getSignalExitCodeForRunState(createRunState())).toBe(0);
|
|
215
|
+
});
|
|
216
|
+
it('fails when the run has not completed yet', () => {
|
|
217
|
+
expect(getSignalExitCodeForRunState(createRunState({ completed: false }))).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
it('fails when the completed run has failures', () => {
|
|
220
|
+
expect(getSignalExitCodeForRunState(createRunState({
|
|
221
|
+
status: 'failed',
|
|
222
|
+
summary: {
|
|
223
|
+
failed: 1,
|
|
224
|
+
passed: 0,
|
|
225
|
+
skipped: 0,
|
|
226
|
+
todo: 0,
|
|
227
|
+
},
|
|
228
|
+
}))).toBe(1);
|
|
229
|
+
});
|
|
230
|
+
it('fails when no run state is available', () => {
|
|
231
|
+
expect(getSignalExitCodeForRunState(null)).toBe(1);
|
|
249
232
|
});
|
|
250
233
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { HarnessError } from '@react-native-harness/tools';
|
|
2
|
+
import { createDiagnostics, HarnessError } from '@react-native-harness/tools';
|
|
3
3
|
import JestHarness from '../index.js';
|
|
4
4
|
const resolveUndefined = async () => undefined;
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -8,6 +8,8 @@ const resolveUndefined = async () => undefined;
|
|
|
8
8
|
const mockSession = {
|
|
9
9
|
config: { metroPort: 8081 },
|
|
10
10
|
context: {},
|
|
11
|
+
diagnostics: createDiagnostics({ enabled: false }),
|
|
12
|
+
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
11
13
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
12
14
|
runTestFile: vi.fn(async () => ({
|
|
13
15
|
name: '',
|
|
@@ -17,6 +19,7 @@ const mockSession = {
|
|
|
17
19
|
duration: 0,
|
|
18
20
|
})),
|
|
19
21
|
restartApp: vi.fn(resolveUndefined),
|
|
22
|
+
resetEnvironment: vi.fn(resolveUndefined),
|
|
20
23
|
resetCrashState: vi.fn(),
|
|
21
24
|
flushClientLogs: vi.fn(() => []),
|
|
22
25
|
callHook: vi.fn(resolveUndefined),
|
|
@@ -41,40 +44,88 @@ const makeTest = () => ({
|
|
|
41
44
|
context: { config: {} },
|
|
42
45
|
});
|
|
43
46
|
const makeGlobalConfig = (overrides = {}) => ({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides });
|
|
47
|
+
const typedMockExecuteRun = mockExecuteRun;
|
|
48
|
+
const makeTestCaseStartPayload = () => [
|
|
49
|
+
'/test/example.ts',
|
|
50
|
+
{
|
|
51
|
+
ancestorTitles: [],
|
|
52
|
+
fullName: 'suite case',
|
|
53
|
+
mode: undefined,
|
|
54
|
+
title: 'case',
|
|
55
|
+
startedAt: 1,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
const makeTestCaseResultPayload = () => [
|
|
59
|
+
'/test/example.ts',
|
|
60
|
+
{
|
|
61
|
+
ancestorTitles: [],
|
|
62
|
+
duration: 1,
|
|
63
|
+
failureDetails: [],
|
|
64
|
+
failureMessages: [],
|
|
65
|
+
fullName: 'suite case',
|
|
66
|
+
numPassingAsserts: 1,
|
|
67
|
+
startedAt: 1,
|
|
68
|
+
status: 'passed',
|
|
69
|
+
title: 'case',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
44
72
|
// ---------------------------------------------------------------------------
|
|
45
73
|
// Tests
|
|
46
74
|
// ---------------------------------------------------------------------------
|
|
47
75
|
beforeEach(() => {
|
|
48
76
|
vi.clearAllMocks();
|
|
49
77
|
mockSession.dispose.mockResolvedValue(undefined);
|
|
50
|
-
|
|
78
|
+
typedMockExecuteRun.mockResolvedValue(undefined);
|
|
51
79
|
mockCreateHarnessSession.mockResolvedValue(mockSession);
|
|
52
80
|
});
|
|
53
81
|
describe('JestHarness', () => {
|
|
54
82
|
describe('session lifecycle', () => {
|
|
55
83
|
it('creates a session on the first run', async () => {
|
|
56
84
|
const runner = new JestHarness(makeGlobalConfig());
|
|
57
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
85
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
58
86
|
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
59
87
|
});
|
|
60
88
|
it('reuses the session across runs in watch mode', async () => {
|
|
61
89
|
const runner = new JestHarness(makeGlobalConfig({ watch: true }));
|
|
62
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
63
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
90
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
91
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
64
92
|
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
65
93
|
expect(mockSession.dispose).not.toHaveBeenCalled();
|
|
66
94
|
});
|
|
67
95
|
it('disposes the session after each run in normal mode', async () => {
|
|
68
96
|
const runner = new JestHarness(makeGlobalConfig());
|
|
69
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
97
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
70
98
|
expect(mockSession.dispose).toHaveBeenCalledOnce();
|
|
71
99
|
});
|
|
72
100
|
it('creates a fresh session for each run in normal mode', async () => {
|
|
73
101
|
const runner = new JestHarness(makeGlobalConfig());
|
|
74
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
75
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
102
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
103
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
76
104
|
expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
|
|
77
105
|
});
|
|
106
|
+
it('emits registered Jest events', async () => {
|
|
107
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
108
|
+
const listener = vi.fn();
|
|
109
|
+
const eventPayload = makeTestCaseStartPayload();
|
|
110
|
+
runner.on('test-case-start', listener);
|
|
111
|
+
typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
|
|
112
|
+
await emitEvent('test-case-start', ...eventPayload);
|
|
113
|
+
});
|
|
114
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
115
|
+
expect(listener).toHaveBeenCalledWith(eventPayload);
|
|
116
|
+
});
|
|
117
|
+
it('stops emitting after listener unsubscribe', async () => {
|
|
118
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
119
|
+
const listener = vi.fn();
|
|
120
|
+
const unsubscribe = runner.on('test-case-result', listener);
|
|
121
|
+
const eventPayload = makeTestCaseResultPayload();
|
|
122
|
+
unsubscribe();
|
|
123
|
+
typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
|
|
124
|
+
await emitEvent('test-case-result', ...eventPayload);
|
|
125
|
+
});
|
|
126
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
127
|
+
expect(listener).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
78
129
|
});
|
|
79
130
|
describe('error handling', () => {
|
|
80
131
|
it('converts HarnessError into a formatted string before throwing', async () => {
|
|
@@ -83,17 +134,17 @@ describe('JestHarness', () => {
|
|
|
83
134
|
}
|
|
84
135
|
mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
|
|
85
136
|
const runner = new JestHarness(makeGlobalConfig());
|
|
86
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
137
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBeTypeOf('string');
|
|
87
138
|
});
|
|
88
139
|
it('propagates non-HarnessError exceptions as-is', async () => {
|
|
89
140
|
const cause = new TypeError('unexpected');
|
|
90
141
|
mockCreateHarnessSession.mockRejectedValue(cause);
|
|
91
142
|
const runner = new JestHarness(makeGlobalConfig());
|
|
92
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
143
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBe(cause);
|
|
93
144
|
});
|
|
94
145
|
it('throws when called without serial flag', async () => {
|
|
95
146
|
const runner = new JestHarness(makeGlobalConfig());
|
|
96
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
147
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), { serial: false })).rejects.toThrow('Parallel test running is not supported');
|
|
97
148
|
});
|
|
98
149
|
});
|
|
99
150
|
});
|
|
@@ -16,11 +16,13 @@ const createConfig = (overrides = {}) => ({
|
|
|
16
16
|
disableViewFlattening: false,
|
|
17
17
|
entryPoint: 'index.js',
|
|
18
18
|
forwardClientLogs: false,
|
|
19
|
+
diagnostics: false,
|
|
19
20
|
maxAppRestarts: 2,
|
|
20
21
|
metroPort: 8081,
|
|
21
22
|
platformReadyTimeout: 300_000,
|
|
22
23
|
resetEnvironmentBetweenTestFiles: true,
|
|
23
24
|
runners: [],
|
|
25
|
+
testTimeout: 5_000,
|
|
24
26
|
unstable__enableMetroCache: false,
|
|
25
27
|
unstable__skipAlreadyIncludedModules: false,
|
|
26
28
|
...overrides,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/run.test.ts"],"names":[],"mappings":""}
|