@react-native-harness/jest 1.1.0-rc.2 → 1.1.0-rc.4
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.js +1 -1
- package/dist/__tests__/harness.test.js +205 -3
- package/dist/__tests__/metro-port.test.d.ts +2 -0
- package/dist/__tests__/metro-port.test.d.ts.map +1 -0
- package/dist/__tests__/metro-port.test.js +52 -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 +138 -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.test.d.ts +2 -0
- package/dist/cli-args.test.d.ts.map +1 -0
- package/dist/cli-args.test.js +1 -0
- package/dist/crash-monitor.d.ts +16 -0
- package/dist/crash-monitor.d.ts.map +1 -0
- package/dist/crash-monitor.js +75 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -2
- package/dist/event-mapper.d.ts +16 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +120 -0
- package/dist/global-setup.d.ts +3 -0
- package/dist/global-setup.d.ts.map +1 -0
- package/dist/global-setup.js +60 -0
- package/dist/global-teardown.d.ts +3 -0
- package/dist/global-teardown.d.ts.map +1 -0
- package/dist/global-teardown.js +11 -0
- package/dist/harness-reporter.d.ts +8 -0
- package/dist/harness-reporter.d.ts.map +1 -0
- package/dist/harness-reporter.js +12 -0
- package/dist/harness.d.ts +1 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +491 -386
- package/dist/jest-options.d.ts +2 -0
- package/dist/jest-options.d.ts.map +1 -0
- package/dist/jest-options.js +1 -0
- package/dist/logs.d.ts +4 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +12 -0
- package/dist/metro-port.d.ts +16 -0
- package/dist/metro-port.d.ts.map +1 -0
- package/dist/metro-port.js +48 -0
- package/dist/resource-lock.d.ts +25 -0
- package/dist/resource-lock.d.ts.map +1 -0
- package/dist/resource-lock.js +299 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +12 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +7 -7
- package/src/__tests__/errors.test.ts +18 -6
- package/src/__tests__/harness-cache.test.ts +2 -2
- package/src/__tests__/harness.test.ts +342 -19
- package/src/__tests__/metro-port.test.ts +61 -0
- package/src/__tests__/resource-lock.test.ts +192 -0
- package/src/action-hooks.ts +66 -0
- package/src/errors.ts +23 -4
- package/src/harness.ts +609 -447
- package/src/logs.ts +24 -3
- package/src/metro-port.ts +76 -0
- package/src/resource-lock.ts +472 -0
- package/src/setup.ts +22 -2
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TestContext, TestResult } from '@jest/test-result';
|
|
2
|
+
import type { Config, TestRunnerOptions } from 'jest-runner';
|
|
3
|
+
import type { CustomEnvironment } from '@react-native-harness/cli';
|
|
4
|
+
export type RunTestOptions<ExtraOptions extends Record<string, unknown> = Record<string, unknown>> = {
|
|
5
|
+
config: Config.ProjectConfig;
|
|
6
|
+
extraOptions: ExtraOptions;
|
|
7
|
+
globalConfig: Config.GlobalConfig;
|
|
8
|
+
rawModuleMap: ReturnType<TestContext['moduleMap']['getRawModuleMap']> | null;
|
|
9
|
+
options: TestRunnerOptions;
|
|
10
|
+
testPath: string;
|
|
11
|
+
};
|
|
12
|
+
export type RunTest<ExtraOptions extends Record<string, unknown> = Record<string, unknown>> = (options: RunTestOptions<ExtraOptions>, environment: CustomEnvironment) => TestResult | Promise<TestResult>;
|
|
13
|
+
export interface TestDetail {
|
|
14
|
+
title: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,MAAM,MAAM,cAAc,CACxB,YAAY,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACpE;IACF,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClC,YAAY,EAAE,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7E,OAAO,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,OAAO,CACjB,YAAY,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACpE,CACF,OAAO,EAAE,cAAc,CAAC,YAAY,CAAC,EACrC,WAAW,EAAE,iBAAiB,KAC3B,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/jest",
|
|
3
|
-
"version": "1.1.0-rc.
|
|
3
|
+
"version": "1.1.0-rc.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"p-limit": "^7.1.1",
|
|
41
41
|
"tslib": "^2.3.0",
|
|
42
42
|
"yargs": "^17.7.2",
|
|
43
|
-
"@react-native-harness/
|
|
44
|
-
"@react-native-harness/
|
|
45
|
-
"@react-native-harness/
|
|
46
|
-
"@react-native-harness/
|
|
47
|
-
"@react-native-harness/
|
|
48
|
-
"@react-native-harness/
|
|
43
|
+
"@react-native-harness/bridge": "1.1.0-rc.4",
|
|
44
|
+
"@react-native-harness/plugins": "1.1.0-rc.4",
|
|
45
|
+
"@react-native-harness/platforms": "1.1.0-rc.4",
|
|
46
|
+
"@react-native-harness/tools": "1.1.0-rc.4",
|
|
47
|
+
"@react-native-harness/config": "1.1.0-rc.4",
|
|
48
|
+
"@react-native-harness/bundler-metro": "1.1.0-rc.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/yargs": "^17.0.32",
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { NativeCrashError } from '../errors.js';
|
|
2
|
+
import { NativeCrashError, PlatformReadyTimeoutError } from '../errors.js';
|
|
3
|
+
|
|
4
|
+
describe('PlatformReadyTimeoutError', () => {
|
|
5
|
+
it('includes the configured timeout and config hint', () => {
|
|
6
|
+
expect(new PlatformReadyTimeoutError(300000).message).toBe(
|
|
7
|
+
'The platform did not become ready within 300000ms. Increase "platformReadyTimeout" if your device, simulator, or emulator needs more time to start.'
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
3
11
|
|
|
4
12
|
describe('NativeCrashError', () => {
|
|
5
13
|
it('formats the extracted stack trace in the error message', () => {
|
|
@@ -35,7 +43,9 @@ describe('NativeCrashError', () => {
|
|
|
35
43
|
exceptionType: 'EXC_CRASH',
|
|
36
44
|
});
|
|
37
45
|
|
|
38
|
-
expect(error.message).not.toContain(
|
|
46
|
+
expect(error.message).not.toContain(
|
|
47
|
+
'notify_get_state check indicated test daemon not ready'
|
|
48
|
+
);
|
|
39
49
|
expect(error.message).toContain('Signal: SIGABRT');
|
|
40
50
|
expect(error.message).toContain('Exception: EXC_CRASH');
|
|
41
51
|
expect(error.message).toContain('Process: HarnessPlayground (pid 18007)');
|
|
@@ -54,9 +64,9 @@ describe('NativeCrashError', () => {
|
|
|
54
64
|
stackTrace: [frame],
|
|
55
65
|
});
|
|
56
66
|
|
|
57
|
-
expect(
|
|
58
|
-
|
|
59
|
-
);
|
|
67
|
+
expect(
|
|
68
|
+
error.message.match(/MainActivity\.onCreate\(MainActivity\.kt:38\)/g)
|
|
69
|
+
).toHaveLength(1);
|
|
60
70
|
});
|
|
61
71
|
|
|
62
72
|
it('collapses the native crash stack header so jest does not reprint multiline messages', () => {
|
|
@@ -65,6 +75,8 @@ describe('NativeCrashError', () => {
|
|
|
65
75
|
summary: ['line one', 'line two'].join('\n'),
|
|
66
76
|
});
|
|
67
77
|
|
|
68
|
-
expect(error.stack).toBe(
|
|
78
|
+
expect(error.stack).toBe(
|
|
79
|
+
'NativeCrashError: The native app crashed while preparing to run this test file.'
|
|
80
|
+
);
|
|
69
81
|
});
|
|
70
82
|
});
|
|
@@ -24,6 +24,7 @@ const platform: HarnessPlatform = {
|
|
|
24
24
|
platformId: 'ios',
|
|
25
25
|
runner: '/virtual/platform-runner.js',
|
|
26
26
|
config: {},
|
|
27
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const createHarnessConfig = (
|
|
@@ -31,14 +32,13 @@ const createHarnessConfig = (
|
|
|
31
32
|
): HarnessConfig =>
|
|
32
33
|
({
|
|
33
34
|
appRegistryComponentName: 'App',
|
|
34
|
-
webSocketPort: 8081,
|
|
35
35
|
disableViewFlattening: false,
|
|
36
36
|
bridgeTimeout: 5000,
|
|
37
37
|
entryPoint: 'index.js',
|
|
38
38
|
unstable__enableMetroCache: true,
|
|
39
39
|
forwardClientLogs: false,
|
|
40
40
|
...overrides,
|
|
41
|
-
}
|
|
41
|
+
} as HarnessConfig);
|
|
42
42
|
|
|
43
43
|
describe('maybeLogMetroCacheReuse', () => {
|
|
44
44
|
beforeEach(() => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { HARNESS_BRIDGE_PATH } from '@react-native-harness/bridge';
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
5
|
+
import { MetroPortRangeExhaustedError } from '../errors.js';
|
|
4
6
|
import { definePlugin } from '@react-native-harness/plugins';
|
|
5
7
|
import type {
|
|
6
8
|
AppMonitor,
|
|
@@ -24,18 +26,23 @@ const mocks = vi.hoisted(() => ({
|
|
|
24
26
|
getMetroInstance: vi.fn(),
|
|
25
27
|
isMetroCacheReusable: vi.fn(() => false),
|
|
26
28
|
logMetroCacheReused: vi.fn(),
|
|
29
|
+
logMetroPortFallback: vi.fn(),
|
|
30
|
+
logRunnerStarting: vi.fn(),
|
|
31
|
+
logRunnerStillWaitingInQueue: vi.fn(),
|
|
32
|
+
logRunnerWaitingInQueue: vi.fn(),
|
|
27
33
|
waitForMetroBackedAppReady: vi.fn(),
|
|
34
|
+
isPortAvailable: vi.fn(async () => true),
|
|
28
35
|
}));
|
|
29
36
|
|
|
30
37
|
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
31
|
-
const actual =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
38
|
+
const actual = await vi.importActual<
|
|
39
|
+
typeof import('@react-native-harness/bundler-metro')
|
|
40
|
+
>('@react-native-harness/bundler-metro');
|
|
35
41
|
|
|
36
42
|
return {
|
|
37
43
|
...actual,
|
|
38
44
|
getMetroInstance: mocks.getMetroInstance,
|
|
45
|
+
isPortAvailable: mocks.isPortAvailable,
|
|
39
46
|
isMetroCacheReusable: mocks.isMetroCacheReusable,
|
|
40
47
|
waitForMetroBackedAppReady: mocks.waitForMetroBackedAppReady,
|
|
41
48
|
};
|
|
@@ -47,13 +54,16 @@ vi.mock('@react-native-harness/bridge/server', () => ({
|
|
|
47
54
|
|
|
48
55
|
vi.mock('../logs.js', () => ({
|
|
49
56
|
logMetroCacheReused: mocks.logMetroCacheReused,
|
|
57
|
+
logMetroPortFallback: mocks.logMetroPortFallback,
|
|
58
|
+
logRunnerStarting: mocks.logRunnerStarting,
|
|
59
|
+
logRunnerStillWaitingInQueue: mocks.logRunnerStillWaitingInQueue,
|
|
60
|
+
logRunnerWaitingInQueue: mocks.logRunnerWaitingInQueue,
|
|
50
61
|
}));
|
|
51
62
|
|
|
52
63
|
vi.mock('@react-native-harness/tools', async () => {
|
|
53
|
-
const actual =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
64
|
+
const actual = await vi.importActual<
|
|
65
|
+
typeof import('@react-native-harness/tools')
|
|
66
|
+
>('@react-native-harness/tools');
|
|
57
67
|
|
|
58
68
|
return {
|
|
59
69
|
...actual,
|
|
@@ -62,13 +72,14 @@ vi.mock('@react-native-harness/tools', async () => {
|
|
|
62
72
|
});
|
|
63
73
|
|
|
64
74
|
import { getHarness, waitForAppReady } from '../harness.js';
|
|
65
|
-
import { StartupStallError } from '../errors.js';
|
|
75
|
+
import { PlatformReadyTimeoutError, StartupStallError } from '../errors.js';
|
|
66
76
|
|
|
67
77
|
const createBridgeServer = () => {
|
|
68
78
|
const emitter = new EventEmitter();
|
|
69
79
|
|
|
70
80
|
return {
|
|
71
81
|
serverBridge: {
|
|
82
|
+
ws: {} as BridgeServer['ws'],
|
|
72
83
|
rpc: {
|
|
73
84
|
clients: [],
|
|
74
85
|
},
|
|
@@ -117,6 +128,8 @@ const createMetroInstance = (
|
|
|
117
128
|
overrides: Partial<MetroInstance> = {}
|
|
118
129
|
): MetroInstance => ({
|
|
119
130
|
events: createReporter(),
|
|
131
|
+
httpServer: {} as never,
|
|
132
|
+
websocketEndpoints: {},
|
|
120
133
|
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
|
|
121
134
|
prewarm: vi.fn(async () => false),
|
|
122
135
|
dispose: vi.fn(async () => undefined),
|
|
@@ -176,16 +189,19 @@ const createHarnessConfig = (
|
|
|
176
189
|
forwardClientLogs: false,
|
|
177
190
|
maxAppRestarts: 2,
|
|
178
191
|
metroPort: 8081,
|
|
192
|
+
platformReadyTimeout: 300_000,
|
|
179
193
|
resetEnvironmentBetweenTestFiles: true,
|
|
180
194
|
runners: [],
|
|
181
195
|
unstable__enableMetroCache: false,
|
|
182
196
|
unstable__skipAlreadyIncludedModules: false,
|
|
183
197
|
webSocketPort: 3001,
|
|
184
198
|
...overrides,
|
|
185
|
-
}
|
|
199
|
+
} as HarnessConfig);
|
|
186
200
|
|
|
187
201
|
beforeEach(() => {
|
|
188
202
|
vi.clearAllMocks();
|
|
203
|
+
mocks.isPortAvailable.mockReset();
|
|
204
|
+
mocks.isPortAvailable.mockResolvedValue(true);
|
|
189
205
|
});
|
|
190
206
|
|
|
191
207
|
afterEach(() => {
|
|
@@ -291,6 +307,208 @@ describe('waitForAppReady', () => {
|
|
|
291
307
|
});
|
|
292
308
|
|
|
293
309
|
describe('getHarness', () => {
|
|
310
|
+
it('fails when the platform runner does not become ready within platformReadyTimeout', async () => {
|
|
311
|
+
const { serverBridge } = createBridgeServer();
|
|
312
|
+
const metroInstance = createMetroInstance();
|
|
313
|
+
|
|
314
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
315
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
316
|
+
|
|
317
|
+
(
|
|
318
|
+
globalThis as typeof globalThis & {
|
|
319
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
320
|
+
}
|
|
321
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(
|
|
322
|
+
async () =>
|
|
323
|
+
await new Promise((_, reject) => {
|
|
324
|
+
setTimeout(() => {
|
|
325
|
+
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
326
|
+
}, 20);
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const platform: HarnessPlatform = {
|
|
331
|
+
config: {},
|
|
332
|
+
getResourceLockKey: () => 'ios:test-platform-ready-timeout',
|
|
333
|
+
name: 'ios',
|
|
334
|
+
platformId: 'ios',
|
|
335
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
336
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
337
|
+
)}`,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await expect(
|
|
341
|
+
getHarness(
|
|
342
|
+
createHarnessConfig({
|
|
343
|
+
platformReadyTimeout: 10,
|
|
344
|
+
}),
|
|
345
|
+
platform,
|
|
346
|
+
'/tmp/project'
|
|
347
|
+
)
|
|
348
|
+
).rejects.toBeInstanceOf(PlatformReadyTimeoutError);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('passes a platform init signal to the runner factory', async () => {
|
|
352
|
+
const { serverBridge } = createBridgeServer();
|
|
353
|
+
const appMonitor = createAppMonitor();
|
|
354
|
+
const platformInstance = createPlatformRunner({
|
|
355
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
356
|
+
});
|
|
357
|
+
const metroInstance = createMetroInstance();
|
|
358
|
+
|
|
359
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
360
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
361
|
+
|
|
362
|
+
const runner = vi.fn(async () => platformInstance);
|
|
363
|
+
(
|
|
364
|
+
globalThis as typeof globalThis & {
|
|
365
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
366
|
+
}
|
|
367
|
+
).__HARNESS_PLATFORM_RUNNER__ = runner;
|
|
368
|
+
|
|
369
|
+
const platform: HarnessPlatform = {
|
|
370
|
+
config: {},
|
|
371
|
+
getResourceLockKey: () => 'ios:test-platform-init-signal',
|
|
372
|
+
name: 'ios',
|
|
373
|
+
platformId: 'ios',
|
|
374
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
375
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
376
|
+
)}`,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const harness = await getHarness(
|
|
380
|
+
createHarnessConfig(),
|
|
381
|
+
platform,
|
|
382
|
+
'/tmp/project'
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expect(runner).toHaveBeenCalledWith(
|
|
386
|
+
platform.config,
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
metroPort: 8081,
|
|
389
|
+
}),
|
|
390
|
+
expect.objectContaining({
|
|
391
|
+
signal: expect.any(AbortSignal),
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await harness.dispose();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('resolves and exposes a fallback Metro port before platform init', async () => {
|
|
399
|
+
const { serverBridge } = createBridgeServer();
|
|
400
|
+
const appMonitor = createAppMonitor();
|
|
401
|
+
const platformInstance = createPlatformRunner({
|
|
402
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
403
|
+
});
|
|
404
|
+
const metroInstance = createMetroInstance();
|
|
405
|
+
|
|
406
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
407
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
408
|
+
mocks.isPortAvailable
|
|
409
|
+
.mockResolvedValueOnce(false)
|
|
410
|
+
.mockResolvedValueOnce(true);
|
|
411
|
+
|
|
412
|
+
const runner = vi.fn(async () => platformInstance);
|
|
413
|
+
(
|
|
414
|
+
globalThis as typeof globalThis & {
|
|
415
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
416
|
+
}
|
|
417
|
+
).__HARNESS_PLATFORM_RUNNER__ = runner;
|
|
418
|
+
|
|
419
|
+
const platform: HarnessPlatform = {
|
|
420
|
+
config: {},
|
|
421
|
+
getResourceLockKey: () => 'android:emulator:Pixel_8_API_35',
|
|
422
|
+
name: 'android',
|
|
423
|
+
platformId: 'android',
|
|
424
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
425
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
426
|
+
)}`,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const harness = await getHarness(
|
|
430
|
+
createHarnessConfig(),
|
|
431
|
+
platform,
|
|
432
|
+
'/tmp/project'
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(harness.config.metroPort).toBe(8082);
|
|
436
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledWith(
|
|
437
|
+
expect.objectContaining({
|
|
438
|
+
harnessConfig: expect.objectContaining({
|
|
439
|
+
metroPort: 8082,
|
|
440
|
+
}),
|
|
441
|
+
}),
|
|
442
|
+
expect.any(AbortSignal)
|
|
443
|
+
);
|
|
444
|
+
expect(runner).toHaveBeenCalledWith(
|
|
445
|
+
platform.config,
|
|
446
|
+
expect.objectContaining({
|
|
447
|
+
metroPort: 8082,
|
|
448
|
+
}),
|
|
449
|
+
expect.objectContaining({
|
|
450
|
+
signal: expect.any(AbortSignal),
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
expect(mocks.logMetroPortFallback).toHaveBeenCalledWith(8081, 8082);
|
|
454
|
+
|
|
455
|
+
await harness.dispose();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('fails when no Metro port is available in the retry window', async () => {
|
|
459
|
+
mocks.isPortAvailable.mockResolvedValue(false);
|
|
460
|
+
|
|
461
|
+
const platform: HarnessPlatform = {
|
|
462
|
+
config: {},
|
|
463
|
+
getResourceLockKey: () => 'android:emulator:Pixel_8_API_35',
|
|
464
|
+
name: 'android',
|
|
465
|
+
platformId: 'android',
|
|
466
|
+
runner: 'data:text/javascript,export default async () => ({})',
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
await expect(
|
|
470
|
+
getHarness(createHarnessConfig(), platform, '/tmp/project')
|
|
471
|
+
).rejects.toBeInstanceOf(MetroPortRangeExhaustedError);
|
|
472
|
+
|
|
473
|
+
expect(mocks.getBridgeServer).not.toHaveBeenCalled();
|
|
474
|
+
expect(mocks.getMetroInstance).not.toHaveBeenCalled();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('falls back to a default resource lock key for platforms without getResourceLockKey', async () => {
|
|
478
|
+
const { serverBridge } = createBridgeServer();
|
|
479
|
+
const appMonitor = createAppMonitor();
|
|
480
|
+
const platformInstance = createPlatformRunner({
|
|
481
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
482
|
+
});
|
|
483
|
+
const metroInstance = createMetroInstance();
|
|
484
|
+
|
|
485
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
486
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
487
|
+
|
|
488
|
+
(
|
|
489
|
+
globalThis as typeof globalThis & {
|
|
490
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
491
|
+
}
|
|
492
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
493
|
+
|
|
494
|
+
const platform: HarnessPlatform = {
|
|
495
|
+
config: {},
|
|
496
|
+
name: 'legacy-ios',
|
|
497
|
+
platformId: 'ios',
|
|
498
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
499
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
500
|
+
)}`,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const harness = await getHarness(
|
|
504
|
+
createHarnessConfig(),
|
|
505
|
+
platform,
|
|
506
|
+
'/tmp/project'
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
await harness.dispose();
|
|
510
|
+
});
|
|
511
|
+
|
|
294
512
|
it('routes ensureAppReady through the shared Metro startup helper', async () => {
|
|
295
513
|
const { serverBridge, emitReady } = createBridgeServer();
|
|
296
514
|
const appMonitor = createAppMonitor();
|
|
@@ -306,9 +524,7 @@ describe('getHarness', () => {
|
|
|
306
524
|
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
307
525
|
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
308
526
|
await options.startAttempt();
|
|
309
|
-
const readyPromise = options.waitForReady(
|
|
310
|
-
new AbortController().signal
|
|
311
|
-
);
|
|
527
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
312
528
|
emitReady();
|
|
313
529
|
await readyPromise;
|
|
314
530
|
}
|
|
@@ -333,10 +549,13 @@ describe('getHarness', () => {
|
|
|
333
549
|
runner: `data:text/javascript,${encodeURIComponent(
|
|
334
550
|
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
335
551
|
)}`,
|
|
552
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
336
553
|
};
|
|
337
554
|
|
|
338
555
|
const harness = await getHarness(
|
|
339
|
-
createHarnessConfig(
|
|
556
|
+
createHarnessConfig({
|
|
557
|
+
bridgeTimeout: 1,
|
|
558
|
+
}),
|
|
340
559
|
platform,
|
|
341
560
|
'/tmp/project'
|
|
342
561
|
);
|
|
@@ -371,9 +590,7 @@ describe('getHarness', () => {
|
|
|
371
590
|
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
372
591
|
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
373
592
|
await options.startAttempt();
|
|
374
|
-
const readyPromise = options.waitForReady(
|
|
375
|
-
new AbortController().signal
|
|
376
|
-
);
|
|
593
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
377
594
|
emitReady();
|
|
378
595
|
await readyPromise;
|
|
379
596
|
}
|
|
@@ -398,6 +615,7 @@ describe('getHarness', () => {
|
|
|
398
615
|
runner: `data:text/javascript,${encodeURIComponent(
|
|
399
616
|
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
400
617
|
)}`,
|
|
618
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
401
619
|
};
|
|
402
620
|
|
|
403
621
|
const harness = await getHarness(
|
|
@@ -406,6 +624,19 @@ describe('getHarness', () => {
|
|
|
406
624
|
'/tmp/project'
|
|
407
625
|
);
|
|
408
626
|
|
|
627
|
+
expect(mocks.getBridgeServer).toHaveBeenCalledWith(
|
|
628
|
+
expect.objectContaining({
|
|
629
|
+
noServer: true,
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledWith(
|
|
633
|
+
expect.objectContaining({
|
|
634
|
+
websocketEndpoints: {
|
|
635
|
+
[HARNESS_BRIDGE_PATH]: serverBridge.ws,
|
|
636
|
+
},
|
|
637
|
+
}),
|
|
638
|
+
expect.any(AbortSignal)
|
|
639
|
+
);
|
|
409
640
|
await harness.restart('/tmp/restart.harness.ts');
|
|
410
641
|
|
|
411
642
|
expect(stopApp).toHaveBeenCalledTimes(1);
|
|
@@ -456,7 +687,25 @@ describe('plugins', () => {
|
|
|
456
687
|
beforeCreation: (ctx) => {
|
|
457
688
|
ctx.state.creationCount += 1;
|
|
458
689
|
observedHooks.push(
|
|
459
|
-
`beforeCreation:${ctx.platform.platformId}:${
|
|
690
|
+
`beforeCreation:${ctx.platform.platformId}:${
|
|
691
|
+
ctx.appLaunchOptions == null
|
|
692
|
+
? 'no-launch-options'
|
|
693
|
+
: 'launch-options'
|
|
694
|
+
}`
|
|
695
|
+
);
|
|
696
|
+
},
|
|
697
|
+
beforeRun: (ctx) => {
|
|
698
|
+
observedHooks.push(
|
|
699
|
+
`beforeRun:${ctx.platform.platformId}:${
|
|
700
|
+
ctx.appLaunchOptions == null
|
|
701
|
+
? 'no-launch-options'
|
|
702
|
+
: 'launch-options'
|
|
703
|
+
}`
|
|
704
|
+
);
|
|
705
|
+
},
|
|
706
|
+
afterRun: (ctx) => {
|
|
707
|
+
observedHooks.push(
|
|
708
|
+
`afterRun:${ctx.state.creationCount}:${ctx.reason}`
|
|
460
709
|
);
|
|
461
710
|
},
|
|
462
711
|
beforeDispose: (ctx) => {
|
|
@@ -467,7 +716,9 @@ describe('plugins', () => {
|
|
|
467
716
|
},
|
|
468
717
|
runtime: {
|
|
469
718
|
ready: (ctx) => {
|
|
470
|
-
observedHooks.push(
|
|
719
|
+
observedHooks.push(
|
|
720
|
+
`runtime.ready:${ctx.runId}:${ctx.device.platform}`
|
|
721
|
+
);
|
|
471
722
|
},
|
|
472
723
|
disconnected: (ctx) => {
|
|
473
724
|
observedHooks.push(`runtime.disconnected:${ctx.reason}`);
|
|
@@ -494,6 +745,7 @@ describe('plugins', () => {
|
|
|
494
745
|
runner: `data:text/javascript,${encodeURIComponent(
|
|
495
746
|
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
496
747
|
)}`,
|
|
748
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
497
749
|
};
|
|
498
750
|
|
|
499
751
|
const harness = await getHarness(
|
|
@@ -530,12 +782,83 @@ describe('plugins', () => {
|
|
|
530
782
|
|
|
531
783
|
expect(observedHooks).toEqual([
|
|
532
784
|
'beforeCreation:ios:launch-options',
|
|
785
|
+
'beforeRun:ios:launch-options',
|
|
533
786
|
'runtime.ready:run-1:ios',
|
|
534
787
|
'collection.started:example.harness.ts',
|
|
535
788
|
'runtime.disconnected:bridge-disconnected',
|
|
789
|
+
'afterRun:1:normal',
|
|
536
790
|
'beforeDispose:1:normal',
|
|
537
791
|
]);
|
|
538
792
|
});
|
|
793
|
+
|
|
794
|
+
it('waits in queue before starting Metro and releases the lock on dispose', async () => {
|
|
795
|
+
const resourceKey = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
796
|
+
const firstPlatformRunner = createPlatformRunner();
|
|
797
|
+
const secondPlatformRunner = createPlatformRunner();
|
|
798
|
+
const secondAppMonitor = createAppMonitor();
|
|
799
|
+
const firstMetroInstance = createMetroInstance();
|
|
800
|
+
const secondMetroInstance = createMetroInstance();
|
|
801
|
+
const firstBridge = createBridgeServer();
|
|
802
|
+
const secondBridge = createBridgeServer();
|
|
803
|
+
|
|
804
|
+
mocks.getBridgeServer
|
|
805
|
+
.mockResolvedValueOnce(firstBridge.serverBridge)
|
|
806
|
+
.mockResolvedValueOnce(secondBridge.serverBridge);
|
|
807
|
+
mocks.getMetroInstance
|
|
808
|
+
.mockResolvedValueOnce(firstMetroInstance)
|
|
809
|
+
.mockResolvedValueOnce(secondMetroInstance);
|
|
810
|
+
|
|
811
|
+
let invocationCount = 0;
|
|
812
|
+
(
|
|
813
|
+
globalThis as typeof globalThis & {
|
|
814
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
815
|
+
}
|
|
816
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => {
|
|
817
|
+
invocationCount += 1;
|
|
818
|
+
return invocationCount === 1
|
|
819
|
+
? firstPlatformRunner
|
|
820
|
+
: createPlatformRunner({
|
|
821
|
+
createAppMonitor: () => secondAppMonitor.appMonitor,
|
|
822
|
+
dispose: secondPlatformRunner.dispose,
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const platform: HarnessPlatform = {
|
|
827
|
+
config: {},
|
|
828
|
+
name: 'ios',
|
|
829
|
+
platformId: 'ios',
|
|
830
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
831
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
832
|
+
)}`,
|
|
833
|
+
getResourceLockKey: () => resourceKey,
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const firstHarness = await getHarness(
|
|
837
|
+
createHarnessConfig(),
|
|
838
|
+
platform,
|
|
839
|
+
'/tmp/project'
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
const secondHarnessPromise = getHarness(
|
|
843
|
+
createHarnessConfig(),
|
|
844
|
+
platform,
|
|
845
|
+
'/tmp/project'
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
849
|
+
|
|
850
|
+
expect(mocks.logRunnerWaitingInQueue).toHaveBeenCalledWith(platform);
|
|
851
|
+
expect(mocks.logRunnerStarting).not.toHaveBeenCalled();
|
|
852
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledTimes(1);
|
|
853
|
+
|
|
854
|
+
await firstHarness.dispose();
|
|
855
|
+
const secondHarness = await secondHarnessPromise;
|
|
856
|
+
|
|
857
|
+
expect(mocks.logRunnerStarting).toHaveBeenCalledWith(platform);
|
|
858
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledTimes(2);
|
|
859
|
+
|
|
860
|
+
await secondHarness.dispose();
|
|
861
|
+
});
|
|
539
862
|
});
|
|
540
863
|
|
|
541
864
|
describe('StartupStallError', () => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
3
|
+
import type { HarnessPlatform } from '@react-native-harness/platforms';
|
|
4
|
+
import { resolveHarnessMetroPort } from '../metro-port.js';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
isPortAvailable: vi.fn(async () => true),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('@react-native-harness/bundler-metro', () => ({
|
|
11
|
+
isPortAvailable: mocks.isPortAvailable,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const createConfig = (overrides: Partial<HarnessConfig> = {}): HarnessConfig =>
|
|
15
|
+
({
|
|
16
|
+
appRegistryComponentName: 'App',
|
|
17
|
+
bridgeTimeout: 60_000,
|
|
18
|
+
bundleStartTimeout: 60_000,
|
|
19
|
+
crashDetectionInterval: 500,
|
|
20
|
+
defaultRunner: 'ios-device',
|
|
21
|
+
detectNativeCrashes: true,
|
|
22
|
+
disableViewFlattening: false,
|
|
23
|
+
entryPoint: 'index.js',
|
|
24
|
+
forwardClientLogs: false,
|
|
25
|
+
maxAppRestarts: 2,
|
|
26
|
+
metroPort: 8081,
|
|
27
|
+
platformReadyTimeout: 300_000,
|
|
28
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
29
|
+
runners: [],
|
|
30
|
+
unstable__enableMetroCache: false,
|
|
31
|
+
unstable__skipAlreadyIncludedModules: false,
|
|
32
|
+
...overrides,
|
|
33
|
+
} as HarnessConfig);
|
|
34
|
+
|
|
35
|
+
describe('resolveHarnessMetroPort', () => {
|
|
36
|
+
it('skips fallback allocation for iOS physical device runners', async () => {
|
|
37
|
+
const acquire = vi.fn();
|
|
38
|
+
const config = createConfig();
|
|
39
|
+
const platform: HarnessPlatform = {
|
|
40
|
+
config: {},
|
|
41
|
+
name: 'ios-device',
|
|
42
|
+
platformId: 'ios',
|
|
43
|
+
runner: 'unused',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = await resolveHarnessMetroPort({
|
|
47
|
+
config,
|
|
48
|
+
platform,
|
|
49
|
+
resourceLockManager: {
|
|
50
|
+
acquire,
|
|
51
|
+
},
|
|
52
|
+
signal: new AbortController().signal,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.config).toBe(config);
|
|
56
|
+
expect(result.metroPortLease).toBeNull();
|
|
57
|
+
expect(result.didFallback).toBe(false);
|
|
58
|
+
expect(acquire).not.toHaveBeenCalled();
|
|
59
|
+
expect(mocks.isPortAvailable).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|