@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/dist/index.d.mts CHANGED
@@ -90,6 +90,8 @@ interface AndroidConfig {
90
90
  packageName: string;
91
91
  /** Launch activity, e.g. `.MainActivity`. */
92
92
  activity: string;
93
+ /** App URL scheme used for Expo dev-client deep links, e.g. `myapp`. */
94
+ scheme?: string;
93
95
  /** Gradle tasks that build the app + androidTest APKs. */
94
96
  gradleTasks?: string[];
95
97
  /** Built app APK path. Defaults to the standard debug output path. */
@@ -194,6 +196,13 @@ type StepAction = {
194
196
  readonly command: CommandSpec;
195
197
  readonly max: number;
196
198
  };
199
+ /**
200
+ * Terminal failure substrings to watch for in the backing process's captured log. If one
201
+ * appears, the readiness wait aborts EARLY (the build/test failed and will never become
202
+ * ready) instead of burning the full timeout. Set by the planner on the companion-ready step
203
+ * (see COMPANION_FAILURE_MARKERS); the executor pairs them with the process's log path.
204
+ */
205
+ readonly failureMarkers?: readonly string[];
197
206
  };
198
207
  /** The lifecycle stage a step belongs to. A failure is attributed to its stage. */
199
208
  type Stage = 'config' | 'metro' | 'device' | 'build' | 'companion' | 'app-launch' | 'hermes-target' | 'playwright' | 'cleanup';
package/dist/index.d.ts CHANGED
@@ -90,6 +90,8 @@ interface AndroidConfig {
90
90
  packageName: string;
91
91
  /** Launch activity, e.g. `.MainActivity`. */
92
92
  activity: string;
93
+ /** App URL scheme used for Expo dev-client deep links, e.g. `myapp`. */
94
+ scheme?: string;
93
95
  /** Gradle tasks that build the app + androidTest APKs. */
94
96
  gradleTasks?: string[];
95
97
  /** Built app APK path. Defaults to the standard debug output path. */
@@ -194,6 +196,13 @@ type StepAction = {
194
196
  readonly command: CommandSpec;
195
197
  readonly max: number;
196
198
  };
199
+ /**
200
+ * Terminal failure substrings to watch for in the backing process's captured log. If one
201
+ * appears, the readiness wait aborts EARLY (the build/test failed and will never become
202
+ * ready) instead of burning the full timeout. Set by the planner on the companion-ready step
203
+ * (see COMPANION_FAILURE_MARKERS); the executor pairs them with the process's log path.
204
+ */
205
+ readonly failureMarkers?: readonly string[];
197
206
  };
198
207
  /** The lifecycle stage a step belongs to. A failure is attributed to its stage. */
199
208
  type Stage = 'config' | 'metro' | 'device' | 'build' | 'companion' | 'app-launch' | 'hermes-target' | 'playwright' | 'cleanup';
package/dist/index.js CHANGED
@@ -42,6 +42,10 @@ var DEFAULTS = {
42
42
  androidTokenFileName: "rn-driver-touch-token"
43
43
  };
44
44
  var SECRET_PLACEHOLDER = "<token-file>";
45
+ var COMPANION_FAILURE_MARKERS = {
46
+ ios: ["** BUILD FAILED **", "** TEST FAILED **"],
47
+ android: ["INSTRUMENTATION_FAILED", "Process crashed"]
48
+ };
45
49
 
46
50
  // src/plan/env.ts
47
51
  function buildIosDriverEnv(resolved, metro, timeoutMs) {
@@ -204,9 +208,10 @@ function planAndroid(input) {
204
208
  }
205
209
  }
206
210
  });
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));
211
+ const launch1Command = launchCommandFor(android, resolved, serial, { forceStopBefore: true });
212
+ const launch2Command = launchCommandFor(android, resolved, serial, { forceStopBefore: false });
213
+ push(launchStep("android.launch-1", android, launch1Command));
214
+ push(hermesStep("android.hermes-1", android, metro, resolved, hermesDeviceName, launch1Command));
210
215
  push({
211
216
  id: "android.forward-clean",
212
217
  stage: "companion",
@@ -260,11 +265,14 @@ function planAndroid(input) {
260
265
  port: resolved.touchPort,
261
266
  tokenFile: resolved.tokenFile,
262
267
  timeoutMs: resolved.companionReadyTimeoutMs
263
- }
268
+ },
269
+ // Abort early if `am instrument` reports the companion failed to start (crash / missing
270
+ // androidTest target) instead of waiting out the readiness budget.
271
+ failureMarkers: COMPANION_FAILURE_MARKERS.android
264
272
  }
