@react-native-harness/jest 1.1.0-rc.2 → 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.js +1 -1
- package/dist/__tests__/harness.test.js +147 -3
- 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.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 +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -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.map +1 -1
- package/dist/harness.js +478 -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 +3 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +9 -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/setup.d.ts.map +1 -1
- package/dist/setup.js +11 -0
- 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 +254 -19
- package/src/__tests__/resource-lock.test.ts +138 -0
- package/src/action-hooks.ts +66 -0
- package/src/errors.ts +10 -4
- package/src/harness.ts +585 -445
- package/src/logs.ts +13 -3
- package/src/resource-lock.ts +450 -0
- package/src/setup.ts +21 -1
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.3",
|
|
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/tools": "1.1.0-rc.
|
|
48
|
-
"@react-native-harness/
|
|
43
|
+
"@react-native-harness/bridge": "1.1.0-rc.3",
|
|
44
|
+
"@react-native-harness/config": "1.1.0-rc.3",
|
|
45
|
+
"@react-native-harness/bundler-metro": "1.1.0-rc.3",
|
|
46
|
+
"@react-native-harness/platforms": "1.1.0-rc.3",
|
|
47
|
+
"@react-native-harness/tools": "1.1.0-rc.3",
|
|
48
|
+
"@react-native-harness/plugins": "1.1.0-rc.3"
|
|
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,4 +1,5 @@
|
|
|
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';
|
|
4
5
|
import { definePlugin } from '@react-native-harness/plugins';
|
|
@@ -24,14 +25,16 @@ const mocks = vi.hoisted(() => ({
|
|
|
24
25
|
getMetroInstance: vi.fn(),
|
|
25
26
|
isMetroCacheReusable: vi.fn(() => false),
|
|
26
27
|
logMetroCacheReused: vi.fn(),
|
|
28
|
+
logRunnerStarting: vi.fn(),
|
|
29
|
+
logRunnerStillWaitingInQueue: vi.fn(),
|
|
30
|
+
logRunnerWaitingInQueue: vi.fn(),
|
|
27
31
|
waitForMetroBackedAppReady: vi.fn(),
|
|
28
32
|
}));
|
|
29
33
|
|
|
30
34
|
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
31
|
-
const actual =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
35
|
+
const actual = await vi.importActual<
|
|
36
|
+
typeof import('@react-native-harness/bundler-metro')
|
|
37
|
+
>('@react-native-harness/bundler-metro');
|
|
35
38
|
|
|
36
39
|
return {
|
|
37
40
|
...actual,
|
|
@@ -47,13 +50,15 @@ vi.mock('@react-native-harness/bridge/server', () => ({
|
|
|
47
50
|
|
|
48
51
|
vi.mock('../logs.js', () => ({
|
|
49
52
|
logMetroCacheReused: mocks.logMetroCacheReused,
|
|
53
|
+
logRunnerStarting: mocks.logRunnerStarting,
|
|
54
|
+
logRunnerStillWaitingInQueue: mocks.logRunnerStillWaitingInQueue,
|
|
55
|
+
logRunnerWaitingInQueue: mocks.logRunnerWaitingInQueue,
|
|
50
56
|
}));
|
|
51
57
|
|
|
52
58
|
vi.mock('@react-native-harness/tools', async () => {
|
|
53
|
-
const actual =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
59
|
+
const actual = await vi.importActual<
|
|
60
|
+
typeof import('@react-native-harness/tools')
|
|
61
|
+
>('@react-native-harness/tools');
|
|
57
62
|
|
|
58
63
|
return {
|
|
59
64
|
...actual,
|
|
@@ -62,13 +67,14 @@ vi.mock('@react-native-harness/tools', async () => {
|
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
import { getHarness, waitForAppReady } from '../harness.js';
|
|
65
|
-
import { StartupStallError } from '../errors.js';
|
|
70
|
+
import { PlatformReadyTimeoutError, StartupStallError } from '../errors.js';
|
|
66
71
|
|
|
67
72
|
const createBridgeServer = () => {
|
|
68
73
|
const emitter = new EventEmitter();
|
|
69
74
|
|
|
70
75
|
return {
|
|
71
76
|
serverBridge: {
|
|
77
|
+
ws: {} as BridgeServer['ws'],
|
|
72
78
|
rpc: {
|
|
73
79
|
clients: [],
|
|
74
80
|
},
|
|
@@ -117,6 +123,8 @@ const createMetroInstance = (
|
|
|
117
123
|
overrides: Partial<MetroInstance> = {}
|
|
118
124
|
): MetroInstance => ({
|
|
119
125
|
events: createReporter(),
|
|
126
|
+
httpServer: {} as never,
|
|
127
|
+
websocketEndpoints: {},
|
|
120
128
|
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
|
|
121
129
|
prewarm: vi.fn(async () => false),
|
|
122
130
|
dispose: vi.fn(async () => undefined),
|
|
@@ -176,13 +184,14 @@ const createHarnessConfig = (
|
|
|
176
184
|
forwardClientLogs: false,
|
|
177
185
|
maxAppRestarts: 2,
|
|
178
186
|
metroPort: 8081,
|
|
187
|
+
platformReadyTimeout: 300_000,
|
|
179
188
|
resetEnvironmentBetweenTestFiles: true,
|
|
180
189
|
runners: [],
|
|
181
190
|
unstable__enableMetroCache: false,
|
|
182
191
|
unstable__skipAlreadyIncludedModules: false,
|
|
183
192
|
webSocketPort: 3001,
|
|
184
193
|
...overrides,
|
|
185
|
-
}
|
|
194
|
+
} as HarnessConfig);
|
|
186
195
|
|
|
187
196
|
beforeEach(() => {
|
|
188
197
|
vi.clearAllMocks();
|
|
@@ -291,6 +300,127 @@ describe('waitForAppReady', () => {
|
|
|
291
300
|
});
|
|
292
301
|
|
|
293
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
|
+
});
|
|
343
|
+
|
|
344
|
+
it('passes a platform init signal to the runner factory', async () => {
|
|
345
|
+
const { serverBridge } = createBridgeServer();
|
|
346
|
+
const appMonitor = createAppMonitor();
|
|
347
|
+
const platformInstance = createPlatformRunner({
|
|
348
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
349
|
+
});
|
|
350
|
+
const metroInstance = createMetroInstance();
|
|
351
|
+
|
|
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
|
+
|
|
294
424
|
it('routes ensureAppReady through the shared Metro startup helper', async () => {
|
|
295
425
|
const { serverBridge, emitReady } = createBridgeServer();
|
|
296
426
|
const appMonitor = createAppMonitor();
|
|
@@ -306,9 +436,7 @@ describe('getHarness', () => {
|
|
|
306
436
|
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
307
437
|
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
308
438
|
await options.startAttempt();
|
|
309
|
-
const readyPromise = options.waitForReady(
|
|
310
|
-
new AbortController().signal
|
|
311
|
-
);
|
|
439
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
312
440
|
emitReady();
|
|
313
441
|
await readyPromise;
|
|
314
442
|
}
|
|
@@ -333,10 +461,13 @@ describe('getHarness', () => {
|
|
|
333
461
|
runner: `data:text/javascript,${encodeURIComponent(
|
|
334
462
|
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
335
463
|
)}`,
|
|
464
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
336
465
|
};
|
|
337
466
|
|
|
338
467
|
const harness = await getHarness(
|
|
339
|
-
createHarnessConfig(
|
|
468
|
+
createHarnessConfig({
|
|
469
|
+
bridgeTimeout: 1,
|
|
470
|
+
}),
|
|
340
471
|
platform,
|
|
341
472
|
'/tmp/project'
|
|
342
473
|
);
|
|
@@ -371,9 +502,7 @@ describe('getHarness', () => {
|
|
|
371
502
|
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
372
503
|
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
373
504
|
await options.startAttempt();
|
|
374
|
-
const readyPromise = options.waitForReady(
|
|
375
|
-
new AbortController().signal
|
|
376
|
-
);
|
|
505
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
377
506
|
emitReady();
|
|
378
507
|
await readyPromise;
|
|
379
508
|
}
|
|
@@ -398,6 +527,7 @@ describe('getHarness', () => {
|
|
|
398
527
|
runner: `data:text/javascript,${encodeURIComponent(
|
|
399
528
|
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
400
529
|
)}`,
|
|
530
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
401
531
|
};
|
|
402
532
|
|
|
403
533
|
const harness = await getHarness(
|
|
@@ -406,6 +536,19 @@ describe('getHarness', () => {
|
|
|
406
536
|
'/tmp/project'
|
|
407
537
|
);
|
|
408
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
|
+
);
|
|
409
552
|
await harness.restart('/tmp/restart.harness.ts');
|
|
410
553
|
|
|
411
554
|
expect(stopApp).toHaveBeenCalledTimes(1);
|
|
@@ -456,7 +599,25 @@ describe('plugins', () => {
|
|
|
456
599
|
beforeCreation: (ctx) => {
|
|
457
600
|
ctx.state.creationCount += 1;
|
|
458
601
|
observedHooks.push(
|
|
459
|
-
`beforeCreation:${ctx.platform.platformId}:${
|
|
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}`
|
|
460
621
|
);
|
|
461
622
|
},
|
|
462
623
|
beforeDispose: (ctx) => {
|
|
@@ -467,7 +628,9 @@ describe('plugins', () => {
|
|
|
467
628
|
},
|
|
468
629
|
runtime: {
|
|
469
630
|
ready: (ctx) => {
|
|
470
|
-
observedHooks.push(
|
|
631
|
+
observedHooks.push(
|
|
632
|
+
`runtime.ready:${ctx.runId}:${ctx.device.platform}`
|
|
633
|
+
);
|
|
471
634
|
},
|
|
472
635
|
disconnected: (ctx) => {
|
|
473
636
|
observedHooks.push(`runtime.disconnected:${ctx.reason}`);
|
|
@@ -494,6 +657,7 @@ describe('plugins', () => {
|
|
|
494
657
|
runner: `data:text/javascript,${encodeURIComponent(
|
|
495
658
|
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
496
659
|
)}`,
|
|
660
|
+
getResourceLockKey: () => 'ios:simulator:iPhone 17 Pro:26.2',
|
|
497
661
|
};
|
|
498
662
|
|
|
499
663
|
const harness = await getHarness(
|
|
@@ -530,12 +694,83 @@ describe('plugins', () => {
|
|
|
530
694
|
|
|
531
695
|
expect(observedHooks).toEqual([
|
|
532
696
|
'beforeCreation:ios:launch-options',
|
|
697
|
+
'beforeRun:ios:launch-options',
|
|
533
698
|
'runtime.ready:run-1:ios',
|
|
534
699
|
'collection.started:example.harness.ts',
|
|
535
700
|
'runtime.disconnected:bridge-disconnected',
|
|
701
|
+
'afterRun:1:normal',
|
|
536
702
|
'beforeDispose:1:normal',
|
|
537
703
|
]);
|
|
538
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
|
+
});
|
|
539
774
|
});
|
|
540
775
|
|
|
541
776
|
describe('StartupStallError', () => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createResourceLockManager,
|
|
7
|
+
hashResourceLockKey,
|
|
8
|
+
} from '../resource-lock.js';
|
|
9
|
+
|
|
10
|
+
describe('resource lock manager', () => {
|
|
11
|
+
let rootDir: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
rootDir = await fs.mkdtemp(
|
|
15
|
+
path.join(os.tmpdir(), 'react-native-harness-resource-lock-test-')
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(rootDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('queues access in FIFO order', async () => {
|
|
24
|
+
const manager = createResourceLockManager({
|
|
25
|
+
rootDir,
|
|
26
|
+
pollIntervalMs: 5,
|
|
27
|
+
heartbeatIntervalMs: 20,
|
|
28
|
+
staleLockTimeoutMs: 200,
|
|
29
|
+
});
|
|
30
|
+
const order: string[] = [];
|
|
31
|
+
|
|
32
|
+
const firstLease = await manager.acquire(
|
|
33
|
+
'ios:simulator:iPhone 17 Pro:26.2'
|
|
34
|
+
);
|
|
35
|
+
const secondAcquire = manager
|
|
36
|
+
.acquire('ios:simulator:iPhone 17 Pro:26.2', {
|
|
37
|
+
onWait: () => {
|
|
38
|
+
order.push('waiting');
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
.then(async (lease) => {
|
|
42
|
+
order.push('acquired');
|
|
43
|
+
await lease.release();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
47
|
+
expect(order).toEqual(['waiting']);
|
|
48
|
+
|
|
49
|
+
await firstLease.release();
|
|
50
|
+
await secondAcquire;
|
|
51
|
+
|
|
52
|
+
expect(order).toEqual(['waiting', 'acquired']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('removes the queued ticket when waiting is aborted', async () => {
|
|
56
|
+
const manager = createResourceLockManager({
|
|
57
|
+
rootDir,
|
|
58
|
+
pollIntervalMs: 5,
|
|
59
|
+
heartbeatIntervalMs: 20,
|
|
60
|
+
staleLockTimeoutMs: 200,
|
|
61
|
+
});
|
|
62
|
+
const key = 'android:emulator:Pixel_8_API_35';
|
|
63
|
+
const firstLease = await manager.acquire(key);
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
|
|
66
|
+
const acquirePromise = manager.acquire(key, {
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
71
|
+
controller.abort();
|
|
72
|
+
|
|
73
|
+
await expect(acquirePromise).rejects.toMatchObject({
|
|
74
|
+
name: 'AbortError',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const queueDir = path.join(rootDir, hashResourceLockKey(key), 'queue');
|
|
78
|
+
const queuedEntries = await fs.readdir(queueDir);
|
|
79
|
+
expect(queuedEntries).toHaveLength(0);
|
|
80
|
+
|
|
81
|
+
await firstLease.release();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('keeps queued tickets alive while the waiting process is still active', async () => {
|
|
85
|
+
const manager = createResourceLockManager({
|
|
86
|
+
rootDir,
|
|
87
|
+
pollIntervalMs: 5,
|
|
88
|
+
heartbeatIntervalMs: 20,
|
|
89
|
+
staleLockTimeoutMs: 30,
|
|
90
|
+
isProcessActive: () => true,
|
|
91
|
+
});
|
|
92
|
+
const key = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
93
|
+
const firstLease = await manager.acquire(key);
|
|
94
|
+
|
|
95
|
+
const secondAcquire = manager.acquire(key);
|
|
96
|
+
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
98
|
+
|
|
99
|
+
await firstLease.release();
|
|
100
|
+
const secondLease = await secondAcquire;
|
|
101
|
+
await secondLease.release();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('reclaims a stale owner before granting the lock', async () => {
|
|
105
|
+
const manager = createResourceLockManager({
|
|
106
|
+
rootDir,
|
|
107
|
+
pollIntervalMs: 5,
|
|
108
|
+
heartbeatIntervalMs: 20,
|
|
109
|
+
staleLockTimeoutMs: 50,
|
|
110
|
+
isProcessActive: () => false,
|
|
111
|
+
});
|
|
112
|
+
const key = 'web:browser:chromium';
|
|
113
|
+
const keyDir = path.join(rootDir, hashResourceLockKey(key));
|
|
114
|
+
const queueDir = path.join(keyDir, 'queue');
|
|
115
|
+
const ownerFilePath = path.join(keyDir, 'owner.json');
|
|
116
|
+
|
|
117
|
+
await fs.mkdir(queueDir, { recursive: true });
|
|
118
|
+
await fs.writeFile(
|
|
119
|
+
ownerFilePath,
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
ticketId: 'stale-owner',
|
|
122
|
+
key,
|
|
123
|
+
pid: 999999,
|
|
124
|
+
createdAt: Date.now() - 1000,
|
|
125
|
+
heartbeatAt: Date.now() - 1000,
|
|
126
|
+
}),
|
|
127
|
+
'utf8'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const lease = await manager.acquire(key);
|
|
131
|
+
const owner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8')) as {
|
|
132
|
+
ticketId: string;
|
|
133
|
+
};
|
|
134
|
+
expect(owner.ticketId).not.toBe('stale-owner');
|
|
135
|
+
|
|
136
|
+
await lease.release();
|
|
137
|
+
});
|
|
138
|
+
});
|