@react-native-harness/jest 1.1.0-rc.1 → 1.1.0-rc.3
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__/errors.test.js +6 -1
- package/dist/__tests__/harness-cache.test.d.ts +2 -0
- package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
- package/dist/__tests__/harness-cache.test.js +51 -0
- package/dist/__tests__/harness.test.js +484 -47
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +42 -0
- package/dist/__tests__/resource-lock.test.d.ts +2 -0
- package/dist/__tests__/resource-lock.test.d.ts.map +1 -0
- package/dist/__tests__/resource-lock.test.js +103 -0
- package/dist/action-hooks.d.ts +9 -0
- package/dist/action-hooks.d.ts.map +1 -0
- package/dist/action-hooks.js +36 -0
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/crash-supervisor.d.ts.map +1 -1
- package/dist/crash-supervisor.js +7 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -2
- package/dist/harness.d.ts +23 -1
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +574 -136
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -58
- package/dist/logs.d.ts +4 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +12 -0
- package/dist/resource-lock.d.ts +25 -0
- package/dist/resource-lock.d.ts.map +1 -0
- package/dist/resource-lock.js +278 -0
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +13 -8
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +29 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/__tests__/errors.test.ts +18 -6
- package/src/__tests__/harness-cache.test.ts +74 -0
- package/src/__tests__/harness.test.ts +702 -49
- package/src/__tests__/index.test.ts +61 -0
- package/src/__tests__/resource-lock.test.ts +138 -0
- package/src/action-hooks.ts +66 -0
- package/src/cli-args.ts +6 -0
- package/src/crash-supervisor.ts +13 -6
- package/src/env.d.ts +1 -1
- package/src/errors.ts +15 -4
- package/src/harness.ts +743 -157
- package/src/index.ts +276 -64
- package/src/logs.ts +17 -3
- package/src/resource-lock.ts +450 -0
- package/src/run.ts +20 -10
- package/src/setup.ts +42 -1
- package/tsconfig.json +3 -3
- package/tsconfig.lib.json +6 -6
|
@@ -1,70 +1,285 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
2
|
+
import { HARNESS_BRIDGE_PATH } from '@react-native-harness/bridge';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
5
|
+
import { definePlugin } from '@react-native-harness/plugins';
|
|
5
6
|
import type {
|
|
6
7
|
AppMonitor,
|
|
8
|
+
AppMonitorEvent,
|
|
7
9
|
AppMonitorListener,
|
|
10
|
+
HarnessPlatform,
|
|
8
11
|
HarnessPlatformRunner,
|
|
9
12
|
} from '@react-native-harness/platforms';
|
|
13
|
+
import type { BridgeServer } from '@react-native-harness/bridge/server';
|
|
14
|
+
import type {
|
|
15
|
+
MetroInstance,
|
|
16
|
+
Reporter,
|
|
17
|
+
ReportableEvent,
|
|
18
|
+
WaitForMetroBackedAppReadyOptions,
|
|
19
|
+
} from '@react-native-harness/bundler-metro';
|
|
10
20
|
import { createCrashSupervisor } from '../crash-supervisor.js';
|
|
11
21
|
|
|
22
|
+
const mocks = vi.hoisted(() => ({
|
|
23
|
+
createCrashArtifactWriter: vi.fn(() => ({})),
|
|
24
|
+
getBridgeServer: vi.fn(),
|
|
25
|
+
getMetroInstance: vi.fn(),
|
|
26
|
+
isMetroCacheReusable: vi.fn(() => false),
|
|
27
|
+
logMetroCacheReused: vi.fn(),
|
|
28
|
+
logRunnerStarting: vi.fn(),
|
|
29
|
+
logRunnerStillWaitingInQueue: vi.fn(),
|
|
30
|
+
logRunnerWaitingInQueue: vi.fn(),
|
|
31
|
+
waitForMetroBackedAppReady: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
35
|
+
const actual = await vi.importActual<
|
|
36
|
+
typeof import('@react-native-harness/bundler-metro')
|
|
37
|
+
>('@react-native-harness/bundler-metro');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
getMetroInstance: mocks.getMetroInstance,
|
|
42
|
+
isMetroCacheReusable: mocks.isMetroCacheReusable,
|
|
43
|
+
waitForMetroBackedAppReady: mocks.waitForMetroBackedAppReady,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
vi.mock('@react-native-harness/bridge/server', () => ({
|
|
48
|
+
getBridgeServer: mocks.getBridgeServer,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('../logs.js', () => ({
|
|
52
|
+
logMetroCacheReused: mocks.logMetroCacheReused,
|
|
53
|
+
logRunnerStarting: mocks.logRunnerStarting,
|
|
54
|
+
logRunnerStillWaitingInQueue: mocks.logRunnerStillWaitingInQueue,
|
|
55
|
+
logRunnerWaitingInQueue: mocks.logRunnerWaitingInQueue,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock('@react-native-harness/tools', async () => {
|
|
59
|
+
const actual = await vi.importActual<
|
|
60
|
+
typeof import('@react-native-harness/tools')
|
|
61
|
+
>('@react-native-harness/tools');
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...actual,
|
|
65
|
+
createCrashArtifactWriter: mocks.createCrashArtifactWriter,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
import { getHarness, waitForAppReady } from '../harness.js';
|
|
70
|
+
import { PlatformReadyTimeoutError, StartupStallError } from '../errors.js';
|
|
71
|
+
|
|
12
72
|
const createBridgeServer = () => {
|
|
13
73
|
const emitter = new EventEmitter();
|
|
14
74
|
|
|
15
75
|
return {
|
|
16
76
|
serverBridge: {
|
|
77
|
+
ws: {} as BridgeServer['ws'],
|
|
78
|
+
rpc: {
|
|
79
|
+
clients: [],
|
|
80
|
+
},
|
|
17
81
|
on: emitter.on.bind(emitter),
|
|
18
82
|
once: emitter.once.bind(emitter),
|
|
19
83
|
off: emitter.off.bind(emitter),
|
|
84
|
+
dispose: vi.fn(),
|
|
20
85
|
} as unknown as BridgeServer,
|
|
21
86
|
emitReady: () => {
|
|
22
|
-
emitter.emit('ready'
|
|
87
|
+
emitter.emit('ready', {
|
|
88
|
+
platform: 'ios',
|
|
89
|
+
manufacturer: 'Apple',
|
|
90
|
+
model: 'Simulator',
|
|
91
|
+
osVersion: '18.0',
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
emitEvent: (event: unknown) => {
|
|
95
|
+
emitter.emit('event', event);
|
|
96
|
+
},
|
|
97
|
+
emitDisconnect: () => {
|
|
98
|
+
emitter.emit('disconnect');
|
|
23
99
|
},
|
|
24
100
|
};
|
|
25
101
|
};
|
|
26
102
|
|
|
27
|
-
const
|
|
28
|
-
const listeners = new Set<
|
|
103
|
+
const createReporter = (): Reporter => {
|
|
104
|
+
const listeners = new Set<(event: ReportableEvent) => void>();
|
|
29
105
|
|
|
30
106
|
return {
|
|
31
|
-
start: async () => undefined,
|
|
32
|
-
stop: async () => undefined,
|
|
33
|
-
dispose: async () => undefined,
|
|
34
107
|
addListener: (listener) => {
|
|
35
108
|
listeners.add(listener);
|
|
36
109
|
},
|
|
37
110
|
removeListener: (listener) => {
|
|
38
111
|
listeners.delete(listener);
|
|
39
112
|
},
|
|
113
|
+
emit: (event) => {
|
|
114
|
+
listeners.forEach((listener) => listener(event));
|
|
115
|
+
},
|
|
116
|
+
clearAllListeners: () => {
|
|
117
|
+
listeners.clear();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const createMetroInstance = (
|
|
123
|
+
overrides: Partial<MetroInstance> = {}
|
|
124
|
+
): MetroInstance => ({
|
|
125
|
+
events: createReporter(),
|
|
126
|
+
httpServer: {} as never,
|
|
127
|
+
websocketEndpoints: {},
|
|
128
|
+
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
|
|
129
|
+
prewarm: vi.fn(async () => false),
|
|
130
|
+
dispose: vi.fn(async () => undefined),
|
|
131
|
+
...overrides,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const createAppMonitor = (): {
|
|
135
|
+
appMonitor: AppMonitor;
|
|
136
|
+
emit: (event: AppMonitorEvent) => void;
|
|
137
|
+
} => {
|
|
138
|
+
const listeners = new Set<AppMonitorListener>();
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
appMonitor: {
|
|
142
|
+
start: vi.fn(async () => undefined),
|
|
143
|
+
stop: vi.fn(async () => undefined),
|
|
144
|
+
dispose: vi.fn(async () => undefined),
|
|
145
|
+
addListener: (listener) => {
|
|
146
|
+
listeners.add(listener);
|
|
147
|
+
},
|
|
148
|
+
removeListener: (listener) => {
|
|
149
|
+
listeners.delete(listener);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
emit: (event) => {
|
|
153
|
+
for (const listener of listeners) {
|
|
154
|
+
listener(event);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
40
157
|
};
|
|
41
158
|
};
|
|
42
159
|
|
|
43
160
|
const createPlatformRunner = (
|
|
44
|
-
|
|
161
|
+
overrides: Partial<HarnessPlatformRunner> = {}
|
|
45
162
|
): HarnessPlatformRunner => ({
|
|
46
|
-
startApp: async () => undefined,
|
|
47
|
-
restartApp,
|
|
48
|
-
stopApp: async () => undefined,
|
|
49
|
-
dispose: async () => undefined,
|
|
50
|
-
isAppRunning: async () => true,
|
|
51
|
-
createAppMonitor: createAppMonitor,
|
|
163
|
+
startApp: vi.fn(async () => undefined),
|
|
164
|
+
restartApp: vi.fn(async () => undefined),
|
|
165
|
+
stopApp: vi.fn(async () => undefined),
|
|
166
|
+
dispose: vi.fn(async () => undefined),
|
|
167
|
+
isAppRunning: vi.fn(async () => true),
|
|
168
|
+
createAppMonitor: () => createAppMonitor().appMonitor,
|
|
169
|
+
...overrides,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const createHarnessConfig = (
|
|
173
|
+
overrides: Partial<HarnessConfig> = {}
|
|
174
|
+
): HarnessConfig =>
|
|
175
|
+
({
|
|
176
|
+
appRegistryComponentName: 'App',
|
|
177
|
+
bridgeTimeout: 60_000,
|
|
178
|
+
bundleStartTimeout: 60_000,
|
|
179
|
+
crashDetectionInterval: 500,
|
|
180
|
+
defaultRunner: 'ios',
|
|
181
|
+
detectNativeCrashes: true,
|
|
182
|
+
disableViewFlattening: false,
|
|
183
|
+
entryPoint: 'index.js',
|
|
184
|
+
forwardClientLogs: false,
|
|
185
|
+
maxAppRestarts: 2,
|
|
186
|
+
metroPort: 8081,
|
|
187
|
+
platformReadyTimeout: 300_000,
|
|
188
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
189
|
+
runners: [],
|
|
190
|
+
unstable__enableMetroCache: false,
|
|
191
|
+
unstable__skipAlreadyIncludedModules: false,
|
|
192
|
+
webSocketPort: 3001,
|
|
193
|
+
...overrides,
|
|
194
|
+
} as HarnessConfig);
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
vi.clearAllMocks();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
afterEach(() => {
|
|
201
|
+
delete (
|
|
202
|
+
globalThis as typeof globalThis & {
|
|
203
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
204
|
+
}
|
|
205
|
+
).__HARNESS_PLATFORM_RUNNER__;
|
|
52
206
|
});
|
|
53
207
|
|
|
54
208
|
describe('waitForAppReady', () => {
|
|
55
|
-
it('
|
|
209
|
+
it('delegates startup orchestration to bundler-metro and resolves readiness from the bridge', async () => {
|
|
56
210
|
const { serverBridge, emitReady } = createBridgeServer();
|
|
57
|
-
const
|
|
58
|
-
const platformInstance = createPlatformRunner(
|
|
211
|
+
const metroInstance = createMetroInstance();
|
|
212
|
+
const platformInstance = createPlatformRunner();
|
|
213
|
+
const { appMonitor } = createAppMonitor();
|
|
214
|
+
const crashSupervisor = createCrashSupervisor({
|
|
215
|
+
appMonitor,
|
|
216
|
+
platformRunner: platformInstance,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
220
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
221
|
+
options.onAttemptStart?.();
|
|
222
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
223
|
+
emitReady();
|
|
224
|
+
await readyPromise;
|
|
225
|
+
options.onAttemptReset?.();
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await waitForAppReady({
|
|
230
|
+
metroInstance,
|
|
231
|
+
serverBridge,
|
|
232
|
+
platformInstance,
|
|
233
|
+
platformId: 'ios',
|
|
234
|
+
bundleStartTimeout: 1_500,
|
|
235
|
+
readyTimeout: 2_500,
|
|
236
|
+
maxAppRestarts: 3,
|
|
237
|
+
testFilePath: '/tmp/test.harness.ts',
|
|
238
|
+
crashSupervisor,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledWith(
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
metro: metroInstance,
|
|
244
|
+
platformId: 'ios',
|
|
245
|
+
bundleStartTimeout: 1_500,
|
|
246
|
+
readyTimeout: 2_500,
|
|
247
|
+
maxAppRestarts: 3,
|
|
248
|
+
startAttempt: expect.any(Function),
|
|
249
|
+
waitForReady: expect.any(Function),
|
|
250
|
+
waitForCrash: expect.any(Function),
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
expect(crashSupervisor.isReady()).toBe(true);
|
|
254
|
+
|
|
255
|
+
await crashSupervisor.dispose();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('passes launch options through the shared Metro startup helper', async () => {
|
|
259
|
+
const { serverBridge } = createBridgeServer();
|
|
260
|
+
const metroInstance = createMetroInstance();
|
|
261
|
+
const restartApp = vi.fn(async () => undefined);
|
|
262
|
+
const platformInstance = createPlatformRunner({ restartApp });
|
|
263
|
+
const { appMonitor } = createAppMonitor();
|
|
59
264
|
const crashSupervisor = createCrashSupervisor({
|
|
60
|
-
appMonitor
|
|
265
|
+
appMonitor,
|
|
61
266
|
platformRunner: platformInstance,
|
|
62
267
|
});
|
|
63
268
|
|
|
64
|
-
|
|
269
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
270
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
271
|
+
await options.startAttempt();
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await waitForAppReady({
|
|
276
|
+
metroInstance,
|
|
65
277
|
serverBridge,
|
|
66
278
|
platformInstance,
|
|
67
|
-
|
|
279
|
+
platformId: 'ios',
|
|
280
|
+
bundleStartTimeout: 1_500,
|
|
281
|
+
readyTimeout: 2_500,
|
|
282
|
+
maxAppRestarts: 3,
|
|
68
283
|
testFilePath: '/tmp/test.harness.ts',
|
|
69
284
|
crashSupervisor,
|
|
70
285
|
appLaunchOptions: {
|
|
@@ -74,56 +289,494 @@ describe('waitForAppReady', () => {
|
|
|
74
289
|
},
|
|
75
290
|
});
|
|
76
291
|
|
|
77
|
-
await Promise.resolve();
|
|
78
292
|
expect(restartApp).toHaveBeenCalledWith({
|
|
79
293
|
extras: {
|
|
80
294
|
mode: 'startup',
|
|
81
295
|
},
|
|
82
296
|
});
|
|
83
297
|
|
|
84
|
-
emitReady();
|
|
85
|
-
await promise;
|
|
86
298
|
await crashSupervisor.dispose();
|
|
87
299
|
});
|
|
300
|
+
});
|
|
88
301
|
|
|
89
|
-
|
|
90
|
-
|
|
302
|
+
describe('getHarness', () => {
|
|
303
|
+
it('fails when the platform runner does not become ready within platformReadyTimeout', async () => {
|
|
304
|
+
const { serverBridge } = createBridgeServer();
|
|
305
|
+
const metroInstance = createMetroInstance();
|
|
306
|
+
|
|
307
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
308
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
309
|
+
|
|
310
|
+
(
|
|
311
|
+
globalThis as typeof globalThis & {
|
|
312
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
313
|
+
}
|
|
314
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(
|
|
315
|
+
async () =>
|
|
316
|
+
await new Promise((_, reject) => {
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
319
|
+
}, 20);
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const platform: HarnessPlatform = {
|
|
324
|
+
config: {},
|
|
325
|
+
getResourceLockKey: () => 'ios:test-platform-ready-timeout',
|
|
326
|
+
name: 'ios',
|
|
327
|
+
platformId: 'ios',
|
|
328
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
329
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
330
|
+
)}`,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
await expect(
|
|
334
|
+
getHarness(
|
|
335
|
+
createHarnessConfig({
|
|
336
|
+
platformReadyTimeout: 10,
|
|
337
|
+
}),
|
|
338
|
+
platform,
|
|
339
|
+
'/tmp/project'
|
|
340
|
+
)
|
|
341
|
+
).rejects.toBeInstanceOf(PlatformReadyTimeoutError);
|
|
342
|
+
});
|
|
91
343
|
|
|
344
|
+
it('passes a platform init signal to the runner factory', async () => {
|
|
92
345
|
const { serverBridge } = createBridgeServer();
|
|
93
|
-
const
|
|
94
|
-
const platformInstance = createPlatformRunner(
|
|
95
|
-
|
|
96
|
-
appMonitor: createAppMonitor(),
|
|
97
|
-
platformRunner: platformInstance,
|
|
346
|
+
const appMonitor = createAppMonitor();
|
|
347
|
+
const platformInstance = createPlatformRunner({
|
|
348
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
98
349
|
});
|
|
350
|
+
const metroInstance = createMetroInstance();
|
|
99
351
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
352
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
353
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
354
|
+
|
|
355
|
+
const runner = vi.fn(async () => platformInstance);
|
|
356
|
+
(
|
|
357
|
+
globalThis as typeof globalThis & {
|
|
358
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
359
|
+
}
|
|
360
|
+
).__HARNESS_PLATFORM_RUNNER__ = runner;
|
|
361
|
+
|
|
362
|
+
const platform: HarnessPlatform = {
|
|
363
|
+
config: {},
|
|
364
|
+
getResourceLockKey: () => 'ios:test-platform-init-signal',
|
|
365
|
+
name: 'ios',
|
|
366
|
+
platformId: 'ios',
|
|
367
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
368
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
369
|
+
)}`,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const harness = await getHarness(
|
|
373
|
+
createHarnessConfig(),
|
|
374
|
+
platform,
|
|
375
|
+
'/tmp/project'
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(runner).toHaveBeenCalledWith(
|
|
379
|
+
platform.config,
|
|
380
|
+
expect.any(Object),
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
signal: expect.any(AbortSignal),
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await harness.dispose();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('falls back to a default resource lock key for platforms without getResourceLockKey', async () => {
|
|
390
|
+
const { serverBridge } = createBridgeServer();
|
|
391
|
+
const appMonitor = createAppMonitor();
|
|
392
|
+
const platformInstance = createPlatformRunner({
|
|
393
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
394
|
+
});
|
|
395
|
+
const metroInstance = createMetroInstance();
|
|
396
|
+
|
|
397
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
398
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
399
|
+
|
|
400
|
+
(
|
|
401
|
+
globalThis as typeof globalThis & {
|
|
402
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
403
|
+
}
|
|
404
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
405
|
+
|
|
406
|
+
const platform: HarnessPlatform = {
|
|
407
|
+
config: {},
|
|
408
|
+
name: 'legacy-ios',
|
|
409
|
+
platformId: 'ios',
|
|
410
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
411
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
412
|
+
)}`,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const harness = await getHarness(
|
|
416
|
+
createHarnessConfig(),
|
|
417
|
+
platform,
|
|
418
|
+
'/tmp/project'
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
await harness.dispose();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('routes ensureAppReady through the shared Metro startup helper', async () => {
|
|
425
|
+
const { serverBridge, emitReady } = createBridgeServer();
|
|
426
|
+
const appMonitor = createAppMonitor();
|
|
427
|
+
const restartApp = vi.fn(async () => undefined);
|
|
428
|
+
const platformInstance = createPlatformRunner({
|
|
429
|
+
restartApp,
|
|
430
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
431
|
+
});
|
|
432
|
+
const metroInstance = createMetroInstance();
|
|
433
|
+
|
|
434
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
435
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
436
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
437
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
438
|
+
await options.startAttempt();
|
|
439
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
440
|
+
emitReady();
|
|
441
|
+
await readyPromise;
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
(
|
|
446
|
+
globalThis as typeof globalThis & {
|
|
447
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
448
|
+
}
|
|
449
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
450
|
+
|
|
451
|
+
const platform: HarnessPlatform = {
|
|
452
|
+
config: {
|
|
453
|
+
appLaunchOptions: {
|
|
454
|
+
extras: {
|
|
455
|
+
source: 'ensure-ready',
|
|
456
|
+
},
|
|
109
457
|
},
|
|
110
458
|
},
|
|
459
|
+
name: 'ios',
|
|
460
|
+
platformId: 'ios',
|
|
461
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
462
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
463
|
+
)}`,
|
|
464
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const harness = await getHarness(
|
|
468
|
+
createHarnessConfig({
|
|
469
|
+
bridgeTimeout: 1,
|
|
470
|
+
}),
|
|
471
|
+
platform,
|
|
472
|
+
'/tmp/project'
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
await harness.ensureAppReady('/tmp/example.harness.ts');
|
|
476
|
+
|
|
477
|
+
expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledTimes(1);
|
|
478
|
+
expect(restartApp).toHaveBeenCalledWith({
|
|
479
|
+
extras: {
|
|
480
|
+
source: 'ensure-ready',
|
|
481
|
+
},
|
|
111
482
|
});
|
|
112
483
|
|
|
113
|
-
await
|
|
114
|
-
|
|
484
|
+
await harness.dispose();
|
|
485
|
+
});
|
|
115
486
|
|
|
116
|
-
|
|
117
|
-
|
|
487
|
+
it('routes restart(testFilePath) through the shared Metro startup helper', async () => {
|
|
488
|
+
const { serverBridge, emitReady } = createBridgeServer();
|
|
489
|
+
const appMonitor = createAppMonitor();
|
|
490
|
+
const restartApp = vi.fn(async () => undefined);
|
|
491
|
+
const stopApp = vi.fn(async () => undefined);
|
|
492
|
+
const platformInstance = createPlatformRunner({
|
|
493
|
+
restartApp,
|
|
494
|
+
stopApp,
|
|
495
|
+
isAppRunning: vi.fn(async () => false),
|
|
496
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
118
497
|
});
|
|
119
|
-
|
|
498
|
+
const metroInstance = createMetroInstance();
|
|
499
|
+
|
|
500
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
501
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
502
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
503
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
504
|
+
await options.startAttempt();
|
|
505
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
506
|
+
emitReady();
|
|
507
|
+
await readyPromise;
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
(
|
|
512
|
+
globalThis as typeof globalThis & {
|
|
513
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
514
|
+
}
|
|
515
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
516
|
+
|
|
517
|
+
const platform: HarnessPlatform = {
|
|
518
|
+
config: {
|
|
519
|
+
appLaunchOptions: {
|
|
520
|
+
extras: {
|
|
521
|
+
source: 'restart',
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
name: 'ios',
|
|
526
|
+
platformId: 'ios',
|
|
527
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
528
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
529
|
+
)}`,
|
|
530
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const harness = await getHarness(
|
|
534
|
+
createHarnessConfig(),
|
|
535
|
+
platform,
|
|
536
|
+
'/tmp/project'
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(mocks.getBridgeServer).toHaveBeenCalledWith(
|
|
540
|
+
expect.objectContaining({
|
|
541
|
+
noServer: true,
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledWith(
|
|
545
|
+
expect.objectContaining({
|
|
546
|
+
websocketEndpoints: {
|
|
547
|
+
[HARNESS_BRIDGE_PATH]: serverBridge.ws,
|
|
548
|
+
},
|
|
549
|
+
}),
|
|
550
|
+
expect.any(AbortSignal)
|
|
551
|
+
);
|
|
552
|
+
await harness.restart('/tmp/restart.harness.ts');
|
|
553
|
+
|
|
554
|
+
expect(stopApp).toHaveBeenCalledTimes(1);
|
|
555
|
+
expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledTimes(1);
|
|
120
556
|
expect(restartApp).toHaveBeenCalledWith({
|
|
121
557
|
extras: {
|
|
122
|
-
|
|
558
|
+
source: 'restart',
|
|
123
559
|
},
|
|
124
560
|
});
|
|
125
561
|
|
|
126
|
-
await
|
|
127
|
-
|
|
562
|
+
await harness.dispose();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('plugins', () => {
|
|
567
|
+
it('invokes lifecycle and runtime plugin handlers for configured plugins', async () => {
|
|
568
|
+
const { serverBridge, emitReady, emitEvent, emitDisconnect } =
|
|
569
|
+
createBridgeServer();
|
|
570
|
+
const appMonitor = createAppMonitor();
|
|
571
|
+
const platformInstance = createPlatformRunner({
|
|
572
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
573
|
+
});
|
|
574
|
+
const observedHooks: string[] = [];
|
|
575
|
+
|
|
576
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
577
|
+
const metroInstance = createMetroInstance();
|
|
578
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
579
|
+
|
|
580
|
+
(
|
|
581
|
+
globalThis as typeof globalThis & {
|
|
582
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
583
|
+
}
|
|
584
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
585
|
+
|
|
586
|
+
const plugin = definePlugin<
|
|
587
|
+
{
|
|
588
|
+
creationCount: number;
|
|
589
|
+
},
|
|
590
|
+
HarnessConfig,
|
|
591
|
+
HarnessPlatform
|
|
592
|
+
>({
|
|
593
|
+
name: 'test-plugin',
|
|
594
|
+
createState: () => ({
|
|
595
|
+
creationCount: 0,
|
|
596
|
+
}),
|
|
597
|
+
hooks: {
|
|
598
|
+
harness: {
|
|
599
|
+
beforeCreation: (ctx) => {
|
|
600
|
+
ctx.state.creationCount += 1;
|
|
601
|
+
observedHooks.push(
|
|
602
|
+
`beforeCreation:${ctx.platform.platformId}:${
|
|
603
|
+
ctx.appLaunchOptions == null
|
|
604
|
+
? 'no-launch-options'
|
|
605
|
+
: 'launch-options'
|
|
606
|
+
}`
|
|
607
|
+
);
|
|
608
|
+
},
|
|
609
|
+
beforeRun: (ctx) => {
|
|
610
|
+
observedHooks.push(
|
|
611
|
+
`beforeRun:${ctx.platform.platformId}:${
|
|
612
|
+
ctx.appLaunchOptions == null
|
|
613
|
+
? 'no-launch-options'
|
|
614
|
+
: 'launch-options'
|
|
615
|
+
}`
|
|
616
|
+
);
|
|
617
|
+
},
|
|
618
|
+
afterRun: (ctx) => {
|
|
619
|
+
observedHooks.push(
|
|
620
|
+
`afterRun:${ctx.state.creationCount}:${ctx.reason}`
|
|
621
|
+
);
|
|
622
|
+
},
|
|
623
|
+
beforeDispose: (ctx) => {
|
|
624
|
+
observedHooks.push(
|
|
625
|
+
`beforeDispose:${ctx.state.creationCount}:${ctx.reason}`
|
|
626
|
+
);
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
runtime: {
|
|
630
|
+
ready: (ctx) => {
|
|
631
|
+
observedHooks.push(
|
|
632
|
+
`runtime.ready:${ctx.runId}:${ctx.device.platform}`
|
|
633
|
+
);
|
|
634
|
+
},
|
|
635
|
+
disconnected: (ctx) => {
|
|
636
|
+
observedHooks.push(`runtime.disconnected:${ctx.reason}`);
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
collection: {
|
|
640
|
+
started: (ctx) => {
|
|
641
|
+
observedHooks.push(`collection.started:${ctx.file}`);
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const platform: HarnessPlatform = {
|
|
648
|
+
config: {
|
|
649
|
+
appLaunchOptions: {
|
|
650
|
+
environment: {
|
|
651
|
+
MODE: 'test',
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
name: 'ios',
|
|
656
|
+
platformId: 'ios',
|
|
657
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
658
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
659
|
+
)}`,
|
|
660
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const harness = await getHarness(
|
|
664
|
+
createHarnessConfig({
|
|
665
|
+
plugins: [plugin],
|
|
666
|
+
}),
|
|
667
|
+
platform,
|
|
668
|
+
'/tmp/project'
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
harness.setRunState({
|
|
672
|
+
runId: 'run-1',
|
|
673
|
+
startTime: Date.now(),
|
|
674
|
+
testFiles: ['example.harness.ts'],
|
|
675
|
+
watchMode: false,
|
|
676
|
+
coverageEnabled: false,
|
|
677
|
+
summary: {
|
|
678
|
+
passed: 1,
|
|
679
|
+
failed: 0,
|
|
680
|
+
skipped: 0,
|
|
681
|
+
todo: 0,
|
|
682
|
+
},
|
|
683
|
+
status: 'passed',
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
emitReady();
|
|
687
|
+
emitEvent({
|
|
688
|
+
type: 'collection-started',
|
|
689
|
+
file: 'example.harness.ts',
|
|
690
|
+
});
|
|
691
|
+
emitDisconnect();
|
|
692
|
+
|
|
693
|
+
await harness.dispose();
|
|
694
|
+
|
|
695
|
+
expect(observedHooks).toEqual([
|
|
696
|
+
'beforeCreation:ios:launch-options',
|
|
697
|
+
'beforeRun:ios:launch-options',
|
|
698
|
+
'runtime.ready:run-1:ios',
|
|
699
|
+
'collection.started:example.harness.ts',
|
|
700
|
+
'runtime.disconnected:bridge-disconnected',
|
|
701
|
+
'afterRun:1:normal',
|
|
702
|
+
'beforeDispose:1:normal',
|
|
703
|
+
]);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('waits in queue before starting Metro and releases the lock on dispose', async () => {
|
|
707
|
+
const resourceKey = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
708
|
+
const firstPlatformRunner = createPlatformRunner();
|
|
709
|
+
const secondPlatformRunner = createPlatformRunner();
|
|
710
|
+
const secondAppMonitor = createAppMonitor();
|
|
711
|
+
const firstMetroInstance = createMetroInstance();
|
|
712
|
+
const secondMetroInstance = createMetroInstance();
|
|
713
|
+
const firstBridge = createBridgeServer();
|
|
714
|
+
const secondBridge = createBridgeServer();
|
|
715
|
+
|
|
716
|
+
mocks.getBridgeServer
|
|
717
|
+
.mockResolvedValueOnce(firstBridge.serverBridge)
|
|
718
|
+
.mockResolvedValueOnce(secondBridge.serverBridge);
|
|
719
|
+
mocks.getMetroInstance
|
|
720
|
+
.mockResolvedValueOnce(firstMetroInstance)
|
|
721
|
+
.mockResolvedValueOnce(secondMetroInstance);
|
|
722
|
+
|
|
723
|
+
let invocationCount = 0;
|
|
724
|
+
(
|
|
725
|
+
globalThis as typeof globalThis & {
|
|
726
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
727
|
+
}
|
|
728
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => {
|
|
729
|
+
invocationCount += 1;
|
|
730
|
+
return invocationCount === 1
|
|
731
|
+
? firstPlatformRunner
|
|
732
|
+
: createPlatformRunner({
|
|
733
|
+
createAppMonitor: () => secondAppMonitor.appMonitor,
|
|
734
|
+
dispose: secondPlatformRunner.dispose,
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const platform: HarnessPlatform = {
|
|
739
|
+
config: {},
|
|
740
|
+
name: 'ios',
|
|
741
|
+
platformId: 'ios',
|
|
742
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
743
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
744
|
+
)}`,
|
|
745
|
+
getResourceLockKey: () => resourceKey,
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const firstHarness = await getHarness(
|
|
749
|
+
createHarnessConfig(),
|
|
750
|
+
platform,
|
|
751
|
+
'/tmp/project'
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
const secondHarnessPromise = getHarness(
|
|
755
|
+
createHarnessConfig(),
|
|
756
|
+
platform,
|
|
757
|
+
'/tmp/project'
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
761
|
+
|
|
762
|
+
expect(mocks.logRunnerWaitingInQueue).toHaveBeenCalledWith(platform);
|
|
763
|
+
expect(mocks.logRunnerStarting).not.toHaveBeenCalled();
|
|
764
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledTimes(1);
|
|
765
|
+
|
|
766
|
+
await firstHarness.dispose();
|
|
767
|
+
const secondHarness = await secondHarnessPromise;
|
|
768
|
+
|
|
769
|
+
expect(mocks.logRunnerStarting).toHaveBeenCalledWith(platform);
|
|
770
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledTimes(2);
|
|
771
|
+
|
|
772
|
+
await secondHarness.dispose();
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
describe('StartupStallError', () => {
|
|
777
|
+
it('includes the configured timeout and attempt count', () => {
|
|
778
|
+
expect(new StartupStallError(1_500, 4).message).toBe(
|
|
779
|
+
'The app did not request its Metro bundle after 4 launch attempts within 1500ms. Last Metro status: unknown.'
|
|
780
|
+
);
|
|
128
781
|
});
|
|
129
782
|
});
|