@react-native-harness/jest 1.1.0-rc.1 → 1.1.0-rc.2
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__/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 +340 -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/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 +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/harness.d.ts +23 -1
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +396 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -58
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -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 +18 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/__tests__/harness-cache.test.ts +74 -0
- package/src/__tests__/harness.test.ts +469 -51
- package/src/__tests__/index.test.ts +61 -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 +5 -0
- package/src/harness.ts +504 -58
- package/src/index.ts +276 -64
- package/src/logs.ts +4 -0
- package/src/run.ts +20 -10
- package/src/setup.ts +22 -1
- package/tsconfig.json +3 -3
- package/tsconfig.lib.json +6 -6
- package/dist/cli-args.test.d.ts +0 -2
- package/dist/cli-args.test.d.ts.map +0 -1
- package/dist/cli-args.test.js +0 -1
- package/dist/crash-monitor.d.ts +0 -16
- package/dist/crash-monitor.d.ts.map +0 -1
- package/dist/crash-monitor.js +0 -75
- package/dist/event-mapper.d.ts +0 -16
- package/dist/event-mapper.d.ts.map +0 -1
- package/dist/event-mapper.js +0 -120
- package/dist/global-setup.d.ts +0 -3
- package/dist/global-setup.d.ts.map +0 -1
- package/dist/global-setup.js +0 -60
- package/dist/global-teardown.d.ts +0 -3
- package/dist/global-teardown.d.ts.map +0 -1
- package/dist/global-teardown.js +0 -11
- package/dist/harness-reporter.d.ts +0 -8
- package/dist/harness-reporter.d.ts.map +0 -1
- package/dist/harness-reporter.js +0 -12
- package/dist/jest-options.d.ts +0 -2
- package/dist/jest-options.d.ts.map +0 -1
- package/dist/jest-options.js +0 -1
- package/dist/types.d.ts +0 -17
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
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.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -40,11 +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/bundler-metro": "1.1.0-rc.
|
|
45
|
-
"@react-native-harness/
|
|
46
|
-
"@react-native-harness/
|
|
47
|
-
"@react-native-harness/tools": "1.1.0-rc.
|
|
43
|
+
"@react-native-harness/config": "1.1.0-rc.2",
|
|
44
|
+
"@react-native-harness/bundler-metro": "1.1.0-rc.2",
|
|
45
|
+
"@react-native-harness/bridge": "1.1.0-rc.2",
|
|
46
|
+
"@react-native-harness/plugins": "1.1.0-rc.2",
|
|
47
|
+
"@react-native-harness/tools": "1.1.0-rc.2",
|
|
48
|
+
"@react-native-harness/platforms": "1.1.0-rc.2"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/yargs": "^17.0.32",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { beforeEach, 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
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
isMetroCacheReusable: vi.fn(),
|
|
7
|
+
logMetroCacheReused: vi.fn(),
|
|
8
|
+
logMetroPrewarmCompleted: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@react-native-harness/bundler-metro', () => ({
|
|
12
|
+
isMetroCacheReusable: mocks.isMetroCacheReusable,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../logs.js', () => ({
|
|
16
|
+
logMetroCacheReused: mocks.logMetroCacheReused,
|
|
17
|
+
logMetroPrewarmCompleted: mocks.logMetroPrewarmCompleted,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { maybeLogMetroCacheReuse } from '../harness.js';
|
|
21
|
+
|
|
22
|
+
const platform: HarnessPlatform = {
|
|
23
|
+
name: 'ios',
|
|
24
|
+
platformId: 'ios',
|
|
25
|
+
runner: '/virtual/platform-runner.js',
|
|
26
|
+
config: {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const createHarnessConfig = (
|
|
30
|
+
overrides: Partial<HarnessConfig> = {}
|
|
31
|
+
): HarnessConfig =>
|
|
32
|
+
({
|
|
33
|
+
appRegistryComponentName: 'App',
|
|
34
|
+
webSocketPort: 8081,
|
|
35
|
+
disableViewFlattening: false,
|
|
36
|
+
bridgeTimeout: 5000,
|
|
37
|
+
entryPoint: 'index.js',
|
|
38
|
+
unstable__enableMetroCache: true,
|
|
39
|
+
forwardClientLogs: false,
|
|
40
|
+
...overrides,
|
|
41
|
+
}) as HarnessConfig;
|
|
42
|
+
|
|
43
|
+
describe('maybeLogMetroCacheReuse', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
mocks.isMetroCacheReusable.mockReturnValue(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('logs when an existing metro cache will be reused', () => {
|
|
50
|
+
mocks.isMetroCacheReusable.mockReturnValue(true);
|
|
51
|
+
|
|
52
|
+
maybeLogMetroCacheReuse(createHarnessConfig(), platform, '/tmp/project');
|
|
53
|
+
|
|
54
|
+
expect(mocks.isMetroCacheReusable).toHaveBeenCalledWith('/tmp/project');
|
|
55
|
+
expect(mocks.logMetroCacheReused).toHaveBeenCalledWith(platform);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not log when metro cache reuse is disabled', () => {
|
|
59
|
+
maybeLogMetroCacheReuse(
|
|
60
|
+
createHarnessConfig({ unstable__enableMetroCache: false }),
|
|
61
|
+
platform,
|
|
62
|
+
'/tmp/project'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(mocks.isMetroCacheReusable).not.toHaveBeenCalled();
|
|
66
|
+
expect(mocks.logMetroCacheReused).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not log when the metro cache is absent', () => {
|
|
70
|
+
maybeLogMetroCacheReuse(createHarnessConfig(), platform, '/tmp/project');
|
|
71
|
+
|
|
72
|
+
expect(mocks.logMetroCacheReused).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,106 +1,276 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
4
|
+
import { definePlugin } from '@react-native-harness/plugins';
|
|
5
5
|
import type {
|
|
6
6
|
AppMonitor,
|
|
7
|
+
AppMonitorEvent,
|
|
7
8
|
AppMonitorListener,
|
|
9
|
+
HarnessPlatform,
|
|
8
10
|
HarnessPlatformRunner,
|
|
9
11
|
} from '@react-native-harness/platforms';
|
|
12
|
+
import type { BridgeServer } from '@react-native-harness/bridge/server';
|
|
13
|
+
import type {
|
|
14
|
+
MetroInstance,
|
|
15
|
+
Reporter,
|
|
16
|
+
ReportableEvent,
|
|
17
|
+
WaitForMetroBackedAppReadyOptions,
|
|
18
|
+
} from '@react-native-harness/bundler-metro';
|
|
10
19
|
import { createCrashSupervisor } from '../crash-supervisor.js';
|
|
11
20
|
|
|
21
|
+
const mocks = vi.hoisted(() => ({
|
|
22
|
+
createCrashArtifactWriter: vi.fn(() => ({})),
|
|
23
|
+
getBridgeServer: vi.fn(),
|
|
24
|
+
getMetroInstance: vi.fn(),
|
|
25
|
+
isMetroCacheReusable: vi.fn(() => false),
|
|
26
|
+
logMetroCacheReused: vi.fn(),
|
|
27
|
+
waitForMetroBackedAppReady: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
31
|
+
const actual =
|
|
32
|
+
await vi.importActual<typeof import('@react-native-harness/bundler-metro')>(
|
|
33
|
+
'@react-native-harness/bundler-metro'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...actual,
|
|
38
|
+
getMetroInstance: mocks.getMetroInstance,
|
|
39
|
+
isMetroCacheReusable: mocks.isMetroCacheReusable,
|
|
40
|
+
waitForMetroBackedAppReady: mocks.waitForMetroBackedAppReady,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
vi.mock('@react-native-harness/bridge/server', () => ({
|
|
45
|
+
getBridgeServer: mocks.getBridgeServer,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock('../logs.js', () => ({
|
|
49
|
+
logMetroCacheReused: mocks.logMetroCacheReused,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('@react-native-harness/tools', async () => {
|
|
53
|
+
const actual =
|
|
54
|
+
await vi.importActual<typeof import('@react-native-harness/tools')>(
|
|
55
|
+
'@react-native-harness/tools'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...actual,
|
|
60
|
+
createCrashArtifactWriter: mocks.createCrashArtifactWriter,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
import { getHarness, waitForAppReady } from '../harness.js';
|
|
65
|
+
import { StartupStallError } from '../errors.js';
|
|
66
|
+
|
|
12
67
|
const createBridgeServer = () => {
|
|
13
68
|
const emitter = new EventEmitter();
|
|
14
69
|
|
|
15
70
|
return {
|
|
16
71
|
serverBridge: {
|
|
72
|
+
rpc: {
|
|
73
|
+
clients: [],
|
|
74
|
+
},
|
|
17
75
|
on: emitter.on.bind(emitter),
|
|
18
76
|
once: emitter.once.bind(emitter),
|
|
19
77
|
off: emitter.off.bind(emitter),
|
|
78
|
+
dispose: vi.fn(),
|
|
20
79
|
} as unknown as BridgeServer,
|
|
21
80
|
emitReady: () => {
|
|
22
|
-
emitter.emit('ready'
|
|
81
|
+
emitter.emit('ready', {
|
|
82
|
+
platform: 'ios',
|
|
83
|
+
manufacturer: 'Apple',
|
|
84
|
+
model: 'Simulator',
|
|
85
|
+
osVersion: '18.0',
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
emitEvent: (event: unknown) => {
|
|
89
|
+
emitter.emit('event', event);
|
|
90
|
+
},
|
|
91
|
+
emitDisconnect: () => {
|
|
92
|
+
emitter.emit('disconnect');
|
|
23
93
|
},
|
|
24
94
|
};
|
|
25
95
|
};
|
|
26
96
|
|
|
27
|
-
const
|
|
28
|
-
const listeners = new Set<
|
|
97
|
+
const createReporter = (): Reporter => {
|
|
98
|
+
const listeners = new Set<(event: ReportableEvent) => void>();
|
|
29
99
|
|
|
30
100
|
return {
|
|
31
|
-
start: async () => undefined,
|
|
32
|
-
stop: async () => undefined,
|
|
33
|
-
dispose: async () => undefined,
|
|
34
101
|
addListener: (listener) => {
|
|
35
102
|
listeners.add(listener);
|
|
36
103
|
},
|
|
37
104
|
removeListener: (listener) => {
|
|
38
105
|
listeners.delete(listener);
|
|
39
106
|
},
|
|
107
|
+
emit: (event) => {
|
|
108
|
+
listeners.forEach((listener) => listener(event));
|
|
109
|
+
},
|
|
110
|
+
clearAllListeners: () => {
|
|
111
|
+
listeners.clear();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const createMetroInstance = (
|
|
117
|
+
overrides: Partial<MetroInstance> = {}
|
|
118
|
+
): MetroInstance => ({
|
|
119
|
+
events: createReporter(),
|
|
120
|
+
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
|
|
121
|
+
prewarm: vi.fn(async () => false),
|
|
122
|
+
dispose: vi.fn(async () => undefined),
|
|
123
|
+
...overrides,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const createAppMonitor = (): {
|
|
127
|
+
appMonitor: AppMonitor;
|
|
128
|
+
emit: (event: AppMonitorEvent) => void;
|
|
129
|
+
} => {
|
|
130
|
+
const listeners = new Set<AppMonitorListener>();
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
appMonitor: {
|
|
134
|
+
start: vi.fn(async () => undefined),
|
|
135
|
+
stop: vi.fn(async () => undefined),
|
|
136
|
+
dispose: vi.fn(async () => undefined),
|
|
137
|
+
addListener: (listener) => {
|
|
138
|
+
listeners.add(listener);
|
|
139
|
+
},
|
|
140
|
+
removeListener: (listener) => {
|
|
141
|
+
listeners.delete(listener);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
emit: (event) => {
|
|
145
|
+
for (const listener of listeners) {
|
|
146
|
+
listener(event);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
40
149
|
};
|
|
41
150
|
};
|
|
42
151
|
|
|
43
152
|
const createPlatformRunner = (
|
|
44
|
-
|
|
153
|
+
overrides: Partial<HarnessPlatformRunner> = {}
|
|
45
154
|
): HarnessPlatformRunner => ({
|
|
46
|
-
startApp: async () => undefined,
|
|
47
|
-
restartApp,
|
|
48
|
-
stopApp: async () => undefined,
|
|
49
|
-
dispose: async () => undefined,
|
|
50
|
-
isAppRunning: async () => true,
|
|
51
|
-
createAppMonitor: createAppMonitor,
|
|
155
|
+
startApp: vi.fn(async () => undefined),
|
|
156
|
+
restartApp: vi.fn(async () => undefined),
|
|
157
|
+
stopApp: vi.fn(async () => undefined),
|
|
158
|
+
dispose: vi.fn(async () => undefined),
|
|
159
|
+
isAppRunning: vi.fn(async () => true),
|
|
160
|
+
createAppMonitor: () => createAppMonitor().appMonitor,
|
|
161
|
+
...overrides,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const createHarnessConfig = (
|
|
165
|
+
overrides: Partial<HarnessConfig> = {}
|
|
166
|
+
): HarnessConfig =>
|
|
167
|
+
({
|
|
168
|
+
appRegistryComponentName: 'App',
|
|
169
|
+
bridgeTimeout: 60_000,
|
|
170
|
+
bundleStartTimeout: 60_000,
|
|
171
|
+
crashDetectionInterval: 500,
|
|
172
|
+
defaultRunner: 'ios',
|
|
173
|
+
detectNativeCrashes: true,
|
|
174
|
+
disableViewFlattening: false,
|
|
175
|
+
entryPoint: 'index.js',
|
|
176
|
+
forwardClientLogs: false,
|
|
177
|
+
maxAppRestarts: 2,
|
|
178
|
+
metroPort: 8081,
|
|
179
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
180
|
+
runners: [],
|
|
181
|
+
unstable__enableMetroCache: false,
|
|
182
|
+
unstable__skipAlreadyIncludedModules: false,
|
|
183
|
+
webSocketPort: 3001,
|
|
184
|
+
...overrides,
|
|
185
|
+
}) as HarnessConfig;
|
|
186
|
+
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
vi.clearAllMocks();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
afterEach(() => {
|
|
192
|
+
delete (
|
|
193
|
+
globalThis as typeof globalThis & {
|
|
194
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
195
|
+
}
|
|
196
|
+
).__HARNESS_PLATFORM_RUNNER__;
|
|
52
197
|
});
|
|
53
198
|
|
|
54
199
|
describe('waitForAppReady', () => {
|
|
55
|
-
it('
|
|
200
|
+
it('delegates startup orchestration to bundler-metro and resolves readiness from the bridge', async () => {
|
|
56
201
|
const { serverBridge, emitReady } = createBridgeServer();
|
|
57
|
-
const
|
|
58
|
-
const platformInstance = createPlatformRunner(
|
|
202
|
+
const metroInstance = createMetroInstance();
|
|
203
|
+
const platformInstance = createPlatformRunner();
|
|
204
|
+
const { appMonitor } = createAppMonitor();
|
|
59
205
|
const crashSupervisor = createCrashSupervisor({
|
|
60
|
-
appMonitor
|
|
206
|
+
appMonitor,
|
|
61
207
|
platformRunner: platformInstance,
|
|
62
208
|
});
|
|
63
209
|
|
|
64
|
-
|
|
210
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
211
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
212
|
+
options.onAttemptStart?.();
|
|
213
|
+
const readyPromise = options.waitForReady(new AbortController().signal);
|
|
214
|
+
emitReady();
|
|
215
|
+
await readyPromise;
|
|
216
|
+
options.onAttemptReset?.();
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await waitForAppReady({
|
|
221
|
+
metroInstance,
|
|
65
222
|
serverBridge,
|
|
66
223
|
platformInstance,
|
|
67
|
-
|
|
224
|
+
platformId: 'ios',
|
|
225
|
+
bundleStartTimeout: 1_500,
|
|
226
|
+
readyTimeout: 2_500,
|
|
227
|
+
maxAppRestarts: 3,
|
|
68
228
|
testFilePath: '/tmp/test.harness.ts',
|
|
69
229
|
crashSupervisor,
|
|
70
|
-
appLaunchOptions: {
|
|
71
|
-
extras: {
|
|
72
|
-
mode: 'startup',
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
230
|
});
|
|
76
231
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
232
|
+
expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledWith(
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
metro: metroInstance,
|
|
235
|
+
platformId: 'ios',
|
|
236
|
+
bundleStartTimeout: 1_500,
|
|
237
|
+
readyTimeout: 2_500,
|
|
238
|
+
maxAppRestarts: 3,
|
|
239
|
+
startAttempt: expect.any(Function),
|
|
240
|
+
waitForReady: expect.any(Function),
|
|
241
|
+
waitForCrash: expect.any(Function),
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
expect(crashSupervisor.isReady()).toBe(true);
|
|
83
245
|
|
|
84
|
-
emitReady();
|
|
85
|
-
await promise;
|
|
86
246
|
await crashSupervisor.dispose();
|
|
87
247
|
});
|
|
88
248
|
|
|
89
|
-
it('
|
|
90
|
-
vi.useFakeTimers();
|
|
91
|
-
|
|
249
|
+
it('passes launch options through the shared Metro startup helper', async () => {
|
|
92
250
|
const { serverBridge } = createBridgeServer();
|
|
93
|
-
const
|
|
94
|
-
const
|
|
251
|
+
const metroInstance = createMetroInstance();
|
|
252
|
+
const restartApp = vi.fn(async () => undefined);
|
|
253
|
+
const platformInstance = createPlatformRunner({ restartApp });
|
|
254
|
+
const { appMonitor } = createAppMonitor();
|
|
95
255
|
const crashSupervisor = createCrashSupervisor({
|
|
96
|
-
appMonitor
|
|
256
|
+
appMonitor,
|
|
97
257
|
platformRunner: platformInstance,
|
|
98
258
|
});
|
|
99
259
|
|
|
100
|
-
|
|
260
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
261
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
262
|
+
await options.startAttempt();
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await waitForAppReady({
|
|
267
|
+
metroInstance,
|
|
101
268
|
serverBridge,
|
|
102
269
|
platformInstance,
|
|
103
|
-
|
|
270
|
+
platformId: 'ios',
|
|
271
|
+
bundleStartTimeout: 1_500,
|
|
272
|
+
readyTimeout: 2_500,
|
|
273
|
+
maxAppRestarts: 3,
|
|
104
274
|
testFilePath: '/tmp/test.harness.ts',
|
|
105
275
|
crashSupervisor,
|
|
106
276
|
appLaunchOptions: {
|
|
@@ -110,13 +280,6 @@ describe('waitForAppReady', () => {
|
|
|
110
280
|
},
|
|
111
281
|
});
|
|
112
282
|
|
|
113
|
-
await Promise.resolve();
|
|
114
|
-
await vi.advanceTimersByTimeAsync(1000);
|
|
115
|
-
|
|
116
|
-
await expect(promise).rejects.toMatchObject({
|
|
117
|
-
name: 'AbortError',
|
|
118
|
-
});
|
|
119
|
-
expect(restartApp).toHaveBeenCalledTimes(1);
|
|
120
283
|
expect(restartApp).toHaveBeenCalledWith({
|
|
121
284
|
extras: {
|
|
122
285
|
mode: 'startup',
|
|
@@ -124,6 +287,261 @@ describe('waitForAppReady', () => {
|
|
|
124
287
|
});
|
|
125
288
|
|
|
126
289
|
await crashSupervisor.dispose();
|
|
127
|
-
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('getHarness', () => {
|
|
294
|
+
it('routes ensureAppReady through the shared Metro startup helper', async () => {
|
|
295
|
+
const { serverBridge, emitReady } = createBridgeServer();
|
|
296
|
+
const appMonitor = createAppMonitor();
|
|
297
|
+
const restartApp = vi.fn(async () => undefined);
|
|
298
|
+
const platformInstance = createPlatformRunner({
|
|
299
|
+
restartApp,
|
|
300
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
301
|
+
});
|
|
302
|
+
const metroInstance = createMetroInstance();
|
|
303
|
+
|
|
304
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
305
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
306
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
307
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
308
|
+
await options.startAttempt();
|
|
309
|
+
const readyPromise = options.waitForReady(
|
|
310
|
+
new AbortController().signal
|
|
311
|
+
);
|
|
312
|
+
emitReady();
|
|
313
|
+
await readyPromise;
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
(
|
|
318
|
+
globalThis as typeof globalThis & {
|
|
319
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
320
|
+
}
|
|
321
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
322
|
+
|
|
323
|
+
const platform: HarnessPlatform = {
|
|
324
|
+
config: {
|
|
325
|
+
appLaunchOptions: {
|
|
326
|
+
extras: {
|
|
327
|
+
source: 'ensure-ready',
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
name: 'ios',
|
|
332
|
+
platformId: 'ios',
|
|
333
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
334
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
335
|
+
)}`,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const harness = await getHarness(
|
|
339
|
+
createHarnessConfig(),
|
|
340
|
+
platform,
|
|
341
|
+
'/tmp/project'
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
await harness.ensureAppReady('/tmp/example.harness.ts');
|
|
345
|
+
|
|
346
|
+
expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledTimes(1);
|
|
347
|
+
expect(restartApp).toHaveBeenCalledWith({
|
|
348
|
+
extras: {
|
|
349
|
+
source: 'ensure-ready',
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await harness.dispose();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('routes restart(testFilePath) through the shared Metro startup helper', async () => {
|
|
357
|
+
const { serverBridge, emitReady } = createBridgeServer();
|
|
358
|
+
const appMonitor = createAppMonitor();
|
|
359
|
+
const restartApp = vi.fn(async () => undefined);
|
|
360
|
+
const stopApp = vi.fn(async () => undefined);
|
|
361
|
+
const platformInstance = createPlatformRunner({
|
|
362
|
+
restartApp,
|
|
363
|
+
stopApp,
|
|
364
|
+
isAppRunning: vi.fn(async () => false),
|
|
365
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
366
|
+
});
|
|
367
|
+
const metroInstance = createMetroInstance();
|
|
368
|
+
|
|
369
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
370
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
371
|
+
mocks.waitForMetroBackedAppReady.mockImplementationOnce(
|
|
372
|
+
async (options: WaitForMetroBackedAppReadyOptions) => {
|
|
373
|
+
await options.startAttempt();
|
|
374
|
+
const readyPromise = options.waitForReady(
|
|
375
|
+
new AbortController().signal
|
|
376
|
+
);
|
|
377
|
+
emitReady();
|
|
378
|
+
await readyPromise;
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
(
|
|
383
|
+
globalThis as typeof globalThis & {
|
|
384
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
385
|
+
}
|
|
386
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
387
|
+
|
|
388
|
+
const platform: HarnessPlatform = {
|
|
389
|
+
config: {
|
|
390
|
+
appLaunchOptions: {
|
|
391
|
+
extras: {
|
|
392
|
+
source: 'restart',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
name: 'ios',
|
|
397
|
+
platformId: 'ios',
|
|
398
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
399
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
400
|
+
)}`,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const harness = await getHarness(
|
|
404
|
+
createHarnessConfig(),
|
|
405
|
+
platform,
|
|
406
|
+
'/tmp/project'
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await harness.restart('/tmp/restart.harness.ts');
|
|
410
|
+
|
|
411
|
+
expect(stopApp).toHaveBeenCalledTimes(1);
|
|
412
|
+
expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledTimes(1);
|
|
413
|
+
expect(restartApp).toHaveBeenCalledWith({
|
|
414
|
+
extras: {
|
|
415
|
+
source: 'restart',
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await harness.dispose();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('plugins', () => {
|
|
424
|
+
it('invokes lifecycle and runtime plugin handlers for configured plugins', async () => {
|
|
425
|
+
const { serverBridge, emitReady, emitEvent, emitDisconnect } =
|
|
426
|
+
createBridgeServer();
|
|
427
|
+
const appMonitor = createAppMonitor();
|
|
428
|
+
const platformInstance = createPlatformRunner({
|
|
429
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
430
|
+
});
|
|
431
|
+
const observedHooks: string[] = [];
|
|
432
|
+
|
|
433
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
434
|
+
const metroInstance = createMetroInstance();
|
|
435
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
436
|
+
|
|
437
|
+
(
|
|
438
|
+
globalThis as typeof globalThis & {
|
|
439
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
440
|
+
}
|
|
441
|
+
).__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
|
|
442
|
+
|
|
443
|
+
const plugin = definePlugin<
|
|
444
|
+
{
|
|
445
|
+
creationCount: number;
|
|
446
|
+
},
|
|
447
|
+
HarnessConfig,
|
|
448
|
+
HarnessPlatform
|
|
449
|
+
>({
|
|
450
|
+
name: 'test-plugin',
|
|
451
|
+
createState: () => ({
|
|
452
|
+
creationCount: 0,
|
|
453
|
+
}),
|
|
454
|
+
hooks: {
|
|
455
|
+
harness: {
|
|
456
|
+
beforeCreation: (ctx) => {
|
|
457
|
+
ctx.state.creationCount += 1;
|
|
458
|
+
observedHooks.push(
|
|
459
|
+
`beforeCreation:${ctx.platform.platformId}:${ctx.appLaunchOptions == null ? 'no-launch-options' : 'launch-options'}`
|
|
460
|
+
);
|
|
461
|
+
},
|
|
462
|
+
beforeDispose: (ctx) => {
|
|
463
|
+
observedHooks.push(
|
|
464
|
+
`beforeDispose:${ctx.state.creationCount}:${ctx.reason}`
|
|
465
|
+
);
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
runtime: {
|
|
469
|
+
ready: (ctx) => {
|
|
470
|
+
observedHooks.push(`runtime.ready:${ctx.runId}:${ctx.device.platform}`);
|
|
471
|
+
},
|
|
472
|
+
disconnected: (ctx) => {
|
|
473
|
+
observedHooks.push(`runtime.disconnected:${ctx.reason}`);
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
collection: {
|
|
477
|
+
started: (ctx) => {
|
|
478
|
+
observedHooks.push(`collection.started:${ctx.file}`);
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const platform: HarnessPlatform = {
|
|
485
|
+
config: {
|
|
486
|
+
appLaunchOptions: {
|
|
487
|
+
environment: {
|
|
488
|
+
MODE: 'test',
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
name: 'ios',
|
|
493
|
+
platformId: 'ios',
|
|
494
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
495
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
496
|
+
)}`,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const harness = await getHarness(
|
|
500
|
+
createHarnessConfig({
|
|
501
|
+
plugins: [plugin],
|
|
502
|
+
}),
|
|
503
|
+
platform,
|
|
504
|
+
'/tmp/project'
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
harness.setRunState({
|
|
508
|
+
runId: 'run-1',
|
|
509
|
+
startTime: Date.now(),
|
|
510
|
+
testFiles: ['example.harness.ts'],
|
|
511
|
+
watchMode: false,
|
|
512
|
+
coverageEnabled: false,
|
|
513
|
+
summary: {
|
|
514
|
+
passed: 1,
|
|
515
|
+
failed: 0,
|
|
516
|
+
skipped: 0,
|
|
517
|
+
todo: 0,
|
|
518
|
+
},
|
|
519
|
+
status: 'passed',
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
emitReady();
|
|
523
|
+
emitEvent({
|
|
524
|
+
type: 'collection-started',
|
|
525
|
+
file: 'example.harness.ts',
|
|
526
|
+
});
|
|
527
|
+
emitDisconnect();
|
|
528
|
+
|
|
529
|
+
await harness.dispose();
|
|
530
|
+
|
|
531
|
+
expect(observedHooks).toEqual([
|
|
532
|
+
'beforeCreation:ios:launch-options',
|
|
533
|
+
'runtime.ready:run-1:ios',
|
|
534
|
+
'collection.started:example.harness.ts',
|
|
535
|
+
'runtime.disconnected:bridge-disconnected',
|
|
536
|
+
'beforeDispose:1:normal',
|
|
537
|
+
]);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('StartupStallError', () => {
|
|
542
|
+
it('includes the configured timeout and attempt count', () => {
|
|
543
|
+
expect(new StartupStallError(1_500, 4).message).toBe(
|
|
544
|
+
'The app did not request its Metro bundle after 4 launch attempts within 1500ms. Last Metro status: unknown.'
|
|
545
|
+
);
|
|
128
546
|
});
|
|
129
547
|
});
|