265
273
  });
266
- push(launchStep("android.launch-2", android, serial));
267
- push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName, launchCommand));
274
+ push(launchStep("android.launch-2", android, launch2Command));
275
+ push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName, launch2Command));
268
276
  const cleanup = [
269
277
  {
270
278
  type: "kill-process",
@@ -319,22 +327,34 @@ function planAndroid(input) {
319
327
  playwright: playwrightCommand(playwright, specs, passthrough)
320
328
  };
321
329
  }
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
- ]);
330
+ function launchCommandFor(android, resolved, serial, opts) {
331
+ if (android.launch.kind === "plain") {
332
+ return adb(serial, [
333
+ "shell",
334
+ "am",
335
+ "start",
336
+ "-W",
337
+ "-n",
338
+ `${android.packageName}/${android.activity}`
339
+ ]);
340
+ }
341
+ if (!android.scheme) {
342
+ throw new Error('android.scheme is required when android.launch.kind is "expo-dev-client"');
343
+ }
344
+ const launchScript = `am start -a android.intent.action.VIEW -d ${shellSingleQuote(
345
+ devClientUrl(android.scheme, resolved.initialUrl)
346
+ )}`;
347
+ return adbShellScript(
348
+ serial,
349
+ opts.forceStopBefore ? `am force-stop ${android.packageName} && ${launchScript}` : launchScript
350
+ );
331
351
  }
