@unrulysystems/rn-playwright-driver-runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/bin/rn-driver.ts +17 -0
- package/dist/cli.d.mts +3 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +1898 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +1891 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +281 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.js +1063 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1057 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
function defineRnDriverConfig(config) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/constants.ts
|
|
9
|
+
var ENV = {
|
|
10
|
+
metroUrl: "RN_METRO_URL",
|
|
11
|
+
deviceName: "RN_DEVICE_NAME",
|
|
12
|
+
timeout: "RN_TIMEOUT",
|
|
13
|
+
touchBackend: "RN_TOUCH_BACKEND",
|
|
14
|
+
xctestPort: "RN_TOUCH_XCTEST_PORT",
|
|
15
|
+
xctestTokenFile: "RN_TOUCH_XCTEST_TOKEN_FILE",
|
|
16
|
+
instrumentationPort: "RN_TOUCH_INSTRUMENTATION_PORT",
|
|
17
|
+
instrumentationTokenFile: "RN_TOUCH_INSTRUMENTATION_TOKEN_FILE",
|
|
18
|
+
androidSerial: "ANDROID_SERIAL"
|
|
19
|
+
};
|
|
20
|
+
var TOUCH_BACKEND = {
|
|
21
|
+
ios: "xctest",
|
|
22
|
+
android: "instrumentation"
|
|
23
|
+
};
|
|
24
|
+
var DEFAULTS = {
|
|
25
|
+
metroHost: "127.0.0.1",
|
|
26
|
+
metroPort: 8081,
|
|
27
|
+
metroReadyTimeoutMs: 9e4,
|
|
28
|
+
companionPort: 9999,
|
|
29
|
+
/** Covers a cold `xcodebuild test` build, not just process startup (FU-2). */
|
|
30
|
+
iosCompanionReadyTimeoutMs: 3e5,
|
|
31
|
+
androidCompanionReadyTimeoutMs: 45e3,
|
|
32
|
+
hermesTargetTimeoutMs: 6e4,
|
|
33
|
+
appLaunchAttempts: 3,
|
|
34
|
+
driverTimeoutMs: 3e4,
|
|
35
|
+
androidGradleTasks: [":app:assembleDebug", ":app:assembleDebugAndroidTest"],
|
|
36
|
+
androidAppApkPath: "android/app/build/outputs/apk/debug/app-debug.apk",
|
|
37
|
+
androidTestApkPath: "android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
|
|
38
|
+
instrumentationClass: "com.rndriver.touchcompanion.RNDriverTouchCompanion",
|
|
39
|
+
/** UI-test method the iOS companion exposes as a long-running server. */
|
|
40
|
+
xctestServerTest: "RNDriverTouchCompanionTests/testRunServer",
|
|
41
|
+
/** Device-private filename the Android companion reads its token from. */
|
|
42
|
+
androidTokenFileName: "rn-driver-touch-token"
|
|
43
|
+
};
|
|
44
|
+
var SECRET_PLACEHOLDER = "<token-file>";
|
|
45
|
+
|
|
46
|
+
// src/plan/env.ts
|
|
47
|
+
function buildIosDriverEnv(resolved, metro, timeoutMs) {
|
|
48
|
+
return {
|
|
49
|
+
[ENV.touchBackend]: TOUCH_BACKEND.ios,
|
|
50
|
+
[ENV.metroUrl]: metro.url,
|
|
51
|
+
[ENV.deviceName]: resolved.simName,
|
|
52
|
+
[ENV.timeout]: String(timeoutMs ?? DEFAULTS.driverTimeoutMs),
|
|
53
|
+
[ENV.xctestPort]: String(resolved.touchPort),
|
|
54
|
+
[ENV.xctestTokenFile]: resolved.tokenFile
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function buildAndroidDriverEnv(resolved, metro, deviceName, timeoutMs) {
|
|
58
|
+
return {
|
|
59
|
+
[ENV.touchBackend]: TOUCH_BACKEND.android,
|
|
60
|
+
[ENV.metroUrl]: metro.url,
|
|
61
|
+
[ENV.deviceName]: deviceName,
|
|
62
|
+
[ENV.androidSerial]: resolved.serial,
|
|
63
|
+
[ENV.timeout]: String(timeoutMs ?? DEFAULTS.driverTimeoutMs),
|
|
64
|
+
[ENV.instrumentationPort]: String(resolved.touchPort),
|
|
65
|
+
[ENV.instrumentationTokenFile]: resolved.tokenFile
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/plan/shared.ts
|
|
70
|
+
function cmd(command, args) {
|
|
71
|
+
return { command, args };
|
|
72
|
+
}
|
|
73
|
+
function npx(args) {
|
|
74
|
+
return { command: "npx", args };
|
|
75
|
+
}
|
|
76
|
+
function shell(command) {
|
|
77
|
+
return { command: "sh", args: ["-c", command] };
|
|
78
|
+
}
|
|
79
|
+
function metroStartStep(metro) {
|
|
80
|
+
return {
|
|
81
|
+
id: "metro.start",
|
|
82
|
+
stage: "metro",
|
|
83
|
+
description: metro.reuseExisting ? `Start Metro (reuse if running) ${metro.url}` : `Start Metro ${metro.url}`,
|
|
84
|
+
action: {
|
|
85
|
+
type: "command",
|
|
86
|
+
background: true,
|
|
87
|
+
processKey: "metro",
|
|
88
|
+
command: shell(metro.command ?? `npx expo start --localhost --port ${metro.port}`)
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function playwrightCommand(playwright, specs, passthrough) {
|
|
93
|
+
const args = ["playwright", "test"];
|
|
94
|
+
if (playwright?.config) args.push("--config", playwright.config);
|
|
95
|
+
const effectiveSpecs = specs.length > 0 ? specs : playwright?.specs ?? [];
|
|
96
|
+
args.push(...effectiveSpecs, ...passthrough);
|
|
97
|
+
args.push("--reporter=line");
|
|
98
|
+
return npx(args);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/plan/android.ts
|
|
102
|
+
function planAndroid(input) {
|
|
103
|
+
const { android, metro, resolved, playwright, timeoutMs, specs, passthrough, hermesDeviceName } = input;
|
|
104
|
+
const serial = resolved.serial;
|
|
105
|
+
const gradleTasks = android.gradleTasks ?? [...DEFAULTS.androidGradleTasks];
|
|
106
|
+
const appApk = android.appApkPath ?? DEFAULTS.androidAppApkPath;
|
|
107
|
+
const testApk = android.testApkPath ?? DEFAULTS.androidTestApkPath;
|
|
108
|
+
const steps = [];
|
|
109
|
+
const push = (step) => steps.push(step);
|
|
110
|
+
push({
|
|
111
|
+
id: "android.prebuild",
|
|
112
|
+
stage: "build",
|
|
113
|
+
description: "Generate Android project (expo prebuild)",
|
|
114
|
+
action: {
|
|
115
|
+
type: "command",
|
|
116
|
+
command: npx(["expo", "prebuild", "--platform", "android", "--no-install"])
|
|
117
|
+
},
|
|
118
|
+
skippable: true
|
|
119
|
+
});
|
|
120
|
+
push({
|
|
121
|
+
id: "android.gradle",
|
|
122
|
+
stage: "build",
|
|
123
|
+
description: `Build app + androidTest APKs (${gradleTasks.join(" ")})`,
|
|
124
|
+
action: {
|
|
125
|
+
type: "command",
|
|
126
|
+
command: { command: "./gradlew", args: gradleTasks, cwd: "android" }
|
|
127
|
+
},
|
|
128
|
+
skippable: true
|
|
129
|
+
});
|
|
130
|
+
push({
|
|
131
|
+
id: "android.install-app",
|
|
132
|
+
stage: "build",
|
|
133
|
+
description: `Install app APK`,
|
|
134
|
+
action: { type: "command", command: adb(serial, ["install", "-r", appApk]) },
|
|
135
|
+
skippable: true
|
|
136
|
+
});
|
|
137
|
+
push({
|
|
138
|
+
id: "android.install-test",
|
|
139
|
+
stage: "build",
|
|
140
|
+
description: `Install androidTest APK`,
|
|
141
|
+
action: { type: "command", command: adb(serial, ["install", "-r", "-t", testApk]) },
|
|
142
|
+
skippable: true
|
|
143
|
+
});
|
|
144
|
+
push({
|
|
145
|
+
id: "android.install-token",
|
|
146
|
+
stage: "companion",
|
|
147
|
+
description: "Install companion token into app private files",
|
|
148
|
+
action: {
|
|
149
|
+
type: "command",
|
|
150
|
+
command: {
|
|
151
|
+
...adbShellScript(
|
|
152
|
+
serial,
|
|
153
|
+
`run-as ${android.packageName} sh -c 'mkdir -p files && cat > files/${resolved.deviceTokenFileName} && chmod 600 files/${resolved.deviceTokenFileName}'`
|
|
154
|
+
),
|
|
155
|
+
stdinFromFile: resolved.tokenFile
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
push(metroStartStep(metro));
|
|
160
|
+
push({
|
|
161
|
+
id: "metro.ready",
|
|
162
|
+
stage: "metro",
|
|
163
|
+
description: `Wait for Metro at ${metro.url}`,
|
|
164
|
+
action: {
|
|
165
|
+
type: "probe",
|
|
166
|
+
probe: { kind: "metro-status", metroUrl: metro.url, timeoutMs: metro.readyTimeoutMs }
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
push({
|
|
170
|
+
id: "android.reverse-metro",
|
|
171
|
+
stage: "device",
|
|
172
|
+
description: `adb reverse tcp:${metro.port}`,
|
|
173
|
+
action: {
|
|
174
|
+
type: "command",
|
|
175
|
+
command: adb(serial, ["reverse", `tcp:${metro.port}`, `tcp:${metro.port}`])
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
if (metro.port !== DEFAULTS.metroPort) {
|
|
179
|
+
push({
|
|
180
|
+
id: "android.reverse-default",
|
|
181
|
+
stage: "device",
|
|
182
|
+
description: `adb reverse tcp:${DEFAULTS.metroPort} -> tcp:${metro.port}`,
|
|
183
|
+
action: {
|
|
184
|
+
type: "command",
|
|
185
|
+
command: adb(serial, ["reverse", `tcp:${DEFAULTS.metroPort}`, `tcp:${metro.port}`])
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
push({
|
|
190
|
+
id: "android.debug-host",
|
|
191
|
+
stage: "device",
|
|
192
|
+
description: "Write app debug_http_host",
|
|
193
|
+
action: {
|
|
194
|
+
type: "command",
|
|
195
|
+
command: {
|
|
196
|
+
// Single adb-shell arg so the redirect runs inside the run-as'd app-uid
|
|
197
|
+
// shell (the shared_prefs path is app-private; the outer `shell` uid
|
|
198
|
+
// cannot write it).
|
|
199
|
+
...adbShellScript(
|
|
200
|
+
serial,
|
|
201
|
+
`run-as ${android.packageName} sh -c 'mkdir -p /data/data/${android.packageName}/shared_prefs && cat > /data/data/${android.packageName}/shared_prefs/${android.packageName}_preferences.xml'`
|
|
202
|
+
),
|
|
203
|
+
stdinContents: debugHostXml(`localhost:${metro.port}`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const launchCommand = launchCommandFor(android, serial);
|
|
208
|
+
push(launchStep("android.launch-1", android, serial));
|
|
209
|
+
push(hermesStep("android.hermes-1", android, metro, resolved, hermesDeviceName, launchCommand));
|
|
210
|
+
push({
|
|
211
|
+
id: "android.forward-clean",
|
|
212
|
+
stage: "companion",
|
|
213
|
+
description: `Clear stale adb forward tcp:${resolved.touchPort}`,
|
|
214
|
+
action: {
|
|
215
|
+
type: "command",
|
|
216
|
+
command: adb(serial, ["forward", "--remove", `tcp:${resolved.touchPort}`]),
|
|
217
|
+
allowFailure: true
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
push({
|
|
221
|
+
id: "android.forward",
|
|
222
|
+
stage: "companion",
|
|
223
|
+
description: `adb forward tcp:${resolved.touchPort}`,
|
|
224
|
+
action: {
|
|
225
|
+
type: "command",
|
|
226
|
+
command: adb(serial, ["forward", `tcp:${resolved.touchPort}`, `tcp:${resolved.touchPort}`])
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
push({
|
|
230
|
+
id: "android.instrument-start",
|
|
231
|
+
stage: "companion",
|
|
232
|
+
description: `Start instrumentation companion on port ${resolved.touchPort}`,
|
|
233
|
+
action: {
|
|
234
|
+
type: "command",
|
|
235
|
+
background: true,
|
|
236
|
+
processKey: "companion",
|
|
237
|
+
command: adb(serial, [
|
|
238
|
+
"shell",
|
|
239
|
+
"am",
|
|
240
|
+
"instrument",
|
|
241
|
+
"-e",
|
|
242
|
+
"rnDriverAuthTokenFile",
|
|
243
|
+
resolved.deviceTokenFileName,
|
|
244
|
+
"-e",
|
|
245
|
+
"rnDriverPort",
|
|
246
|
+
String(resolved.touchPort),
|
|
247
|
+
"-w",
|
|
248
|
+
resolved.instrumentationTarget
|
|
249
|
+
])
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
push({
|
|
253
|
+
id: "android.instrument-ready",
|
|
254
|
+
stage: "companion",
|
|
255
|
+
description: "Wait for companion to accept a hello",
|
|
256
|
+
action: {
|
|
257
|
+
type: "probe",
|
|
258
|
+
probe: {
|
|
259
|
+
kind: "instrumentation-hello",
|
|
260
|
+
port: resolved.touchPort,
|
|
261
|
+
tokenFile: resolved.tokenFile,
|
|
262
|
+
timeoutMs: resolved.companionReadyTimeoutMs
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
push(launchStep("android.launch-2", android, serial));
|
|
267
|
+
push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName, launchCommand));
|
|
268
|
+
const cleanup = [
|
|
269
|
+
{
|
|
270
|
+
type: "kill-process",
|
|
271
|
+
processKey: "companion",
|
|
272
|
+
description: "Stop instrumentation companion"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
type: "command",
|
|
276
|
+
command: adb(serial, ["reverse", "--remove", `tcp:${metro.port}`]),
|
|
277
|
+
description: "Remove metro reverse"
|
|
278
|
+
},
|
|
279
|
+
// Mirror android.reverse-default: when Metro is off 8081 the plan also adds a
|
|
280
|
+
// `tcp:8081 -> tcp:<port>` fallback reverse, so cleanup must remove BOTH or a
|
|
281
|
+
// stale 8081 mapping wedges the next run (REQ-CLEAN-003).
|
|
282
|
+
...metro.port === DEFAULTS.metroPort ? [] : [
|
|
283
|
+
{
|
|
284
|
+
type: "command",
|
|
285
|
+
command: adb(serial, ["reverse", "--remove", `tcp:${DEFAULTS.metroPort}`]),
|
|
286
|
+
description: "Remove fallback 8081 metro reverse"
|
|
287
|
+
}
|
|
288
|
+
],
|
|
289
|
+
{
|
|
290
|
+
type: "command",
|
|
291
|
+
command: adb(serial, ["forward", "--remove", `tcp:${resolved.touchPort}`]),
|
|
292
|
+
description: "Remove companion forward"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
type: "command",
|
|
296
|
+
command: adb(serial, [
|
|
297
|
+
"shell",
|
|
298
|
+
"run-as",
|
|
299
|
+
android.packageName,
|
|
300
|
+
"rm",
|
|
301
|
+
"-f",
|
|
302
|
+
`files/${resolved.deviceTokenFileName}`
|
|
303
|
+
]),
|
|
304
|
+
description: "Remove device token file"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
type: "command",
|
|
308
|
+
command: adb(serial, ["shell", "am", "force-stop", android.packageName]),
|
|
309
|
+
description: "Force-stop app"
|
|
310
|
+
},
|
|
311
|
+
{ type: "kill-process", processKey: "metro", description: "Stop runner-owned Metro" },
|
|
312
|
+
{ type: "remove-file", path: resolved.tokenFile, description: "Remove per-run token file" }
|
|
313
|
+
];
|
|
314
|
+
return {
|
|
315
|
+
platform: "android",
|
|
316
|
+
steps,
|
|
317
|
+
cleanup,
|
|
318
|
+
driverEnv: buildAndroidDriverEnv(resolved, metro, hermesDeviceName, timeoutMs),
|
|
319
|
+
playwright: playwrightCommand(playwright, specs, passthrough)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function launchCommandFor(android, serial) {
|
|
323
|
+
return adb(serial, [
|
|
324
|
+
"shell",
|
|
325
|
+
"am",
|
|
326
|
+
"start",
|
|
327
|
+
"-W",
|
|
328
|
+
"-n",
|
|
329
|
+
`${android.packageName}/${android.activity}`
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
function launchStep(id, android, serial) {
|
|
333
|
+
return {
|
|
334
|
+
id,
|
|
335
|
+
stage: "app-launch",
|
|
336
|
+
description: `Launch ${android.packageName}/${android.activity}`,
|
|
337
|
+
action: { type: "command", command: launchCommandFor(android, serial) }
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function hermesStep(id, android, metro, resolved, deviceNameMatch, launchCommand) {
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
stage: "hermes-target",
|
|
344
|
+
description: "Wait for Hermes target",
|
|
345
|
+
action: {
|
|
346
|
+
type: "probe",
|
|
347
|
+
probe: {
|
|
348
|
+
kind: "hermes-target",
|
|
349
|
+
platform: "android",
|
|
350
|
+
metroUrl: metro.url,
|
|
351
|
+
appId: android.packageName,
|
|
352
|
+
deviceNameMatch,
|
|
353
|
+
timeoutMs: resolved.hermesTimeoutMs
|
|
354
|
+
},
|
|
355
|
+
// REQ-AND-005: on a transient miss, re-issue `am start` and re-probe.
|
|
356
|
+
// `appLaunchAttempts` total attempts ⇒ that many minus the first = retries.
|
|
357
|
+
retry: { command: launchCommand, max: DEFAULTS.appLaunchAttempts - 1 }
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function debugHostXml(host) {
|
|
362
|
+
return `<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
|
363
|
+
<map>
|
|
364
|
+
<string name="debug_http_host">${host}</string>
|
|
365
|
+
</map>
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
function adb(serial, args) {
|
|
369
|
+
return { command: "adb", args: ["-s", serial, ...args] };
|
|
370
|
+
}
|
|
371
|
+
function adbShellScript(serial, remote) {
|
|
372
|
+
return { command: "adb", args: ["-s", serial, "shell", remote] };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/plan/ios.ts
|
|
376
|
+
function planIos(input) {
|
|
377
|
+
const { ios, metro, resolved, playwright, timeoutMs, specs, passthrough } = input;
|
|
378
|
+
const isDevClient = ios.launch.kind === "expo-dev-client";
|
|
379
|
+
const steps = [];
|
|
380
|
+
const push = (step) => steps.push(step);
|
|
381
|
+
push({
|
|
382
|
+
id: "ios.boot",
|
|
383
|
+
stage: "device",
|
|
384
|
+
description: `Boot simulator ${resolved.simName}`,
|
|
385
|
+
action: {
|
|
386
|
+
type: "command",
|
|
387
|
+
command: xcrun(["simctl", "boot", resolved.simUdid]),
|
|
388
|
+
allowFailure: true
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
push({
|
|
392
|
+
id: "ios.boot-wait",
|
|
393
|
+
stage: "device",
|
|
394
|
+
description: `Wait for ${resolved.simName} to finish booting`,
|
|
395
|
+
action: { type: "command", command: xcrun(["simctl", "bootstatus", resolved.simUdid, "-b"]) }
|
|
396
|
+
});
|
|
397
|
+
push({
|
|
398
|
+
id: "ios.prebuild",
|
|
399
|
+
stage: "build",
|
|
400
|
+
description: "Generate iOS project (expo prebuild)",
|
|
401
|
+
action: {
|
|
402
|
+
type: "command",
|
|
403
|
+
command: npx(["expo", "prebuild", "--platform", "ios", "--no-install"])
|
|
404
|
+
},
|
|
405
|
+
skippable: true
|
|
406
|
+
});
|
|
407
|
+
push({
|
|
408
|
+
id: "ios.scaffold",
|
|
409
|
+
stage: "build",
|
|
410
|
+
description: "Scaffold XCTest companion target",
|
|
411
|
+
action: {
|
|
412
|
+
type: "command",
|
|
413
|
+
// Pass the resolved UI-test scheme so a custom `ios.uitestScheme` scaffolds
|
|
414
|
+
// the SAME target that companion startup later builds (default is
|
|
415
|
+
// `${appScheme}UITests`).
|
|
416
|
+
command: npx([
|
|
417
|
+
"rn-driver-xctest-scaffold",
|
|
418
|
+
"--ios-dir",
|
|
419
|
+
"ios",
|
|
420
|
+
"--project-name",
|
|
421
|
+
ios.appScheme,
|
|
422
|
+
"--uitest-scheme",
|
|
423
|
+
resolved.uitestScheme
|
|
424
|
+
])
|
|
425
|
+
},
|
|
426
|
+
skippable: true
|
|
427
|
+
});
|
|
428
|
+
push({
|
|
429
|
+
id: "ios.runtime-config",
|
|
430
|
+
stage: "build",
|
|
431
|
+
description: "Write companion runtime config (port + token-file ref)",
|
|
432
|
+
action: {
|
|
433
|
+
type: "write-file",
|
|
434
|
+
path: resolved.runtimeConfigFile,
|
|
435
|
+
contents: runtimeConfigJson(resolved, ios),
|
|
436
|
+
mode: 384
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
push({
|
|
440
|
+
id: "ios.pods",
|
|
441
|
+
stage: "build",
|
|
442
|
+
description: "Install CocoaPods",
|
|
443
|
+
action: { type: "command", command: cmd("pod", ["install", "--project-directory=ios"]) },
|
|
444
|
+
skippable: true
|
|
445
|
+
});
|
|
446
|
+
push(metroStartStep(metro));
|
|
447
|
+
push({
|
|
448
|
+
id: "metro.ready",
|
|
449
|
+
stage: "metro",
|
|
450
|
+
description: `Wait for Metro at ${metro.url}`,
|
|
451
|
+
action: {
|
|
452
|
+
type: "probe",
|
|
453
|
+
probe: { kind: "metro-status", metroUrl: metro.url, timeoutMs: metro.readyTimeoutMs }
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
push({
|
|
457
|
+
id: "ios.packager-host-location",
|
|
458
|
+
stage: "device",
|
|
459
|
+
description: "Point app at Metro (RCT_jsLocation)",
|
|
460
|
+
action: {
|
|
461
|
+
type: "command",
|
|
462
|
+
command: xcrun([
|
|
463
|
+
"simctl",
|
|
464
|
+
"spawn",
|
|
465
|
+
resolved.simUdid,
|
|
466
|
+
"defaults",
|
|
467
|
+
"write",
|
|
468
|
+
ios.bundleId,
|
|
469
|
+
"RCT_jsLocation",
|
|
470
|
+
`${metro.host}:${metro.port}`
|
|
471
|
+
]),
|
|
472
|
+
allowFailure: true
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
push({
|
|
476
|
+
id: "ios.packager-host-scheme",
|
|
477
|
+
stage: "device",
|
|
478
|
+
description: "Point app at Metro (RCT_packager_scheme)",
|
|
479
|
+
action: {
|
|
480
|
+
type: "command",
|
|
481
|
+
command: xcrun([
|
|
482
|
+
"simctl",
|
|
483
|
+
"spawn",
|
|
484
|
+
resolved.simUdid,
|
|
485
|
+
"defaults",
|
|
486
|
+
"write",
|
|
487
|
+
ios.bundleId,
|
|
488
|
+
"RCT_packager_scheme",
|
|
489
|
+
"http"
|
|
490
|
+
]),
|
|
491
|
+
allowFailure: true
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
push({
|
|
495
|
+
id: "ios.build-app",
|
|
496
|
+
stage: "build",
|
|
497
|
+
description: `Build app scheme ${ios.appScheme}`,
|
|
498
|
+
action: {
|
|
499
|
+
type: "command",
|
|
500
|
+
command: xcodebuild([
|
|
501
|
+
"build",
|
|
502
|
+
"-workspace",
|
|
503
|
+
ios.workspace,
|
|
504
|
+
"-scheme",
|
|
505
|
+
ios.appScheme,
|
|
506
|
+
"-destination",
|
|
507
|
+
resolved.destination,
|
|
508
|
+
`RCT_METRO_PORT=${metro.port}`
|
|
509
|
+
])
|
|
510
|
+
},
|
|
511
|
+
skippable: true
|
|
512
|
+
});
|
|
513
|
+
for (const [key, value] of Object.entries(ios.defaults ?? {})) {
|
|
514
|
+
push({
|
|
515
|
+
id: `ios.seed.${key}`,
|
|
516
|
+
stage: "device",
|
|
517
|
+
description: `Seed default ${key}`,
|
|
518
|
+
action: {
|
|
519
|
+
type: "command",
|
|
520
|
+
command: xcrun([
|
|
521
|
+
"simctl",
|
|
522
|
+
"spawn",
|
|
523
|
+
resolved.simUdid,
|
|
524
|
+
"defaults",
|
|
525
|
+
"write",
|
|
526
|
+
ios.bundleId,
|
|
527
|
+
...defaultsArgs(key, value)
|
|
528
|
+
]),
|
|
529
|
+
allowFailure: true
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
push({
|
|
534
|
+
id: "ios.free-port",
|
|
535
|
+
stage: "companion",
|
|
536
|
+
description: `Free stale listener on port ${resolved.touchPort}`,
|
|
537
|
+
action: { type: "free-port", port: resolved.touchPort }
|
|
538
|
+
});
|
|
539
|
+
push({
|
|
540
|
+
id: "ios.companion-start",
|
|
541
|
+
stage: "companion",
|
|
542
|
+
description: `Start XCTest companion on port ${resolved.touchPort}`,
|
|
543
|
+
action: {
|
|
544
|
+
type: "command",
|
|
545
|
+
background: true,
|
|
546
|
+
processKey: "companion",
|
|
547
|
+
command: xcodebuild(
|
|
548
|
+
[
|
|
549
|
+
"test",
|
|
550
|
+
"-workspace",
|
|
551
|
+
ios.workspace,
|
|
552
|
+
"-scheme",
|
|
553
|
+
resolved.uitestScheme,
|
|
554
|
+
"-destination",
|
|
555
|
+
resolved.destination,
|
|
556
|
+
`-only-testing:${resolved.uitestScheme}/${DEFAULTS.xctestServerTest}`,
|
|
557
|
+
`RCT_METRO_PORT=${metro.port}`
|
|
558
|
+
],
|
|
559
|
+
{
|
|
560
|
+
RN_TOUCH_XCTEST_PORT: String(resolved.touchPort),
|
|
561
|
+
RN_TOUCH_XCTEST_CONFIG_FILE: resolved.runtimeConfigFile
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
push({
|
|
567
|
+
id: "ios.companion-ready",
|
|
568
|
+
stage: "companion",
|
|
569
|
+
description: "Wait for companion to accept a hello",
|
|
570
|
+
action: {
|
|
571
|
+
type: "probe",
|
|
572
|
+
probe: {
|
|
573
|
+
kind: "xctest-hello",
|
|
574
|
+
port: resolved.touchPort,
|
|
575
|
+
tokenFile: resolved.tokenFile,
|
|
576
|
+
timeoutMs: resolved.companionReadyTimeoutMs
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
if (isDevClient) {
|
|
581
|
+
push({
|
|
582
|
+
id: "ios.terminate-before-launch",
|
|
583
|
+
stage: "app-launch",
|
|
584
|
+
description: "Terminate any running instance (cold launch requires it)",
|
|
585
|
+
action: {
|
|
586
|
+
type: "command",
|
|
587
|
+
command: xcrun(["simctl", "terminate", resolved.simUdid, ios.bundleId]),
|
|
588
|
+
allowFailure: true
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
push({
|
|
592
|
+
id: "ios.launch",
|
|
593
|
+
stage: "app-launch",
|
|
594
|
+
description: `Cold-launch dev client via --initialUrl ${resolved.initialUrl}`,
|
|
595
|
+
action: {
|
|
596
|
+
type: "command",
|
|
597
|
+
command: xcrun([
|
|
598
|
+
"simctl",
|
|
599
|
+
"launch",
|
|
600
|
+
resolved.simUdid,
|
|
601
|
+
ios.bundleId,
|
|
602
|
+
"--initialUrl",
|
|
603
|
+
resolved.initialUrl
|
|
604
|
+
])
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
push({
|
|
609
|
+
id: "ios.hermes",
|
|
610
|
+
stage: "hermes-target",
|
|
611
|
+
description: "Wait for Hermes target",
|
|
612
|
+
action: {
|
|
613
|
+
type: "probe",
|
|
614
|
+
probe: {
|
|
615
|
+
kind: "hermes-target",
|
|
616
|
+
platform: "ios",
|
|
617
|
+
metroUrl: metro.url,
|
|
618
|
+
appId: ios.bundleId,
|
|
619
|
+
deviceNameMatch: resolved.simName,
|
|
620
|
+
timeoutMs: resolved.hermesTimeoutMs
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
const cleanup = [
|
|
625
|
+
{ type: "kill-process", processKey: "companion", description: "Stop XCTest companion" },
|
|
626
|
+
{
|
|
627
|
+
type: "free-port",
|
|
628
|
+
port: resolved.touchPort,
|
|
629
|
+
description: "Free companion port (reap sim-hosted child)"
|
|
630
|
+
},
|
|
631
|
+
{ type: "kill-process", processKey: "metro", description: "Stop runner-owned Metro" },
|
|
632
|
+
{ type: "remove-file", path: resolved.tokenFile, description: "Remove per-run token file" },
|
|
633
|
+
// REQ-SEC-004: the per-run runtime config (port + token-file ref) is written
|
|
634
|
+
// into the UI-test target every run; remove it so no generated artifact is
|
|
635
|
+
// left in the app project.
|
|
636
|
+
{
|
|
637
|
+
type: "remove-file",
|
|
638
|
+
path: resolved.runtimeConfigFile,
|
|
639
|
+
description: "Remove per-run companion runtime config"
|
|
640
|
+
}
|
|
641
|
+
];
|
|
642
|
+
return {
|
|
643
|
+
platform: "ios",
|
|
644
|
+
steps,
|
|
645
|
+
cleanup,
|
|
646
|
+
driverEnv: buildIosDriverEnv(resolved, metro, timeoutMs),
|
|
647
|
+
playwright: playwrightCommand(playwright, specs, passthrough)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function runtimeConfigJson(resolved, ios) {
|
|
651
|
+
return JSON.stringify({
|
|
652
|
+
port: resolved.touchPort,
|
|
653
|
+
authTokenFile: resolved.tokenFile,
|
|
654
|
+
launch: ios.launch.mode
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function defaultsArgs(key, value) {
|
|
658
|
+
if (typeof value === "boolean") return [key, "-bool", value ? "YES" : "NO"];
|
|
659
|
+
if (typeof value === "number") return [key, "-int", String(value)];
|
|
660
|
+
return [key, value];
|
|
661
|
+
}
|
|
662
|
+
function xcrun(args) {
|
|
663
|
+
return { command: "xcrun", args };
|
|
664
|
+
}
|
|
665
|
+
function xcodebuild(args, env) {
|
|
666
|
+
return env ? { command: "env", args: ["-u", "LD", "xcodebuild", ...args], env } : { command: "env", args: ["-u", "LD", "xcodebuild", ...args] };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/plan/resolved.ts
|
|
670
|
+
function resolveMetro(metro, overrides = {}) {
|
|
671
|
+
const host = overrides.host ?? metro?.host ?? DEFAULTS.metroHost;
|
|
672
|
+
const fromUrl = parseMetroUrl(overrides.url ?? metro?.url);
|
|
673
|
+
const port = fromUrl?.port ?? overrides.port ?? metro?.port ?? DEFAULTS.metroPort;
|
|
674
|
+
const resolvedHost = fromUrl?.host ?? host;
|
|
675
|
+
const url = fromUrl?.url ?? `http://${resolvedHost}:${port}`;
|
|
676
|
+
return {
|
|
677
|
+
url,
|
|
678
|
+
host: resolvedHost,
|
|
679
|
+
port,
|
|
680
|
+
command: metro?.command,
|
|
681
|
+
reuseExisting: metro?.reuseExisting ?? false,
|
|
682
|
+
readyTimeoutMs: metro?.readyTimeoutMs ?? DEFAULTS.metroReadyTimeoutMs
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function parseMetroUrl(raw) {
|
|
686
|
+
if (!raw) return void 0;
|
|
687
|
+
const parsed = new URL(raw);
|
|
688
|
+
const port = parsed.port ? Number.parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : 80;
|
|
689
|
+
return { url: raw.replace(/\/$/, ""), host: parsed.hostname, port };
|
|
690
|
+
}
|
|
691
|
+
function uitestScheme(ios) {
|
|
692
|
+
return ios.uitestScheme ?? `${ios.appScheme}UITests`;
|
|
693
|
+
}
|
|
694
|
+
function instrumentationTarget(android) {
|
|
695
|
+
return android.instrumentationTarget ?? `${android.packageName}.test/${DEFAULTS.instrumentationClass}`;
|
|
696
|
+
}
|
|
697
|
+
function placeholderIos(ios, metro) {
|
|
698
|
+
return {
|
|
699
|
+
simUdid: "<sim-udid>",
|
|
700
|
+
simName: "<sim-name>",
|
|
701
|
+
destination: ios.destination ?? "platform=iOS Simulator,id=<sim-udid>",
|
|
702
|
+
uitestScheme: uitestScheme(ios),
|
|
703
|
+
touchPort: ios.companion?.port ?? DEFAULTS.companionPort,
|
|
704
|
+
companionReadyTimeoutMs: ios.companion?.readyTimeoutMs ?? DEFAULTS.iosCompanionReadyTimeoutMs,
|
|
705
|
+
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
706
|
+
tokenFile: SECRET_PLACEHOLDER,
|
|
707
|
+
runtimeConfigFile: "<runtime-config>",
|
|
708
|
+
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function placeholderAndroid(android, _metro) {
|
|
712
|
+
return {
|
|
713
|
+
serial: "<android-serial>",
|
|
714
|
+
touchPort: android.companion?.port ?? DEFAULTS.companionPort,
|
|
715
|
+
companionReadyTimeoutMs: android.companion?.readyTimeoutMs ?? DEFAULTS.androidCompanionReadyTimeoutMs,
|
|
716
|
+
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
717
|
+
tokenFile: SECRET_PLACEHOLDER,
|
|
718
|
+
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
719
|
+
instrumentationTarget: instrumentationTarget(android)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/build-plan.ts
|
|
724
|
+
function buildDryRunPlan(config, platform, opts = {}) {
|
|
725
|
+
const metro = resolveMetro(config.metro, opts.metroOverrides ?? {});
|
|
726
|
+
const specs = opts.specs ?? [];
|
|
727
|
+
const passthrough = opts.passthrough ?? [];
|
|
728
|
+
if (platform === "ios") {
|
|
729
|
+
const ios = config.ios;
|
|
730
|
+
if (!ios) throw new Error("config.ios is required to plan the ios platform");
|
|
731
|
+
return planIos({
|
|
732
|
+
ios,
|
|
733
|
+
metro,
|
|
734
|
+
resolved: placeholderIos(ios, metro),
|
|
735
|
+
playwright: config.playwright,
|
|
736
|
+
timeoutMs: config.timeoutMs,
|
|
737
|
+
specs,
|
|
738
|
+
passthrough
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
const android = config.android;
|
|
742
|
+
if (!android) throw new Error("config.android is required to plan the android platform");
|
|
743
|
+
return planAndroid({
|
|
744
|
+
android,
|
|
745
|
+
metro,
|
|
746
|
+
resolved: placeholderAndroid(android),
|
|
747
|
+
playwright: config.playwright,
|
|
748
|
+
timeoutMs: config.timeoutMs,
|
|
749
|
+
specs,
|
|
750
|
+
passthrough,
|
|
751
|
+
hermesDeviceName: "<android-device>"
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/print-plan.ts
|
|
756
|
+
function renderPlan(plan) {
|
|
757
|
+
const lines = [];
|
|
758
|
+
lines.push(`Plan (${plan.platform}) \u2014 ${plan.steps.length} steps`);
|
|
759
|
+
for (const step of plan.steps) {
|
|
760
|
+
const tag = `[${step.stage}]`.padEnd(15);
|
|
761
|
+
const skip = step.skippable ? " (skip-build: skipped)" : "";
|
|
762
|
+
lines.push(` ${tag}${step.id} \u2014 ${step.description}${skip}`);
|
|
763
|
+
lines.push(` ${renderAction(step.action)}`);
|
|
764
|
+
}
|
|
765
|
+
lines.push("");
|
|
766
|
+
lines.push("Driver env (handed to Playwright):");
|
|
767
|
+
for (const [key, value] of Object.entries(plan.driverEnv)) {
|
|
768
|
+
lines.push(` ${key}=${value}`);
|
|
769
|
+
}
|
|
770
|
+
lines.push("");
|
|
771
|
+
lines.push("Playwright:");
|
|
772
|
+
lines.push(` ${renderCommand(plan.playwright)}`);
|
|
773
|
+
lines.push("");
|
|
774
|
+
lines.push("Cleanup (defensive, idempotent):");
|
|
775
|
+
for (const action of plan.cleanup) {
|
|
776
|
+
lines.push(` - ${action.description}: ${renderCleanup(action)}`);
|
|
777
|
+
}
|
|
778
|
+
return lines.join("\n");
|
|
779
|
+
}
|
|
780
|
+
function renderAction(action) {
|
|
781
|
+
switch (action.type) {
|
|
782
|
+
case "command":
|
|
783
|
+
return `${action.background ? "spawn " : "$ "}${renderCommand(action.command)}${action.allowFailure ? " (best-effort)" : ""}`;
|
|
784
|
+
case "write-file":
|
|
785
|
+
return `write ${action.path}${action.mode ? ` (mode ${action.mode.toString(8)})` : ""}`;
|
|
786
|
+
case "free-port":
|
|
787
|
+
return `free-port ${action.port}`;
|
|
788
|
+
case "probe":
|
|
789
|
+
return `probe ${renderProbe(action.probe)}`;
|
|
790
|
+
default: {
|
|
791
|
+
const _exhaustive = action;
|
|
792
|
+
throw new Error(`unhandled action: ${JSON.stringify(_exhaustive)}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function renderCleanup(action) {
|
|
797
|
+
switch (action.type) {
|
|
798
|
+
case "kill-process":
|
|
799
|
+
return `kill ${action.processKey}`;
|
|
800
|
+
case "free-port":
|
|
801
|
+
return `free-port ${action.port}`;
|
|
802
|
+
case "remove-file":
|
|
803
|
+
return `rm ${action.path}`;
|
|
804
|
+
case "command":
|
|
805
|
+
return `$ ${renderCommand(action.command)}`;
|
|
806
|
+
default: {
|
|
807
|
+
const _exhaustive = action;
|
|
808
|
+
throw new Error(`unhandled cleanup: ${JSON.stringify(_exhaustive)}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function renderCommand(command) {
|
|
813
|
+
const env = command.env ? `${Object.entries(command.env).map(([k, v]) => `${k}=${v}`).join(" ")} ` : "";
|
|
814
|
+
const stdin = command.stdinFromFile ? ` < ${command.stdinFromFile}` : command.stdinContents ? " < <stdin>" : "";
|
|
815
|
+
const cwd = command.cwd ? ` (cwd: ${command.cwd})` : "";
|
|
816
|
+
return `${env}${command.command} ${command.args.join(" ")}${stdin}${cwd}`.trim();
|
|
817
|
+
}
|
|
818
|
+
function renderProbe(probe) {
|
|
819
|
+
switch (probe.kind) {
|
|
820
|
+
case "metro-status":
|
|
821
|
+
return `metro-status ${probe.metroUrl} (\u2264${probe.timeoutMs}ms)`;
|
|
822
|
+
case "hermes-target":
|
|
823
|
+
return `hermes-target ${probe.appId}${probe.deviceNameMatch ? ` @ ${probe.deviceNameMatch}` : ""} (\u2264${probe.timeoutMs}ms)`;
|
|
824
|
+
case "xctest-hello":
|
|
825
|
+
return `xctest-hello :${probe.port} (\u2264${probe.timeoutMs}ms)`;
|
|
826
|
+
case "instrumentation-hello":
|
|
827
|
+
return `instrumentation-hello :${probe.port} (\u2264${probe.timeoutMs}ms)`;
|
|
828
|
+
default: {
|
|
829
|
+
const _exhaustive = probe;
|
|
830
|
+
throw new Error(`unhandled probe: ${JSON.stringify(_exhaustive)}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/validate.ts
|
|
836
|
+
var LAUNCH_MODES = ["launch", "activate", "attach"];
|
|
837
|
+
var LAUNCH_KINDS = ["plain", "expo-dev-client"];
|
|
838
|
+
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["metro", "ios", "android", "playwright", "timeoutMs"]);
|
|
839
|
+
var METRO_KEYS = /* @__PURE__ */ new Set(["url", "command", "host", "port", "reuseExisting", "readyTimeoutMs"]);
|
|
840
|
+
var LAUNCH_KEYS = /* @__PURE__ */ new Set(["mode", "kind", "initialUrl"]);
|
|
841
|
+
var COMPANION_KEYS = /* @__PURE__ */ new Set(["port", "readyTimeoutMs"]);
|
|
842
|
+
var IOS_KEYS = /* @__PURE__ */ new Set([
|
|
843
|
+
"bundleId",
|
|
844
|
+
"workspace",
|
|
845
|
+
"appScheme",
|
|
846
|
+
"uitestScheme",
|
|
847
|
+
"destination",
|
|
848
|
+
"launch",
|
|
849
|
+
"companion",
|
|
850
|
+
"defaults"
|
|
851
|
+
]);
|
|
852
|
+
var ANDROID_KEYS = /* @__PURE__ */ new Set([
|
|
853
|
+
"packageName",
|
|
854
|
+
"activity",
|
|
855
|
+
"gradleTasks",
|
|
856
|
+
"appApkPath",
|
|
857
|
+
"testApkPath",
|
|
858
|
+
"instrumentationTarget",
|
|
859
|
+
"launch",
|
|
860
|
+
"companion"
|
|
861
|
+
]);
|
|
862
|
+
var PLAYWRIGHT_KEYS = /* @__PURE__ */ new Set(["config", "specs"]);
|
|
863
|
+
function validateConfig(config, platforms) {
|
|
864
|
+
const errors = [];
|
|
865
|
+
if (!isRecord(config)) {
|
|
866
|
+
return {
|
|
867
|
+
ok: false,
|
|
868
|
+
errors: ["config: expected an object exported as default from rn-driver.config"]
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
reportUnknownKeys("config", config, TOP_LEVEL_KEYS, errors);
|
|
872
|
+
if (config.timeoutMs !== void 0 && !isPositiveNumber(config.timeoutMs)) {
|
|
873
|
+
errors.push("config.timeoutMs: expected a positive number");
|
|
874
|
+
}
|
|
875
|
+
validateMetro(config.metro, errors);
|
|
876
|
+
validatePlaywright(config.playwright, errors);
|
|
877
|
+
if (platforms.includes("ios")) validateIos(config.ios, errors);
|
|
878
|
+
if (platforms.includes("android")) validateAndroid(config.android, errors);
|
|
879
|
+
return { ok: errors.length === 0, errors };
|
|
880
|
+
}
|
|
881
|
+
function validateMetro(metro, errors) {
|
|
882
|
+
if (metro === void 0) return;
|
|
883
|
+
if (!isRecord(metro)) {
|
|
884
|
+
errors.push("config.metro: expected an object");
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
reportUnknownKeys("config.metro", metro, METRO_KEYS, errors);
|
|
888
|
+
optionalString("config.metro.url", metro.url, errors);
|
|
889
|
+
optionalString("config.metro.command", metro.command, errors);
|
|
890
|
+
optionalString("config.metro.host", metro.host, errors);
|
|
891
|
+
if (metro.port !== void 0 && !isPort(metro.port))
|
|
892
|
+
errors.push("config.metro.port: expected a port (1-65535)");
|
|
893
|
+
if (metro.reuseExisting !== void 0 && typeof metro.reuseExisting !== "boolean") {
|
|
894
|
+
errors.push("config.metro.reuseExisting: expected a boolean");
|
|
895
|
+
}
|
|
896
|
+
if (metro.readyTimeoutMs !== void 0 && !isPositiveNumber(metro.readyTimeoutMs)) {
|
|
897
|
+
errors.push("config.metro.readyTimeoutMs: expected a positive number");
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function validatePlaywright(playwright, errors) {
|
|
901
|
+
if (playwright === void 0) return;
|
|
902
|
+
if (!isRecord(playwright)) {
|
|
903
|
+
errors.push("config.playwright: expected an object");
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
reportUnknownKeys("config.playwright", playwright, PLAYWRIGHT_KEYS, errors);
|
|
907
|
+
optionalString("config.playwright.config", playwright.config, errors);
|
|
908
|
+
if (playwright.specs !== void 0 && !isStringArray(playwright.specs)) {
|
|
909
|
+
errors.push("config.playwright.specs: expected an array of strings");
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function validateIos(ios, errors) {
|
|
913
|
+
if (!isRecord(ios)) {
|
|
914
|
+
errors.push('config.ios: required when platform "ios" is selected (expected an object)');
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
reportUnknownKeys("config.ios", ios, IOS_KEYS, errors);
|
|
918
|
+
requireString("config.ios.bundleId", ios.bundleId, errors);
|
|
919
|
+
requireString("config.ios.workspace", ios.workspace, errors);
|
|
920
|
+
requireString("config.ios.appScheme", ios.appScheme, errors);
|
|
921
|
+
optionalString("config.ios.uitestScheme", ios.uitestScheme, errors);
|
|
922
|
+
optionalString("config.ios.destination", ios.destination, errors);
|
|
923
|
+
validateCompanion("config.ios.companion", ios.companion, errors);
|
|
924
|
+
if (ios.defaults !== void 0) {
|
|
925
|
+
if (!isRecord(ios.defaults)) {
|
|
926
|
+
errors.push("config.ios.defaults: expected an object of key -> string|number|boolean");
|
|
927
|
+
} else {
|
|
928
|
+
for (const [key, value] of Object.entries(ios.defaults)) {
|
|
929
|
+
const kind = typeof value;
|
|
930
|
+
if (kind !== "string" && kind !== "number" && kind !== "boolean") {
|
|
931
|
+
errors.push(`config.ios.defaults.${key}: expected string|number|boolean`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const launch = validateLaunch("config.ios.launch", ios.launch, errors);
|
|
937
|
+
if (launch && launch.kind === "expo-dev-client" && launch.mode !== "attach") {
|
|
938
|
+
errors.push(
|
|
939
|
+
'config.ios.launch: kind "expo-dev-client" requires mode "attach" (the host owns the launch)'
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (launch && launch.mode === "attach" && launch.kind !== "expo-dev-client") {
|
|
943
|
+
errors.push(
|
|
944
|
+
'config.ios.launch: mode "attach" requires kind "expo-dev-client"; a plain app uses mode "launch" or "activate" (the companion launches it)'
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function validateAndroid(android, errors) {
|
|
949
|
+
if (!isRecord(android)) {
|
|
950
|
+
errors.push('config.android: required when platform "android" is selected (expected an object)');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
reportUnknownKeys("config.android", android, ANDROID_KEYS, errors);
|
|
954
|
+
requireAndroidPackage("config.android.packageName", android.packageName, errors);
|
|
955
|
+
requireAndroidActivity("config.android.activity", android.activity, errors);
|
|
956
|
+
optionalString("config.android.appApkPath", android.appApkPath, errors);
|
|
957
|
+
optionalString("config.android.testApkPath", android.testApkPath, errors);
|
|
958
|
+
if (android.instrumentationTarget !== void 0)
|
|
959
|
+
requireInstrumentationTarget(
|
|
960
|
+
"config.android.instrumentationTarget",
|
|
961
|
+
android.instrumentationTarget,
|
|
962
|
+
errors
|
|
963
|
+
);
|
|
964
|
+
if (android.gradleTasks !== void 0 && !isStringArray(android.gradleTasks)) {
|
|
965
|
+
errors.push("config.android.gradleTasks: expected an array of strings");
|
|
966
|
+
}
|
|
967
|
+
validateCompanion("config.android.companion", android.companion, errors);
|
|
968
|
+
validateLaunch("config.android.launch", android.launch, errors);
|
|
969
|
+
}
|
|
970
|
+
function validateCompanion(path, companion, errors) {
|
|
971
|
+
if (companion === void 0) return;
|
|
972
|
+
if (!isRecord(companion)) {
|
|
973
|
+
errors.push(`${path}: expected an object`);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
reportUnknownKeys(path, companion, COMPANION_KEYS, errors);
|
|
977
|
+
if (companion.port !== void 0 && !isPort(companion.port))
|
|
978
|
+
errors.push(`${path}.port: expected a port (1-65535)`);
|
|
979
|
+
if (companion.readyTimeoutMs !== void 0 && !isPositiveNumber(companion.readyTimeoutMs)) {
|
|
980
|
+
errors.push(`${path}.readyTimeoutMs: expected a positive number`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function validateLaunch(path, launch, errors) {
|
|
984
|
+
if (!isRecord(launch)) {
|
|
985
|
+
errors.push(`${path}: required (expected an object with mode and kind)`);
|
|
986
|
+
return void 0;
|
|
987
|
+
}
|
|
988
|
+
reportUnknownKeys(path, launch, LAUNCH_KEYS, errors);
|
|
989
|
+
optionalString(`${path}.initialUrl`, launch.initialUrl, errors);
|
|
990
|
+
const modeOk = typeof launch.mode === "string" && LAUNCH_MODES.includes(launch.mode);
|
|
991
|
+
const kindOk = typeof launch.kind === "string" && LAUNCH_KINDS.includes(launch.kind);
|
|
992
|
+
if (!modeOk) errors.push(`${path}.mode: expected one of ${LAUNCH_MODES.join(", ")}`);
|
|
993
|
+
if (!kindOk) errors.push(`${path}.kind: expected one of ${LAUNCH_KINDS.join(", ")}`);
|
|
994
|
+
return modeOk && kindOk ? { mode: launch.mode, kind: launch.kind } : void 0;
|
|
995
|
+
}
|
|
996
|
+
function isRecord(value) {
|
|
997
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
998
|
+
}
|
|
999
|
+
function isPositiveNumber(value) {
|
|
1000
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
1001
|
+
}
|
|
1002
|
+
function isPort(value) {
|
|
1003
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
|
|
1004
|
+
}
|
|
1005
|
+
function isStringArray(value) {
|
|
1006
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1007
|
+
}
|
|
1008
|
+
function requireString(path, value, errors) {
|
|
1009
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
1010
|
+
errors.push(`${path}: required non-empty string`);
|
|
1011
|
+
}
|
|
1012
|
+
var ANDROID_PACKAGE_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
1013
|
+
var ANDROID_ACTIVITY_RE = /^\.?[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
1014
|
+
function requireAndroidPackage(path, value, errors) {
|
|
1015
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1016
|
+
errors.push(`${path}: required non-empty string`);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (!ANDROID_PACKAGE_RE.test(value))
|
|
1020
|
+
errors.push(`${path}: expected a valid Android application id (e.g. com.company.app)`);
|
|
1021
|
+
}
|
|
1022
|
+
function requireAndroidActivity(path, value, errors) {
|
|
1023
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1024
|
+
errors.push(`${path}: required non-empty string`);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (!ANDROID_ACTIVITY_RE.test(value))
|
|
1028
|
+
errors.push(`${path}: expected an activity name (e.g. .MainActivity)`);
|
|
1029
|
+
}
|
|
1030
|
+
var ANDROID_INSTRUMENTATION_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*\/[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
1031
|
+
function requireInstrumentationTarget(path, value, errors) {
|
|
1032
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1033
|
+
errors.push(`${path}: expected a non-empty string`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (!ANDROID_INSTRUMENTATION_RE.test(value))
|
|
1037
|
+
errors.push(`${path}: expected an am instrument target (e.g. com.app.test/com.app.Runner)`);
|
|
1038
|
+
}
|
|
1039
|
+
function optionalString(path, value, errors) {
|
|
1040
|
+
if (value !== void 0 && typeof value !== "string") errors.push(`${path}: expected a string`);
|
|
1041
|
+
}
|
|
1042
|
+
function reportUnknownKeys(path, value, allowed, errors) {
|
|
1043
|
+
for (const key of Object.keys(value)) {
|
|
1044
|
+
if (!allowed.has(key)) errors.push(`${path}.${key}: unknown key`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
var ConfigValidationError = class extends Error {
|
|
1048
|
+
errors;
|
|
1049
|
+
constructor(errors) {
|
|
1050
|
+
super(`Invalid rn-driver config:
|
|
1051
|
+
- ${errors.join("\n - ")}`);
|
|
1052
|
+
this.name = "ConfigValidationError";
|
|
1053
|
+
this.errors = errors;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
exports.ConfigValidationError = ConfigValidationError;
|
|
1058
|
+
exports.buildDryRunPlan = buildDryRunPlan;
|
|
1059
|
+
exports.defineRnDriverConfig = defineRnDriverConfig;
|
|
1060
|
+
exports.renderPlan = renderPlan;
|
|
1061
|
+
exports.validateConfig = validateConfig;
|
|
1062
|
+
//# sourceMappingURL=index.js.map
|
|
1063
|
+
//# sourceMappingURL=index.js.map
|