@react-native-harness/jest 1.2.0-rc.1 → 1.3.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.d.ts +2 -0
- package/dist/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/__tests__/bridge.test.js +142 -0
- package/dist/__tests__/client-log-handler.test.js +116 -72
- package/dist/__tests__/crash-monitor.test.d.ts +2 -0
- package/dist/__tests__/crash-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/crash-monitor.test.js +212 -0
- package/dist/__tests__/execute-run.test.d.ts +2 -0
- package/dist/__tests__/execute-run.test.d.ts.map +1 -0
- package/dist/__tests__/execute-run.test.js +229 -0
- 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 +250 -0
- package/dist/__tests__/hook-queue.test.d.ts +2 -0
- package/dist/__tests__/hook-queue.test.d.ts.map +1 -0
- package/dist/__tests__/hook-queue.test.js +56 -0
- package/dist/__tests__/index.test.js +94 -37
- package/dist/__tests__/resource-lock.test.js +24 -24
- package/dist/client-log-handler.d.ts +10 -11
- package/dist/client-log-handler.d.ts.map +1 -1
- package/dist/client-log-handler.js +36 -58
- package/dist/crash-monitor.d.ts +17 -10
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +164 -63
- package/dist/execute-run.d.ts +4 -0
- package/dist/execute-run.d.ts.map +1 -0
- package/dist/execute-run.js +151 -0
- package/dist/harness-session.d.ts +34 -0
- package/dist/harness-session.d.ts.map +1 -0
- package/dist/harness-session.js +523 -0
- package/dist/hook-queue.d.ts +6 -0
- package/dist/hook-queue.d.ts.map +1 -0
- package/dist/hook-queue.js +18 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -230
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/resource-lock.d.ts +0 -1
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +1 -1
- package/dist/run.d.ts +2 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -3
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -8
- package/src/__tests__/bridge.test.ts +184 -0
- package/src/__tests__/client-log-handler.test.ts +117 -81
- package/src/__tests__/crash-monitor.test.ts +288 -0
- package/src/__tests__/execute-run.test.ts +325 -0
- package/src/__tests__/hook-queue.test.ts +77 -0
- package/src/__tests__/index.test.ts +135 -55
- package/src/__tests__/resource-lock.test.ts +29 -42
- package/src/client-log-handler.ts +48 -64
- package/src/crash-monitor.ts +248 -0
- package/src/execute-run.ts +199 -0
- package/src/harness-session.ts +734 -0
- package/src/hook-queue.ts +26 -0
- package/src/index.ts +17 -330
- package/src/logs.ts +4 -0
- package/src/resource-lock.ts +1 -1
- package/src/run.ts +5 -5
- package/tsconfig.tsbuildinfo +1 -0
- package/src/__tests__/crash-supervisor.test.ts +0 -200
- package/src/__tests__/harness-cache.test.ts +0 -74
- package/src/__tests__/harness.test.ts +0 -923
- package/src/crash-supervisor.ts +0 -321
- package/src/env.d.ts +0 -9
- package/src/harness.ts +0 -870
- package/src/setup.ts +0 -127
- package/src/teardown.ts +0 -14
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createHarnessSession } from '../harness-session.js';
|
|
3
|
+
const pluginManagerMocks = vi.hoisted(() => ({
|
|
4
|
+
callHook: vi.fn(async () => undefined),
|
|
5
|
+
}));
|
|
6
|
+
const getConfigMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
const resolveHarnessMetroPortMock = vi.hoisted(() => vi.fn());
|
|
8
|
+
const getMetroInstanceMock = vi.hoisted(() => vi.fn());
|
|
9
|
+
const createHarnessBridgeMock = vi.hoisted(() => vi.fn());
|
|
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),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const makePlatformRunner = (overrides = {}) => ({
|
|
100
|
+
startApp: vi.fn(async () => undefined),
|
|
101
|
+
restartApp: vi.fn(async () => undefined),
|
|
102
|
+
stopApp: vi.fn(async () => undefined),
|
|
103
|
+
dispose: vi.fn(async () => undefined),
|
|
104
|
+
isAppRunning: vi.fn(async () => false),
|
|
105
|
+
createAppMonitor: vi.fn(() => createAppMonitor()),
|
|
106
|
+
...overrides,
|
|
107
|
+
});
|
|
108
|
+
const globalConfig = {
|
|
109
|
+
rootDir: '/project',
|
|
110
|
+
watch: false,
|
|
111
|
+
watchAll: false,
|
|
112
|
+
collectCoverage: false,
|
|
113
|
+
};
|
|
114
|
+
const harnessConfig = {
|
|
115
|
+
entryPoint: 'index.js',
|
|
116
|
+
appRegistryComponentName: 'App',
|
|
117
|
+
runners: [
|
|
118
|
+
{
|
|
119
|
+
name: 'ios',
|
|
120
|
+
platformId: 'ios',
|
|
121
|
+
runner: './__tests__/fixtures/mock-platform-runner.ts',
|
|
122
|
+
config: {},
|
|
123
|
+
},
|
|
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,
|
|
139
|
+
};
|
|
140
|
+
describe('createHarnessSession permission cleanup', () => {
|
|
141
|
+
let bridge;
|
|
142
|
+
let metroInstance;
|
|
143
|
+
let crashMonitor;
|
|
144
|
+
let lockManager;
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
vi.clearAllMocks();
|
|
147
|
+
bridge = makeBridge();
|
|
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) },
|
|
157
|
+
});
|
|
158
|
+
getMetroInstanceMock.mockResolvedValue(metroInstance);
|
|
159
|
+
createHarnessBridgeMock.mockResolvedValue(bridge);
|
|
160
|
+
createCrashMonitorMock.mockReturnValue(crashMonitor);
|
|
161
|
+
});
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
delete globalThis.__HARNESS_TEST_PLATFORM_RUNNER__;
|
|
164
|
+
});
|
|
165
|
+
it('resets outstanding permission mutations before restart between test files', async () => {
|
|
166
|
+
const deviceState = makeDeviceState();
|
|
167
|
+
const platformRunner = makePlatformRunner({
|
|
168
|
+
deviceState,
|
|
169
|
+
isAppRunning: vi.fn(async () => true),
|
|
170
|
+
});
|
|
171
|
+
bridge.connection = {};
|
|
172
|
+
crashMonitor.isAlive.mockReturnValue(true);
|
|
173
|
+
globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
|
|
174
|
+
const session = await createHarnessSession(globalConfig, { lockManager });
|
|
175
|
+
const callOrder = [];
|
|
176
|
+
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
177
|
+
callOrder.push('resetOutstanding');
|
|
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,
|
|
200
|
+
};
|
|
201
|
+
getConfigMock.mockResolvedValue({ config: permissionsEnabledConfig });
|
|
202
|
+
resolveHarnessMetroPortMock.mockResolvedValue({
|
|
203
|
+
config: permissionsEnabledConfig,
|
|
204
|
+
initialMetroPort: 8081,
|
|
205
|
+
didFallback: false,
|
|
206
|
+
metroPortLease: { release: vi.fn(async () => undefined) },
|
|
207
|
+
});
|
|
208
|
+
const session = await createHarnessSession(globalConfig, { lockManager });
|
|
209
|
+
const callOrder = [];
|
|
210
|
+
deviceState.permissions.apply.mockClear();
|
|
211
|
+
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
212
|
+
callOrder.push('resetOutstanding');
|
|
213
|
+
});
|
|
214
|
+
deviceState.permissions.apply.mockImplementation(async () => {
|
|
215
|
+
callOrder.push('applyConfiguredPermissions');
|
|
216
|
+
return { mutation: null };
|
|
217
|
+
});
|
|
218
|
+
platformRunner.stopApp = vi.fn(async () => {
|
|
219
|
+
callOrder.push('stopApp');
|
|
220
|
+
});
|
|
221
|
+
await session.restartApp('/test/example.ts');
|
|
222
|
+
expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
|
|
223
|
+
expect(deviceState.permissions.apply).toHaveBeenCalledWith({
|
|
224
|
+
kind: 'permission-all',
|
|
225
|
+
decision: 'grant',
|
|
226
|
+
});
|
|
227
|
+
expect(callOrder).toEqual([
|
|
228
|
+
'resetOutstanding',
|
|
229
|
+
'applyConfiguredPermissions',
|
|
230
|
+
'stopApp',
|
|
231
|
+
]);
|
|
232
|
+
await session.dispose();
|
|
233
|
+
});
|
|
234
|
+
it('resets outstanding permission mutations during dispose', async () => {
|
|
235
|
+
const deviceState = makeDeviceState();
|
|
236
|
+
const callOrder = [];
|
|
237
|
+
const platformRunner = makePlatformRunner({ deviceState });
|
|
238
|
+
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
239
|
+
callOrder.push('resetOutstanding');
|
|
240
|
+
});
|
|
241
|
+
platformRunner.dispose = vi.fn(async () => {
|
|
242
|
+
callOrder.push('dispose');
|
|
243
|
+
});
|
|
244
|
+
globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
|
|
245
|
+
const session = await createHarnessSession(globalConfig, { lockManager });
|
|
246
|
+
await session.dispose();
|
|
247
|
+
expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
|
|
248
|
+
expect(callOrder).toEqual(['resetOutstanding', 'dispose']);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-queue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hook-queue.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createHookQueue } from '../hook-queue.js';
|
|
3
|
+
describe('createHookQueue', () => {
|
|
4
|
+
it('executes scheduled work in FIFO order', async () => {
|
|
5
|
+
const queue = createHookQueue();
|
|
6
|
+
const order = [];
|
|
7
|
+
queue.schedule(async () => { order.push(1); });
|
|
8
|
+
queue.schedule(async () => { order.push(2); });
|
|
9
|
+
queue.schedule(async () => { order.push(3); });
|
|
10
|
+
await queue.drain();
|
|
11
|
+
expect(order).toEqual([1, 2, 3]);
|
|
12
|
+
});
|
|
13
|
+
it('drain awaits async work before resolving', async () => {
|
|
14
|
+
const queue = createHookQueue();
|
|
15
|
+
let resolved = false;
|
|
16
|
+
queue.schedule(async () => {
|
|
17
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
18
|
+
resolved = true;
|
|
19
|
+
});
|
|
20
|
+
expect(resolved).toBe(false);
|
|
21
|
+
await queue.drain();
|
|
22
|
+
expect(resolved).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('drain resolves immediately when nothing is scheduled', async () => {
|
|
25
|
+
const queue = createHookQueue();
|
|
26
|
+
await expect(queue.drain()).resolves.toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
it('captures the first error and re-throws it on drain', async () => {
|
|
29
|
+
const queue = createHookQueue();
|
|
30
|
+
const err = new Error('hook failed');
|
|
31
|
+
queue.schedule(async () => { throw err; });
|
|
32
|
+
await expect(queue.drain()).rejects.toBe(err);
|
|
33
|
+
});
|
|
34
|
+
it('continues executing subsequent hooks after one throws', async () => {
|
|
35
|
+
const queue = createHookQueue();
|
|
36
|
+
const ran = [];
|
|
37
|
+
queue.schedule(async () => { throw new Error('first'); });
|
|
38
|
+
queue.schedule(async () => { ran.push('second'); });
|
|
39
|
+
queue.schedule(async () => { ran.push('third'); });
|
|
40
|
+
await expect(queue.drain()).rejects.toThrow('first');
|
|
41
|
+
expect(ran).toEqual(['second', 'third']);
|
|
42
|
+
});
|
|
43
|
+
it('error is cleared after drain re-throws it', async () => {
|
|
44
|
+
const queue = createHookQueue();
|
|
45
|
+
queue.schedule(async () => { throw new Error('once'); });
|
|
46
|
+
await expect(queue.drain()).rejects.toThrow('once');
|
|
47
|
+
// Second drain should see no accumulated error.
|
|
48
|
+
await expect(queue.drain()).resolves.toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
it('surfaces only the first error when multiple hooks throw', async () => {
|
|
51
|
+
const queue = createHookQueue();
|
|
52
|
+
queue.schedule(async () => { throw new Error('first'); });
|
|
53
|
+
queue.schedule(async () => { throw new Error('second'); });
|
|
54
|
+
await expect(queue.drain()).rejects.toThrow('first');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,42 +1,99 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { HarnessError } from '@react-native-harness/tools';
|
|
2
3
|
import JestHarness from '../index.js';
|
|
3
|
-
|
|
4
|
+
const resolveUndefined = async () => undefined;
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Module mocks
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const mockSession = {
|
|
9
|
+
config: { metroPort: 8081 },
|
|
10
|
+
context: {},
|
|
11
|
+
ensureAppReady: vi.fn(resolveUndefined),
|
|
12
|
+
runTestFile: vi.fn(async () => ({
|
|
13
|
+
name: '',
|
|
14
|
+
tests: [],
|
|
15
|
+
suites: [],
|
|
16
|
+
status: 'passed',
|
|
17
|
+
duration: 0,
|
|
18
|
+
})),
|
|
19
|
+
restartApp: vi.fn(resolveUndefined),
|
|
20
|
+
resetCrashState: vi.fn(),
|
|
21
|
+
flushClientLogs: vi.fn(() => []),
|
|
22
|
+
callHook: vi.fn(resolveUndefined),
|
|
23
|
+
setRunState: vi.fn(),
|
|
24
|
+
dispose: vi.fn(resolveUndefined),
|
|
25
|
+
};
|
|
26
|
+
const mockCreateHarnessSession = vi.hoisted(() => vi.fn(async () => mockSession));
|
|
27
|
+
const mockExecuteRun = vi.hoisted(() => vi.fn(async () => undefined));
|
|
28
|
+
vi.mock('../harness-session.js', () => ({
|
|
29
|
+
createHarnessSession: mockCreateHarnessSession,
|
|
30
|
+
}));
|
|
31
|
+
vi.mock('../execute-run.js', () => ({
|
|
32
|
+
executeRun: mockExecuteRun,
|
|
33
|
+
}));
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const makeOptions = () => ({ serial: true });
|
|
38
|
+
const makeWatcher = () => ({ isInterrupted: () => false });
|
|
39
|
+
const makeTest = () => ({
|
|
40
|
+
path: '/test/example.ts',
|
|
41
|
+
context: { config: {} },
|
|
42
|
+
});
|
|
43
|
+
const makeGlobalConfig = (overrides = {}) => ({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides });
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Tests
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
mockSession.dispose.mockResolvedValue(undefined);
|
|
50
|
+
mockExecuteRun.mockResolvedValue(undefined);
|
|
51
|
+
mockCreateHarnessSession.mockResolvedValue(mockSession);
|
|
52
|
+
});
|
|
4
53
|
describe('JestHarness', () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
describe('session lifecycle', () => {
|
|
55
|
+
it('creates a session on the first run', async () => {
|
|
56
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
57
|
+
await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
|
|
58
|
+
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
59
|
+
});
|
|
60
|
+
it('reuses the session across runs in watch mode', async () => {
|
|
61
|
+
const runner = new JestHarness(makeGlobalConfig({ watch: true }));
|
|
62
|
+
await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
|
|
63
|
+
await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
|
|
64
|
+
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
65
|
+
expect(mockSession.dispose).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
it('disposes the session after each run in normal mode', async () => {
|
|
68
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
69
|
+
await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
|
|
70
|
+
expect(mockSession.dispose).toHaveBeenCalledOnce();
|
|
71
|
+
});
|
|
72
|
+
it('creates a fresh session for each run in normal mode', async () => {
|
|
73
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
74
|
+
await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
|
|
75
|
+
await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
|
|
76
|
+
expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('error handling', () => {
|
|
80
|
+
it('converts HarnessError into a formatted string before throwing', async () => {
|
|
81
|
+
class TestHarnessError extends HarnessError {
|
|
82
|
+
constructor() { super('something went wrong'); this.name = 'TestHarnessError'; }
|
|
83
|
+
}
|
|
84
|
+
mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
|
|
85
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
86
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions())).rejects.toBeTypeOf('string');
|
|
87
|
+
});
|
|
88
|
+
it('propagates non-HarnessError exceptions as-is', async () => {
|
|
89
|
+
const cause = new TypeError('unexpected');
|
|
90
|
+
mockCreateHarnessSession.mockRejectedValue(cause);
|
|
91
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
92
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions())).rejects.toBe(cause);
|
|
93
|
+
});
|
|
94
|
+
it('throws when called without serial flag', async () => {
|
|
95
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
96
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), { serial: false })).rejects.toThrow('Parallel test running is not supported');
|
|
40
97
|
});
|
|
41
98
|
});
|
|
42
99
|
});
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { createResourceLockManager
|
|
5
|
+
import { createResourceLockManager } from '../resource-lock.js';
|
|
6
6
|
describe('resource lock manager', () => {
|
|
7
7
|
let rootDir;
|
|
8
8
|
beforeEach(async () => {
|
|
@@ -54,10 +54,11 @@ describe('resource lock manager', () => {
|
|
|
54
54
|
await expect(acquirePromise).rejects.toMatchObject({
|
|
55
55
|
name: 'AbortError',
|
|
56
56
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(queuedEntries).toHaveLength(0);
|
|
57
|
+
// The aborted waiter must have cleaned up its ticket. Verify by releasing
|
|
58
|
+
// the first lock and confirming a fresh acquire completes immediately.
|
|
60
59
|
await firstLease.release();
|
|
60
|
+
const cleanupLease = await manager.acquire(key);
|
|
61
|
+
await cleanupLease.release();
|
|
61
62
|
});
|
|
62
63
|
it('keeps queued tickets alive while the waiting process is still active', async () => {
|
|
63
64
|
const manager = createResourceLockManager({
|
|
@@ -76,29 +77,27 @@ describe('resource lock manager', () => {
|
|
|
76
77
|
await secondLease.release();
|
|
77
78
|
});
|
|
78
79
|
it('reclaims a stale owner before granting the lock', async () => {
|
|
79
|
-
const
|
|
80
|
+
const key = 'web:browser:chromium';
|
|
81
|
+
// Simulate a live process holding the lock.
|
|
82
|
+
const manager1 = createResourceLockManager({
|
|
83
|
+
rootDir,
|
|
84
|
+
pollIntervalMs: 5,
|
|
85
|
+
heartbeatIntervalMs: 20,
|
|
86
|
+
});
|
|
87
|
+
const staleLease = await manager1.acquire(key);
|
|
88
|
+
// A second manager whose isProcessActive always returns false will consider
|
|
89
|
+
// any owner — including the live one above — immediately stale and reclaim it.
|
|
90
|
+
const manager2 = createResourceLockManager({
|
|
80
91
|
rootDir,
|
|
81
92
|
pollIntervalMs: 5,
|
|
82
93
|
heartbeatIntervalMs: 20,
|
|
83
94
|
staleLockTimeoutMs: 50,
|
|
84
95
|
isProcessActive: () => false,
|
|
85
96
|
});
|
|
86
|
-
const
|
|
87
|
-
const keyDir = path.join(rootDir, hashResourceLockKey(key));
|
|
88
|
-
const queueDir = path.join(keyDir, 'queue');
|
|
89
|
-
const ownerFilePath = path.join(keyDir, 'owner.json');
|
|
90
|
-
await fs.mkdir(queueDir, { recursive: true });
|
|
91
|
-
await fs.writeFile(ownerFilePath, JSON.stringify({
|
|
92
|
-
ticketId: 'stale-owner',
|
|
93
|
-
key,
|
|
94
|
-
pid: 999999,
|
|
95
|
-
createdAt: Date.now() - 1000,
|
|
96
|
-
heartbeatAt: Date.now() - 1000,
|
|
97
|
-
}), 'utf8');
|
|
98
|
-
const lease = await manager.acquire(key);
|
|
99
|
-
const owner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8'));
|
|
100
|
-
expect(owner.ticketId).not.toBe('stale-owner');
|
|
97
|
+
const lease = await manager2.acquire(key);
|
|
101
98
|
await lease.release();
|
|
99
|
+
// manager1 was evicted; its release is best-effort.
|
|
100
|
+
await staleLease.release();
|
|
102
101
|
});
|
|
103
102
|
it('keeps owner metadata valid when heartbeat writes overlap', async () => {
|
|
104
103
|
const manager = createResourceLockManager({
|
|
@@ -108,20 +107,21 @@ describe('resource lock manager', () => {
|
|
|
108
107
|
staleLockTimeoutMs: 200,
|
|
109
108
|
});
|
|
110
109
|
const key = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
111
|
-
const ownerFilePath = path.join(rootDir, hashResourceLockKey(key), 'owner.json');
|
|
112
110
|
const actualWriteFile = fs.writeFile.bind(fs);
|
|
113
111
|
const writeFileSpy = vi
|
|
114
112
|
.spyOn(fs, 'writeFile')
|
|
115
113
|
.mockImplementation(async (file, data, options) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
file !== ownerFilePath) {
|
|
114
|
+
// Delay atomic temp-file writes to simulate overlapping heartbeat flushes.
|
|
115
|
+
if (typeof file === 'string' && file.startsWith(rootDir) && file.endsWith('.tmp')) {
|
|
119
116
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
120
117
|
}
|
|
121
118
|
return await actualWriteFile(file, data, options);
|
|
122
119
|
});
|
|
123
120
|
try {
|
|
124
121
|
const lease = await manager.acquire(key);
|
|
122
|
+
// Discover the owner file after acquire creates the key directory.
|
|
123
|
+
const [keyDirName] = await fs.readdir(rootDir);
|
|
124
|
+
const ownerFilePath = path.join(rootDir, keyDirName, 'owner.json');
|
|
125
125
|
const initialOwner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8'));
|
|
126
126
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
127
127
|
for (let index = 0; index < 5; index += 1) {
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import type { ReportableEvent } from '@react-native-harness/bundler-metro';
|
|
2
|
+
import type { TestResult as JestTestResult } from '@jest/test-result';
|
|
2
3
|
export type ClientLogEvent = Extract<ReportableEvent, {
|
|
3
4
|
type: 'client_log';
|
|
4
5
|
}>;
|
|
6
|
+
export type ClientLogBuffer = NonNullable<JestTestResult['console']>;
|
|
7
|
+
export type ClientLogEntry = ClientLogBuffer[number];
|
|
8
|
+
export type ClientLogCollector = {
|
|
9
|
+
handleEvent: (event: ReportableEvent) => void;
|
|
10
|
+
flush: () => ClientLogBuffer;
|
|
11
|
+
};
|
|
5
12
|
/**
|
|
6
13
|
* Formats a client log event data array into a string message.
|
|
7
14
|
* Uses util.format for printf-style format specifier support (%s, %d, %j, etc.)
|
|
8
15
|
*/
|
|
9
16
|
export declare const formatClientLogMessage: (data: unknown[]) => string;
|
|
10
17
|
/**
|
|
11
|
-
* Formats a client log event into
|
|
18
|
+
* Formats a client log event into Jest's buffered console entry format.
|
|
12
19
|
*/
|
|
13
|
-
export declare const
|
|
14
|
-
|
|
15
|
-
* Handles a client_log event by formatting and logging it.
|
|
16
|
-
* Returns true if the event was handled, false otherwise.
|
|
17
|
-
*/
|
|
18
|
-
export declare const handleClientLogEvent: (event: ReportableEvent) => boolean;
|
|
19
|
-
/**
|
|
20
|
-
* Creates a client log event listener that can be added to the Metro reporter.
|
|
21
|
-
*/
|
|
22
|
-
export declare const createClientLogListener: () => ((event: ReportableEvent) => void);
|
|
20
|
+
export declare const formatClientLogEntry: (event: ClientLogEvent) => ClientLogEntry | null;
|
|
21
|
+
export declare const createClientLogCollector: () => ClientLogCollector;
|
|
23
22
|
//# sourceMappingURL=client-log-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-log-handler.d.ts","sourceRoot":"","sources":["../src/client-log-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"client-log-handler.d.ts","sourceRoot":"","sources":["../src/client-log-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGtE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,eAAe,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAC9E,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC;AACrE,MAAM,MAAM,cAAc,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;AACrD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,KAAK,EAAE,MAAM,eAAe,CAAC;CAC9B,CAAC;AAuBF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,OAAO,EAAE,KAAG,MAMxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,cAAc,KACpB,cAAc,GAAG,IAYnB,CAAC;AAEF,eAAO,MAAM,wBAAwB,QAAO,kBAoB3C,CAAC"}
|