@unrulysystems/rn-playwright-driver-runner 0.1.0 → 0.2.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 +23 -0
- package/README.md +79 -6
- package/bin/rn-driver.mjs +9 -0
- package/dist/cli.js +140 -45
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +140 -45
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +75 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +75 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -3
- package/bin/rn-driver.ts +0 -17
package/dist/cli.mjs
CHANGED
|
@@ -47,6 +47,10 @@ var DEFAULTS = {
|
|
|
47
47
|
androidTokenFileName: "rn-driver-touch-token"
|
|
48
48
|
};
|
|
49
49
|
var SECRET_PLACEHOLDER = "<token-file>";
|
|
50
|
+
var COMPANION_FAILURE_MARKERS = {
|
|
51
|
+
ios: ["** BUILD FAILED **", "** TEST FAILED **"],
|
|
52
|
+
android: ["INSTRUMENTATION_FAILED", "Process crashed"]
|
|
53
|
+
};
|
|
50
54
|
|
|
51
55
|
// src/plan/env.ts
|
|
52
56
|
function buildIosDriverEnv(resolved, metro, timeoutMs) {
|
|
@@ -209,9 +213,10 @@ function planAndroid(input) {
|
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
215
|
});
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
push(
|
|
216
|
+
const launch1Command = launchCommandFor(android, resolved, serial, { forceStopBefore: true });
|
|
217
|
+
const launch2Command = launchCommandFor(android, resolved, serial, { forceStopBefore: false });
|
|
218
|
+
push(launchStep("android.launch-1", android, launch1Command));
|
|
219
|
+
push(hermesStep("android.hermes-1", android, metro, resolved, hermesDeviceName, launch1Command));
|
|
215
220
|
push({
|
|
216
221
|
id: "android.forward-clean",
|
|
217
222
|
stage: "companion",
|
|
@@ -265,11 +270,14 @@ function planAndroid(input) {
|
|
|
265
270
|
port: resolved.touchPort,
|
|
266
271
|
tokenFile: resolved.tokenFile,
|
|
267
272
|
timeoutMs: resolved.companionReadyTimeoutMs
|
|
268
|
-
}
|
|
273
|
+
},
|
|
274
|
+
// Abort early if `am instrument` reports the companion failed to start (crash / missing
|
|
275
|
+
// androidTest target) instead of waiting out the readiness budget.
|
|
276
|
+
failureMarkers: COMPANION_FAILURE_MARKERS.android
|
|
269
277
|
}
|
|
270
278
|
});
|
|
271
|
-
push(launchStep("android.launch-2", android,
|
|
272
|
-
push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName,
|
|
279
|
+
push(launchStep("android.launch-2", android, launch2Command));
|
|
280
|
+
push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName, launch2Command));
|
|
273
281
|
const cleanup = [
|
|
274
282
|
{
|
|
275
283
|
type: "kill-process",
|
|
@@ -324,22 +332,34 @@ function planAndroid(input) {
|
|
|
324
332
|
playwright: playwrightCommand(playwright, specs, passthrough)
|
|
325
333
|
};
|
|
326
334
|
}
|
|
327
|
-
function launchCommandFor(android, serial) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
function launchCommandFor(android, resolved, serial, opts) {
|
|
336
|
+
if (android.launch.kind === "plain") {
|
|
337
|
+
return adb(serial, [
|
|
338
|
+
"shell",
|
|
339
|
+
"am",
|
|
340
|
+
"start",
|
|
341
|
+
"-W",
|
|
342
|
+
"-n",
|
|
343
|
+
`${android.packageName}/${android.activity}`
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
if (!android.scheme) {
|
|
347
|
+
throw new Error('android.scheme is required when android.launch.kind is "expo-dev-client"');
|
|
348
|
+
}
|
|
349
|
+
const launchScript = `am start -a android.intent.action.VIEW -d ${shellSingleQuote(
|
|
350
|
+
devClientUrl(android.scheme, resolved.initialUrl)
|
|
351
|
+
)}`;
|
|
352
|
+
return adbShellScript(
|
|
353
|
+
serial,
|
|
354
|
+
opts.forceStopBefore ? `am force-stop ${android.packageName} && ${launchScript}` : launchScript
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
function launchStep(id, android, command) {
|
|
338
358
|
return {
|
|
339
359
|
id,
|
|
340
360
|
stage: "app-launch",
|
|
341
361
|
description: `Launch ${android.packageName}/${android.activity}`,
|
|
342
|
-
action: { type: "command", command
|
|
362
|
+
action: { type: "command", command }
|
|
343
363
|
};
|
|
344
364
|
}
|
|
345
365
|
function hermesStep(id, android, metro, resolved, deviceNameMatch, launchCommand) {
|
|
@@ -373,6 +393,12 @@ function debugHostXml(host) {
|
|
|
373
393
|
function adb(serial, args) {
|
|
374
394
|
return { command: "adb", args: ["-s", serial, ...args] };
|
|
375
395
|
}
|
|
396
|
+
function devClientUrl(scheme, initialUrl) {
|
|
397
|
+
return `${scheme}://expo-development-client/?url=${initialUrl}`;
|
|
398
|
+
}
|
|
399
|
+
function shellSingleQuote(value) {
|
|
400
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
401
|
+
}
|
|
376
402
|
function adbShellScript(serial, remote) {
|
|
377
403
|
return { command: "adb", args: ["-s", serial, "shell", remote] };
|
|
378
404
|
}
|
|
@@ -418,8 +444,7 @@ function planIos(input) {
|
|
|
418
444
|
// Pass the resolved UI-test scheme so a custom `ios.uitestScheme` scaffolds
|
|
419
445
|
// the SAME target that companion startup later builds (default is
|
|
420
446
|
// `${appScheme}UITests`).
|
|
421
|
-
command:
|
|
422
|
-
"rn-driver-xctest-scaffold",
|
|
447
|
+
command: cmd("node_modules/.bin/rn-driver-xctest-scaffold", [
|
|
423
448
|
"--ios-dir",
|
|
424
449
|
"ios",
|
|
425
450
|
"--project-name",
|
|
@@ -579,7 +604,10 @@ function planIos(input) {
|
|
|
579
604
|
port: resolved.touchPort,
|
|
580
605
|
tokenFile: resolved.tokenFile,
|
|
581
606
|
timeoutMs: resolved.companionReadyTimeoutMs
|
|
582
|
-
}
|
|
607
|
+
},
|
|
608
|
+
// Abort early if `xcodebuild test` reports a build/test failure (it lingers "alive" after, so
|
|
609
|
+
// the 300s readiness budget would otherwise be burnt waiting for a companion that cannot bind).
|
|
610
|
+
failureMarkers: COMPANION_FAILURE_MARKERS.ios
|
|
583
611
|
}
|
|
584
612
|
});
|
|
585
613
|
if (isDevClient) {
|
|
@@ -713,7 +741,7 @@ function placeholderIos(ios, metro) {
|
|
|
713
741
|
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
714
742
|
};
|
|
715
743
|
}
|
|
716
|
-
function placeholderAndroid(android,
|
|
744
|
+
function placeholderAndroid(android, metro) {
|
|
717
745
|
return {
|
|
718
746
|
serial: "<android-serial>",
|
|
719
747
|
touchPort: android.companion?.port ?? DEFAULTS.companionPort,
|
|
@@ -721,7 +749,8 @@ function placeholderAndroid(android, _metro) {
|
|
|
721
749
|
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
722
750
|
tokenFile: SECRET_PLACEHOLDER,
|
|
723
751
|
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
724
|
-
instrumentationTarget: instrumentationTarget(android)
|
|
752
|
+
instrumentationTarget: instrumentationTarget(android),
|
|
753
|
+
initialUrl: android.launch.initialUrl ?? metro.url
|
|
725
754
|
};
|
|
726
755
|
}
|
|
727
756
|
|
|
@@ -748,7 +777,7 @@ function buildDryRunPlan(config, platform, opts = {}) {
|
|
|
748
777
|
return planAndroid({
|
|
749
778
|
android,
|
|
750
779
|
metro,
|
|
751
|
-
resolved: placeholderAndroid(android),
|
|
780
|
+
resolved: placeholderAndroid(android, metro),
|
|
752
781
|
playwright: config.playwright,
|
|
753
782
|
timeoutMs: config.timeoutMs,
|
|
754
783
|
specs,
|
|
@@ -834,8 +863,10 @@ function renderAction(action) {
|
|
|
834
863
|
return `write ${action.path}${action.mode ? ` (mode ${action.mode.toString(8)})` : ""}`;
|
|
835
864
|
case "free-port":
|
|
836
865
|
return `free-port ${action.port}`;
|
|
837
|
-
case "probe":
|
|
838
|
-
|
|
866
|
+
case "probe": {
|
|
867
|
+
const fastFail = action.failureMarkers?.length ? ` [fast-fail on: ${action.failureMarkers.join(", ")}]` : "";
|
|
868
|
+
return `probe ${renderProbe(action.probe)}${fastFail}`;
|
|
869
|
+
}
|
|
839
870
|
default: {
|
|
840
871
|
const _exhaustive = action;
|
|
841
872
|
throw new Error(`unhandled action: ${JSON.stringify(_exhaustive)}`);
|
|
@@ -881,6 +912,25 @@ function renderProbe(probe) {
|
|
|
881
912
|
}
|
|
882
913
|
}
|
|
883
914
|
|
|
915
|
+
// src/runner/probe-failure.ts
|
|
916
|
+
var ProbeFailure = class extends Error {
|
|
917
|
+
marker;
|
|
918
|
+
constructor(marker, detail) {
|
|
919
|
+
super(
|
|
920
|
+
`companion process reported a terminal failure ("${marker}") \u2014 aborting the readiness wait. The build/test failed; it will not become ready.${detail ? `
|
|
921
|
+
${detail}` : ""}`
|
|
922
|
+
);
|
|
923
|
+
this.name = "ProbeFailure";
|
|
924
|
+
this.marker = marker;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
function findFailureMarker(log, markers) {
|
|
928
|
+
for (const marker of markers) {
|
|
929
|
+
if (log.includes(marker)) return marker;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
|
|
884
934
|
// src/runner/execute.ts
|
|
885
935
|
var StageError = class extends Error {
|
|
886
936
|
stage;
|
|
@@ -948,22 +998,30 @@ async function runStep(step, runner, opts, processes, isAlive) {
|
|
|
948
998
|
case "probe": {
|
|
949
999
|
const key = processKeyForProbe(action.probe);
|
|
950
1000
|
const aliveFn = key === null ? () => true : () => isAlive(key);
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
remaining
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1001
|
+
const watch = key !== null && action.failureMarkers && action.failureMarkers.length > 0 ? { logPath: logPathFor(opts.logDir, key), failureMarkers: action.failureMarkers } : void 0;
|
|
1002
|
+
try {
|
|
1003
|
+
let ready = await runner.probe(action.probe, aliveFn, watch);
|
|
1004
|
+
let remaining = action.retry?.max ?? 0;
|
|
1005
|
+
while (!ready && remaining > 0) {
|
|
1006
|
+
remaining -= 1;
|
|
1007
|
+
if (action.retry) {
|
|
1008
|
+
runner.log(`retry ${step.id}: re-running launch (${remaining} attempt(s) left)`);
|
|
1009
|
+
await runner.exec(action.retry.command);
|
|
1010
|
+
}
|
|
1011
|
+
ready = await runner.probe(action.probe, aliveFn, watch);
|
|
958
1012
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1013
|
+
if (!ready) {
|
|
1014
|
+
throw new StageError(
|
|
1015
|
+
step.stage,
|
|
1016
|
+
step.id,
|
|
1017
|
+
`readiness timed out after ${action.probe.timeoutMs}ms`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (error instanceof ProbeFailure) {
|
|
1022
|
+
throw new StageError(step.stage, step.id, error.message);
|
|
1023
|
+
}
|
|
1024
|
+
throw error;
|
|
967
1025
|
}
|
|
968
1026
|
return;
|
|
969
1027
|
}
|
|
@@ -1102,10 +1160,15 @@ var NodeProcessRunner = class {
|
|
|
1102
1160
|
}
|
|
1103
1161
|
if (pids.length > 0) await delay(1e3);
|
|
1104
1162
|
}
|
|
1105
|
-
probe(probe, isAlive) {
|
|
1163
|
+
probe(probe, isAlive, watch) {
|
|
1106
1164
|
const deadline = Date.now() + probe.timeoutMs;
|
|
1107
1165
|
const attempt = async () => {
|
|
1108
1166
|
for (; ; ) {
|
|
1167
|
+
if (watch) {
|
|
1168
|
+
const log = await readWatchedLog(watch.logPath);
|
|
1169
|
+
const marker = findFailureMarker(log, watch.failureMarkers);
|
|
1170
|
+
if (marker) throw new ProbeFailure(marker, lastLines(log));
|
|
1171
|
+
}
|
|
1109
1172
|
if (!isAlive()) return false;
|
|
1110
1173
|
if (await probeOnce(probe)) return true;
|
|
1111
1174
|
if (Date.now() >= deadline) return false;
|
|
@@ -1150,6 +1213,19 @@ function delay(ms) {
|
|
|
1150
1213
|
setTimeout(resolve, ms);
|
|
1151
1214
|
});
|
|
1152
1215
|
}
|
|
1216
|
+
async function readWatchedLog(path4) {
|
|
1217
|
+
try {
|
|
1218
|
+
return await readFile(path4, "utf8");
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
`cannot read companion log for fast-fail marker detection (${path4}): ${String(error)}`,
|
|
1222
|
+
{ cause: error }
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function lastLines(text, n) {
|
|
1227
|
+
return text.split("\n").filter((line) => line.trim().length > 0).slice(-12).join("\n");
|
|
1228
|
+
}
|
|
1153
1229
|
async function probeOnce(probe) {
|
|
1154
1230
|
switch (probe.kind) {
|
|
1155
1231
|
case "metro-status":
|
|
@@ -1288,7 +1364,7 @@ async function resolveIosTarget(ios, metro, opts) {
|
|
|
1288
1364
|
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
1289
1365
|
};
|
|
1290
1366
|
}
|
|
1291
|
-
async function resolveAndroidTarget(android,
|
|
1367
|
+
async function resolveAndroidTarget(android, metro, opts) {
|
|
1292
1368
|
const serial = await selectSerial(opts.device);
|
|
1293
1369
|
await requireBooted(serial);
|
|
1294
1370
|
const deviceName = (await capture("adb", ["-s", serial, "shell", "getprop", "ro.product.model"])).trim();
|
|
@@ -1301,7 +1377,8 @@ async function resolveAndroidTarget(android, _metro, opts) {
|
|
|
1301
1377
|
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
1302
1378
|
tokenFile,
|
|
1303
1379
|
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
1304
|
-
instrumentationTarget: instrumentationTarget(android)
|
|
1380
|
+
instrumentationTarget: instrumentationTarget(android),
|
|
1381
|
+
initialUrl: android.launch.initialUrl ?? metro.url
|
|
1305
1382
|
},
|
|
1306
1383
|
deviceName
|
|
1307
1384
|
};
|
|
@@ -1406,6 +1483,7 @@ var IOS_KEYS = /* @__PURE__ */ new Set([
|
|
|
1406
1483
|
var ANDROID_KEYS = /* @__PURE__ */ new Set([
|
|
1407
1484
|
"packageName",
|
|
1408
1485
|
"activity",
|
|
1486
|
+
"scheme",
|
|
1409
1487
|
"gradleTasks",
|
|
1410
1488
|
"appApkPath",
|
|
1411
1489
|
"testApkPath",
|
|
@@ -1507,6 +1585,8 @@ function validateAndroid(android, errors) {
|
|
|
1507
1585
|
reportUnknownKeys("config.android", android, ANDROID_KEYS, errors);
|
|
1508
1586
|
requireAndroidPackage("config.android.packageName", android.packageName, errors);
|
|
1509
1587
|
requireAndroidActivity("config.android.activity", android.activity, errors);
|
|
1588
|
+
if (android.scheme !== void 0)
|
|
1589
|
+
requireAppScheme("config.android.scheme", android.scheme, errors);
|
|
1510
1590
|
optionalString("config.android.appApkPath", android.appApkPath, errors);
|
|
1511
1591
|
optionalString("config.android.testApkPath", android.testApkPath, errors);
|
|
1512
1592
|
if (android.instrumentationTarget !== void 0)
|
|
@@ -1519,7 +1599,10 @@ function validateAndroid(android, errors) {
|
|
|
1519
1599
|
errors.push("config.android.gradleTasks: expected an array of strings");
|
|
1520
1600
|
}
|
|
1521
1601
|
validateCompanion("config.android.companion", android.companion, errors);
|
|
1522
|
-
validateLaunch("config.android.launch", android.launch, errors);
|
|
1602
|
+
const launch = validateLaunch("config.android.launch", android.launch, errors);
|
|
1603
|
+
if (launch?.kind === "expo-dev-client" && android.scheme === void 0) {
|
|
1604
|
+
errors.push('config.android.scheme: required when android.launch.kind is "expo-dev-client"');
|
|
1605
|
+
}
|
|
1523
1606
|
}
|
|
1524
1607
|
function validateCompanion(path4, companion, errors) {
|
|
1525
1608
|
if (companion === void 0) return;
|
|
@@ -1565,6 +1648,7 @@ function requireString(path4, value, errors) {
|
|
|
1565
1648
|
}
|
|
1566
1649
|
var ANDROID_PACKAGE_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
1567
1650
|
var ANDROID_ACTIVITY_RE = /^\.?[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
1651
|
+
var APP_SCHEME_RE = /^[a-z][a-z0-9+.-]*$/;
|
|
1568
1652
|
function requireAndroidPackage(path4, value, errors) {
|
|
1569
1653
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1570
1654
|
errors.push(`${path4}: required non-empty string`);
|
|
@@ -1581,6 +1665,17 @@ function requireAndroidActivity(path4, value, errors) {
|
|
|
1581
1665
|
if (!ANDROID_ACTIVITY_RE.test(value))
|
|
1582
1666
|
errors.push(`${path4}: expected an activity name (e.g. .MainActivity)`);
|
|
1583
1667
|
}
|
|
1668
|
+
function requireAppScheme(path4, value, errors) {
|
|
1669
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1670
|
+
errors.push(`${path4}: required non-empty string`);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (!APP_SCHEME_RE.test(value)) {
|
|
1674
|
+
errors.push(
|
|
1675
|
+
`${path4}: expected a valid URL scheme (lowercase letter, then lowercase letters, digits, +, ., or -)`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1584
1679
|
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_]*)*$/;
|
|
1585
1680
|
function requireInstrumentationTarget(path4, value, errors) {
|
|
1586
1681
|
if (typeof value !== "string" || value.trim() === "") {
|