332
- function launchStep(id, android, serial) {
352
+ function launchStep(id, android, command) {
333
353
  return {
334
354
  id,
335
355
  stage: "app-launch",
336
356
  description: `Launch ${android.packageName}/${android.activity}`,
337
- action: { type: "command", command: launchCommandFor(android, serial) }
357
+ action: { type: "command", command }
338
358
  };
339
359
  }
340
360
  function hermesStep(id, android, metro, resolved, deviceNameMatch, launchCommand) {
@@ -368,6 +388,12 @@ function debugHostXml(host) {
368
388
  function adb(serial, args) {
369
389
  return { command: "adb", args: ["-s", serial, ...args] };
370
390
  }
391
+ function devClientUrl(scheme, initialUrl) {
392
+ return `${scheme}://expo-development-client/?url=${initialUrl}`;
393
+ }
394
+ function shellSingleQuote(value) {
395
+ return `'${value.replaceAll("'", "'\\''")}'`;
396
+ }
371
397
  function adbShellScript(serial, remote) {
372
398
  return { command: "adb", args: ["-s", serial, "shell", remote] };
373
399
  }
@@ -413,8 +439,7 @@ function planIos(input) {
413
439
  // Pass the resolved UI-test scheme so a custom `ios.uitestScheme` scaffolds
414
440
  // the SAME target that companion startup later builds (default is
415
441
  // `${appScheme}UITests`).
416
- command: npx([
417
- "rn-driver-xctest-scaffold",
442
+ command: cmd("node_modules/.bin/rn-driver-xctest-scaffold", [
418
443
  "--ios-dir",
419
444
  "ios",
420
445
  "--project-name",
@@ -574,7 +599,10 @@ function planIos(input) {
574
599
  port: resolved.touchPort,
575
600
  tokenFile: resolved.tokenFile,
576
601
  timeoutMs: resolved.companionReadyTimeoutMs
577
- }
602
+ },
603
+ // Abort early if `xcodebuild test` reports a build/test failure (it lingers "alive" after, so
604
+ // the 300s readiness budget would otherwise be burnt waiting for a companion that cannot bind).
605
+ failureMarkers: COMPANION_FAILURE_MARKERS.ios
578
606
  }
579
607
  });
580
608
  if (isDevClient) {
@@ -708,7 +736,7 @@ function placeholderIos(ios, metro) {
708
736
  initialUrl: ios.launch.initialUrl ?? metro.url
709
737
  };
710
738
  }
711
- function placeholderAndroid(android, _metro) {
739
+ function placeholderAndroid(android, metro) {
712
740
  return {
713
741
  serial: "<android-serial>",
714
742
  touchPort: android.companion?.port ?? DEFAULTS.companionPort,
@@ -716,7 +744,8 @@ function placeholderAndroid(android, _metro) {
716
744
  hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
717
745
  tokenFile: SECRET_PLACEHOLDER,
718
746
  deviceTokenFileName: DEFAULTS.androidTokenFileName,
719
- instrumentationTarget: instrumentationTarget(android)
747
+ instrumentationTarget: instrumentationTarget(android),
748
+ initialUrl: android.launch.initialUrl ?? metro.url
720
749
  };
721
750
  }
722
751
 
@@ -743,7 +772,7 @@ function buildDryRunPlan(config, platform, opts = {}) {
743
772
  return planAndroid({
744
773
  android,
745
774
  metro,
746
- resolved: placeholderAndroid(android),
775
+ resolved: placeholderAndroid(android, metro),
747
776
  playwright: config.playwright,
748
777
  timeoutMs: config.timeoutMs,
749
778
  specs,
@@ -785,8 +814,10 @@ function renderAction(action) {
785
814
  return `write ${action.path}${action.mode ? ` (mode ${action.mode.toString(8)})` : ""}`;
786
815
  case "free-port":
787
816
  return `free-port ${action.port}`;
788
- case "probe":
789
- return `probe ${renderProbe(action.probe)}`;
817
+ case "probe": {
818
+ const fastFail = action.failureMarkers?.length ? ` [fast-fail on: ${action.failureMarkers.join(", ")}]` : "";
819
+ return `probe ${renderProbe(action.probe)}${fastFail}`;
820
+ }
790
821
  default: {
791
822
  const _exhaustive = action;
792
823
  throw new Error(`unhandled action: ${JSON.stringify(_exhaustive)}`);
@@ -852,6 +883,7 @@ var IOS_KEYS = /* @__PURE__ */ new Set([
852
883
  var ANDROID_KEYS = /* @__PURE__ */ new Set([
853
884
  "packageName",
854
885
  "activity",
886
+ "scheme",
855
887
  "gradleTasks",
856
888
  "appApkPath",
857
889
  "testApkPath",
@@ -953,6 +985,8 @@ function validateAndroid(android, errors) {
953
985
  reportUnknownKeys("config.android", android, ANDROID_KEYS, errors);
954
986
  requireAndroidPackage("config.android.packageName", android.packageName, errors);
955
987
  requireAndroidActivity("config.android.activity", android.activity, errors);
988
+ if (android.scheme !== void 0)
989
+ requireAppScheme("config.android.scheme", android.scheme, errors);
956
990
  optionalString("config.android.appApkPath", android.appApkPath, errors);
957
991
  optionalString("config.android.testApkPath", android.testApkPath, errors);
958
992
  if (android.instrumentationTarget !== void 0)
@@ -965,7 +999,10 @@ function validateAndroid(android, errors) {
965
999
  errors.push("config.android.gradleTasks: expected an array of strings");
966
1000
  }
967
1001
  validateCompanion("config.android.companion", android.companion, errors);
968
- validateLaunch("config.android.launch", android.launch, errors);
1002
+ const launch = validateLaunch("config.android.launch", android.launch, errors);
1003
+ if (launch?.kind === "expo-dev-client" && android.scheme === void 0) {
1004
+ errors.push('config.android.scheme: required when android.launch.kind is "expo-dev-client"');
1005
+ }
969
1006
  }
970
1007
  function validateCompanion(path, companion, errors) {
971
1008
  if (companion === void 0) return;
@@ -1011,6 +1048,7 @@ function requireString(path, value, errors) {
1011
1048
  }
1012
1049
  var ANDROID_PACKAGE_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
1013
1050
  var ANDROID_ACTIVITY_RE = /^\.?[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
1051
+ var APP_SCHEME_RE = /^[a-z][a-z0-9+.-]*$/;
1014
1052
  function requireAndroidPackage(path, value, errors) {
1015
1053
  if (typeof value !== "string" || value.trim() === "") {
1016
1054
  errors.push(`${path}: required non-empty string`);
@@ -1027,6 +1065,17 @@ function requireAndroidActivity(path, value, errors) {
1027
1065
  if (!ANDROID_ACTIVITY_RE.test(value))
1028
1066
  errors.push(`${path}: expected an activity name (e.g. .MainActivity)`);
1029
1067
  }
1068
+ function requireAppScheme(path, value, errors) {
1069
+ if (typeof value !== "string" || value.trim() === "") {
1070
+ errors.push(`${path}: required non-empty string`);
1071
+ return;
1072
+ }
1073
+ if (!APP_SCHEME_RE.test(value)) {
1074
+ errors.push(
1075
+ `${path}: expected a valid URL scheme (lowercase letter, then lowercase letters, digits, +, ., or -)`
1076
+ );
1077
+ }
1078
+ }
1030
1079
  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
1080
  function requireInstrumentationTarget(path, value, errors) {
1032
1081
  if (typeof value !== "string" || value.trim() === "") {