@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @unrulysystems/rn-playwright-driver-runner
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#30](https://github.com/unrulysystems/rn-playwright-driver/pull/30) [`4bdbe20`](https://github.com/unrulysystems/rn-playwright-driver/commit/4bdbe2073f7b613f6bfb8d5a13db5e0ca000e1c5) Thanks [@alleneubank](https://github.com/alleneubank)! - Upgrade the dogfood example and native module tooling to Expo SDK 56.
|
|
8
|
+
|
|
9
|
+
The driver now sends a React Native inspector-compatible WebSocket Origin when
|
|
10
|
+
attaching to Hermes CDP, which keeps Expo SDK 56 dev-client debugging connected
|
|
11
|
+
on localhost Metro servers. The runner also fast-fails companion readiness when
|
|
12
|
+
captured iOS or Android companion logs contain terminal build, test, or
|
|
13
|
+
instrumentation failure markers instead of waiting for the full probe timeout.
|
|
14
|
+
|
|
15
|
+
## 0.1.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#27](https://github.com/unrulysystems/rn-playwright-driver/pull/27) [`b8eb24e`](https://github.com/unrulysystems/rn-playwright-driver/commit/b8eb24e9daef7f00605fe06dcf520089f62f58be) Thanks [@alleneubank](https://github.com/alleneubank)! - Harden the runner and driver fixtures for dev-client dogfooding.
|
|
20
|
+
|
|
21
|
+
The runner now ships a Node-compatible `rn-driver` bin, supports Android
|
|
22
|
+
Expo dev-client deep-link launch, and documents runner-owned lifecycle
|
|
23
|
+
boundaries. The driver Playwright fixture resolves `@playwright/test` from the
|
|
24
|
+
consumer project so npm and Yarn installs use the app's Playwright instance.
|
|
25
|
+
|
|
3
26
|
## 0.1.0
|
|
4
27
|
|
|
5
28
|
### Initial Release
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ this package; your Playwright specs import the driver directly.)
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
|
|
32
|
+
npm install --save-dev @unrulysystems/rn-playwright-driver-runner
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
This package orchestrates the platform companions; install and configure them per
|
|
@@ -68,6 +68,8 @@ export default defineRnDriverConfig({
|
|
|
68
68
|
android: {
|
|
69
69
|
packageName: 'com.company.app',
|
|
70
70
|
activity: '.MainActivity',
|
|
71
|
+
// Required only for expo-dev-client launch:
|
|
72
|
+
// scheme: 'companyapp',
|
|
71
73
|
launch: { mode: 'launch', kind: 'plain' },
|
|
72
74
|
},
|
|
73
75
|
playwright: {
|
|
@@ -77,7 +79,34 @@ export default defineRnDriverConfig({
|
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
A plain (non dev-client) Expo app uses `launch: { mode: 'launch', kind: 'plain' }`
|
|
80
|
-
on
|
|
82
|
+
on both platforms. The companion launches the app itself on iOS; Android launches
|
|
83
|
+
the configured `packageName`/`activity` directly.
|
|
84
|
+
|
|
85
|
+
For Expo dev-client, the host owns native app launch so the app starts on the
|
|
86
|
+
test Metro instead of stopping at the launcher UI:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export default defineRnDriverConfig({
|
|
90
|
+
metro: { command: 'npx expo start --localhost --port 8081' },
|
|
91
|
+
ios: {
|
|
92
|
+
bundleId: 'com.company.app',
|
|
93
|
+
workspace: 'ios/App.xcworkspace',
|
|
94
|
+
appScheme: 'App',
|
|
95
|
+
launch: { mode: 'attach', kind: 'expo-dev-client' },
|
|
96
|
+
},
|
|
97
|
+
android: {
|
|
98
|
+
packageName: 'com.company.app',
|
|
99
|
+
activity: '.MainActivity',
|
|
100
|
+
scheme: 'companyapp',
|
|
101
|
+
launch: { mode: 'launch', kind: 'expo-dev-client' },
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`launch.initialUrl` defaults to the resolved Metro URL on iOS and Android. iOS
|
|
107
|
+
uses `simctl launch --initialUrl`; Android uses the configured
|
|
108
|
+
`android.scheme` to open
|
|
109
|
+
`<scheme>://expo-development-client/?url=<resolved-metro-url>`.
|
|
81
110
|
|
|
82
111
|
## Run
|
|
83
112
|
|
|
@@ -128,13 +157,57 @@ Token material always travels by `0600` file path (the driver's
|
|
|
128
157
|
`--dry-run` output. Cleanup is defensive and idempotent — a crashed prior run
|
|
129
158
|
never wedges the next (stale companion ports are freed at startup and teardown).
|
|
130
159
|
|
|
160
|
+
### Playwright lifecycle boundary
|
|
161
|
+
|
|
162
|
+
Invoke specs through `rn-driver test`, not a standalone `playwright test`
|
|
163
|
+
command. The runner starts or reuses Metro, builds and launches the native app,
|
|
164
|
+
starts the companion, waits for Hermes, sets the driver env contract, runs
|
|
165
|
+
Playwright, and cleans up the state it owns.
|
|
166
|
+
|
|
167
|
+
Runner-managed Playwright configs should not define app-level `globalSetup` or
|
|
168
|
+
`globalTeardown` that starts/stops Metro, launches the native app, starts/stops a
|
|
169
|
+
touch companion, or removes runner companion state. Keep those hooks for
|
|
170
|
+
test-local concerns only. Specs should consume the environment that
|
|
171
|
+
`rn-driver test` provides.
|
|
172
|
+
|
|
173
|
+
### Runner env contract
|
|
174
|
+
|
|
175
|
+
These names are the runner-owned surface between native lifecycle setup and
|
|
176
|
+
Playwright/driver execution. Token values are never exposed; only file paths are.
|
|
177
|
+
|
|
178
|
+
| Variable | Scope | Meaning |
|
|
179
|
+
| ---------------------------------------------------------------------- | ------- | -------------------------------------------------- |
|
|
180
|
+
| `RN_METRO_URL` | both | Metro URL the app and driver should use |
|
|
181
|
+
| `RN_DEVICE_NAME` | both | Hermes target device-name pin |
|
|
182
|
+
| `RN_TIMEOUT` | both | Driver request timeout |
|
|
183
|
+
| `RN_TOUCH_BACKEND` | both | `xctest` on iOS, `instrumentation` on Android |
|
|
184
|
+
| `RN_TOUCH_XCTEST_PORT`, `RN_TOUCH_XCTEST_TOKEN_FILE` | iOS | XCTest companion port and token-file path |
|
|
185
|
+
| `RN_TOUCH_INSTRUMENTATION_PORT`, `RN_TOUCH_INSTRUMENTATION_TOKEN_FILE` | Android | Instrumentation companion port and token-file path |
|
|
186
|
+
| `ANDROID_SERIAL` | Android | adb device pin for the selected emulator/device |
|
|
187
|
+
|
|
188
|
+
The runner also owns internal token/config file paths used to start companions
|
|
189
|
+
and configure native test targets. Do not pass token values through argv, inline
|
|
190
|
+
env, or docs.
|
|
191
|
+
|
|
192
|
+
### Prebuild environment and priming
|
|
193
|
+
|
|
194
|
+
`expo prebuild` runs inside the runner process, so it inherits the runner
|
|
195
|
+
process environment. The intended stable marker for test-only Expo config,
|
|
196
|
+
plugins, or native settings is `RN_E2E=1`; this package does not emit that marker
|
|
197
|
+
yet, so treat it as the planned contract rather than current behavior.
|
|
198
|
+
|
|
199
|
+
The implemented lifecycle already covers prebuild, Metro, app launch, companion
|
|
200
|
+
startup, Hermes waits, Playwright env, and cleanup. Priming controls such as
|
|
201
|
+
`RN_E2E_PRIMED=1` or a `prebuild.clean` option are future design space and are
|
|
202
|
+
not available runner flags today.
|
|
203
|
+
|
|
131
204
|
## Requirements
|
|
132
205
|
|
|
133
|
-
- The `rn-driver` CLI runs under **
|
|
134
|
-
|
|
135
|
-
|
|
206
|
+
- The `rn-driver` CLI runs under **Node >= 22**. The published `bin` is a thin
|
|
207
|
+
Node shim that loads built JavaScript from `dist`, so npm/Yarn consumers do not
|
|
208
|
+
need bun or nub on `PATH`.
|
|
136
209
|
- The readiness probes use the global `WebSocket`/`fetch`, so embedding the
|
|
137
|
-
library API (the `.` export) standalone requires **
|
|
210
|
+
library API (the `.` export) standalone requires **Node >= 22**.
|
|
138
211
|
- The platform companion packages installed and their Expo config plugins added.
|
|
139
212
|
- `xcrun`/`xcodebuild`/`pod` (iOS) and `adb`/`gradle` (Android) on `PATH`.
|
|
140
213
|
|
package/dist/cli.js
CHANGED
|
@@ -54,6 +54,10 @@ var DEFAULTS = {
|
|
|
54
54
|
androidTokenFileName: "rn-driver-touch-token"
|
|
55
55
|
};
|
|
56
56
|
var SECRET_PLACEHOLDER = "<token-file>";
|
|
57
|
+
var COMPANION_FAILURE_MARKERS = {
|
|
58
|
+
ios: ["** BUILD FAILED **", "** TEST FAILED **"],
|
|
59
|
+
android: ["INSTRUMENTATION_FAILED", "Process crashed"]
|
|
60
|
+
};
|
|
57
61
|
|
|
58
62
|
// src/plan/env.ts
|
|
59
63
|
function buildIosDriverEnv(resolved, metro, timeoutMs) {
|
|
@@ -216,9 +220,10 @@ function planAndroid(input) {
|
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
});
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
push(
|
|
223
|
+
const launch1Command = launchCommandFor(android, resolved, serial, { forceStopBefore: true });
|
|
224
|
+
const launch2Command = launchCommandFor(android, resolved, serial, { forceStopBefore: false });
|
|
225
|
+
push(launchStep("android.launch-1", android, launch1Command));
|
|
226
|
+
push(hermesStep("android.hermes-1", android, metro, resolved, hermesDeviceName, launch1Command));
|
|
222
227
|
push({
|
|
223
228
|
id: "android.forward-clean",
|
|
224
229
|
stage: "companion",
|
|
@@ -272,11 +277,14 @@ function planAndroid(input) {
|
|
|
272
277
|
port: resolved.touchPort,
|
|
273
278
|
tokenFile: resolved.tokenFile,
|
|
274
279
|
timeoutMs: resolved.companionReadyTimeoutMs
|
|
275
|
-
}
|
|
280
|
+
},
|
|
281
|
+
// Abort early if `am instrument` reports the companion failed to start (crash / missing
|
|
282
|
+
// androidTest target) instead of waiting out the readiness budget.
|
|
283
|
+
failureMarkers: COMPANION_FAILURE_MARKERS.android
|
|
276
284
|
}
|
|
277
285
|
});
|
|
278
|
-
push(launchStep("android.launch-2", android,
|
|
279
|
-
push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName,
|
|
286
|
+
push(launchStep("android.launch-2", android, launch2Command));
|
|
287
|
+
push(hermesStep("android.hermes-2", android, metro, resolved, hermesDeviceName, launch2Command));
|
|
280
288
|
const cleanup = [
|
|
281
289
|
{
|
|
282
290
|
type: "kill-process",
|
|
@@ -331,22 +339,34 @@ function planAndroid(input) {
|
|
|
331
339
|
playwright: playwrightCommand(playwright, specs, passthrough)
|
|
332
340
|
};
|
|
333
341
|
}
|
|
334
|
-
function launchCommandFor(android, serial) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
function launchCommandFor(android, resolved, serial, opts) {
|
|
343
|
+
if (android.launch.kind === "plain") {
|
|
344
|
+
return adb(serial, [
|
|
345
|
+
"shell",
|
|
346
|
+
"am",
|
|
347
|
+
"start",
|
|
348
|
+
"-W",
|
|
349
|
+
"-n",
|
|
350
|
+
`${android.packageName}/${android.activity}`
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
if (!android.scheme) {
|
|
354
|
+
throw new Error('android.scheme is required when android.launch.kind is "expo-dev-client"');
|
|
355
|
+
}
|
|
356
|
+
const launchScript = `am start -a android.intent.action.VIEW -d ${shellSingleQuote(
|
|
357
|
+
devClientUrl(android.scheme, resolved.initialUrl)
|
|
358
|
+
)}`;
|
|
359
|
+
return adbShellScript(
|
|
360
|
+
serial,
|
|
361
|
+
opts.forceStopBefore ? `am force-stop ${android.packageName} && ${launchScript}` : launchScript
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
function launchStep(id, android, command) {
|
|
345
365
|
return {
|
|
346
366
|
id,
|
|
347
367
|
stage: "app-launch",
|
|
348
368
|
description: `Launch ${android.packageName}/${android.activity}`,
|
|
349
|
-
action: { type: "command", command
|
|
369
|
+
action: { type: "command", command }
|
|
350
370
|
};
|
|
351
371
|
}
|
|
352
372
|
function hermesStep(id, android, metro, resolved, deviceNameMatch, launchCommand) {
|
|
@@ -380,6 +400,12 @@ function debugHostXml(host) {
|
|
|
380
400
|
function adb(serial, args) {
|
|
381
401
|
return { command: "adb", args: ["-s", serial, ...args] };
|
|
382
402
|
}
|
|
403
|
+
function devClientUrl(scheme, initialUrl) {
|
|
404
|
+
return `${scheme}://expo-development-client/?url=${initialUrl}`;
|
|
405
|
+
}
|
|
406
|
+
function shellSingleQuote(value) {
|
|
407
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
408
|
+
}
|
|
383
409
|
function adbShellScript(serial, remote) {
|
|
384
410
|
return { command: "adb", args: ["-s", serial, "shell", remote] };
|
|
385
411
|
}
|
|
@@ -425,8 +451,7 @@ function planIos(input) {
|
|
|
425
451
|
// Pass the resolved UI-test scheme so a custom `ios.uitestScheme` scaffolds
|
|
426
452
|
// the SAME target that companion startup later builds (default is
|
|
427
453
|
// `${appScheme}UITests`).
|
|
428
|
-
command:
|
|
429
|
-
"rn-driver-xctest-scaffold",
|
|
454
|
+
command: cmd("node_modules/.bin/rn-driver-xctest-scaffold", [
|
|
430
455
|
"--ios-dir",
|
|
431
456
|
"ios",
|
|
432
457
|
"--project-name",
|
|
@@ -586,7 +611,10 @@ function planIos(input) {
|
|
|
586
611
|
port: resolved.touchPort,
|
|
587
612
|
tokenFile: resolved.tokenFile,
|
|
588
613
|
timeoutMs: resolved.companionReadyTimeoutMs
|
|
589
|
-
}
|
|
614
|
+
},
|
|
615
|
+
// Abort early if `xcodebuild test` reports a build/test failure (it lingers "alive" after, so
|
|
616
|
+
// the 300s readiness budget would otherwise be burnt waiting for a companion that cannot bind).
|
|
617
|
+
failureMarkers: COMPANION_FAILURE_MARKERS.ios
|
|
590
618
|
}
|
|
591
619
|
});
|
|
592
620
|
if (isDevClient) {
|
|
@@ -720,7 +748,7 @@ function placeholderIos(ios, metro) {
|
|
|
720
748
|
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
721
749
|
};
|
|
722
750
|
}
|
|
723
|
-
function placeholderAndroid(android,
|
|
751
|
+
function placeholderAndroid(android, metro) {
|
|
724
752
|
return {
|
|
725
753
|
serial: "<android-serial>",
|
|
726
754
|
touchPort: android.companion?.port ?? DEFAULTS.companionPort,
|
|
@@ -728,7 +756,8 @@ function placeholderAndroid(android, _metro) {
|
|
|
728
756
|
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
729
757
|
tokenFile: SECRET_PLACEHOLDER,
|
|
730
758
|
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
731
|
-
instrumentationTarget: instrumentationTarget(android)
|
|
759
|
+
instrumentationTarget: instrumentationTarget(android),
|
|
760
|
+
initialUrl: android.launch.initialUrl ?? metro.url
|
|
732
761
|
};
|
|
733
762
|
}
|
|
734
763
|
|
|
@@ -755,7 +784,7 @@ function buildDryRunPlan(config, platform, opts = {}) {
|
|
|
755
784
|
return planAndroid({
|
|
756
785
|
android,
|
|
757
786
|
metro,
|
|
758
|
-
resolved: placeholderAndroid(android),
|
|
787
|
+
resolved: placeholderAndroid(android, metro),
|
|
759
788
|
playwright: config.playwright,
|
|
760
789
|
timeoutMs: config.timeoutMs,
|
|
761
790
|
specs,
|
|
@@ -841,8 +870,10 @@ function renderAction(action) {
|
|
|
841
870
|
return `write ${action.path}${action.mode ? ` (mode ${action.mode.toString(8)})` : ""}`;
|
|
842
871
|
case "free-port":
|
|
843
872
|
return `free-port ${action.port}`;
|
|
844
|
-
case "probe":
|
|
845
|
-
|
|
873
|
+
case "probe": {
|
|
874
|
+
const fastFail = action.failureMarkers?.length ? ` [fast-fail on: ${action.failureMarkers.join(", ")}]` : "";
|
|
875
|
+
return `probe ${renderProbe(action.probe)}${fastFail}`;
|
|
876
|
+
}
|
|
846
877
|
default: {
|
|
847
878
|
const _exhaustive = action;
|
|
848
879
|
throw new Error(`unhandled action: ${JSON.stringify(_exhaustive)}`);
|
|
@@ -888,6 +919,25 @@ function renderProbe(probe) {
|
|
|
888
919
|
}
|
|
889
920
|
}
|
|
890
921
|
|
|
922
|
+
// src/runner/probe-failure.ts
|
|
923
|
+
var ProbeFailure = class extends Error {
|
|
924
|
+
marker;
|
|
925
|
+
constructor(marker, detail) {
|
|
926
|
+
super(
|
|
927
|
+
`companion process reported a terminal failure ("${marker}") \u2014 aborting the readiness wait. The build/test failed; it will not become ready.${detail ? `
|
|
928
|
+
${detail}` : ""}`
|
|
929
|
+
);
|
|
930
|
+
this.name = "ProbeFailure";
|
|
931
|
+
this.marker = marker;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
function findFailureMarker(log, markers) {
|
|
935
|
+
for (const marker of markers) {
|
|
936
|
+
if (log.includes(marker)) return marker;
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
|
|
891
941
|
// src/runner/execute.ts
|
|
892
942
|
var StageError = class extends Error {
|
|
893
943
|
stage;
|
|
@@ -955,22 +1005,30 @@ async function runStep(step, runner, opts, processes, isAlive) {
|
|
|
955
1005
|
case "probe": {
|
|
956
1006
|
const key = processKeyForProbe(action.probe);
|
|
957
1007
|
const aliveFn = key === null ? () => true : () => isAlive(key);
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
remaining
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1008
|
+
const watch = key !== null && action.failureMarkers && action.failureMarkers.length > 0 ? { logPath: logPathFor(opts.logDir, key), failureMarkers: action.failureMarkers } : void 0;
|
|
1009
|
+
try {
|
|
1010
|
+
let ready = await runner.probe(action.probe, aliveFn, watch);
|
|
1011
|
+
let remaining = action.retry?.max ?? 0;
|
|
1012
|
+
while (!ready && remaining > 0) {
|
|
1013
|
+
remaining -= 1;
|
|
1014
|
+
if (action.retry) {
|
|
1015
|
+
runner.log(`retry ${step.id}: re-running launch (${remaining} attempt(s) left)`);
|
|
1016
|
+
await runner.exec(action.retry.command);
|
|
1017
|
+
}
|
|
1018
|
+
ready = await runner.probe(action.probe, aliveFn, watch);
|
|
965
1019
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1020
|
+
if (!ready) {
|
|
1021
|
+
throw new StageError(
|
|
1022
|
+
step.stage,
|
|
1023
|
+
step.id,
|
|
1024
|
+
`readiness timed out after ${action.probe.timeoutMs}ms`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
if (error instanceof ProbeFailure) {
|
|
1029
|
+
throw new StageError(step.stage, step.id, error.message);
|
|
1030
|
+
}
|
|
1031
|
+
throw error;
|
|
974
1032
|
}
|
|
975
1033
|
return;
|
|
976
1034
|
}
|
|
@@ -1109,10 +1167,15 @@ var NodeProcessRunner = class {
|
|
|
1109
1167
|
}
|
|
1110
1168
|
if (pids.length > 0) await delay(1e3);
|
|
1111
1169
|
}
|
|
1112
|
-
probe(probe, isAlive) {
|
|
1170
|
+
probe(probe, isAlive, watch) {
|
|
1113
1171
|
const deadline = Date.now() + probe.timeoutMs;
|
|
1114
1172
|
const attempt = async () => {
|
|
1115
1173
|
for (; ; ) {
|
|
1174
|
+
if (watch) {
|
|
1175
|
+
const log = await readWatchedLog(watch.logPath);
|
|
1176
|
+
const marker = findFailureMarker(log, watch.failureMarkers);
|
|
1177
|
+
if (marker) throw new ProbeFailure(marker, lastLines(log));
|
|
1178
|
+
}
|
|
1116
1179
|
if (!isAlive()) return false;
|
|
1117
1180
|
if (await probeOnce(probe)) return true;
|
|
1118
1181
|
if (Date.now() >= deadline) return false;
|
|
@@ -1157,6 +1220,19 @@ function delay(ms) {
|
|
|
1157
1220
|
setTimeout(resolve, ms);
|
|
1158
1221
|
});
|
|
1159
1222
|
}
|
|
1223
|
+
async function readWatchedLog(path4) {
|
|
1224
|
+
try {
|
|
1225
|
+
return await promises.readFile(path4, "utf8");
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
`cannot read companion log for fast-fail marker detection (${path4}): ${String(error)}`,
|
|
1229
|
+
{ cause: error }
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function lastLines(text, n) {
|
|
1234
|
+
return text.split("\n").filter((line) => line.trim().length > 0).slice(-12).join("\n");
|
|
1235
|
+
}
|
|
1160
1236
|
async function probeOnce(probe) {
|
|
1161
1237
|
switch (probe.kind) {
|
|
1162
1238
|
case "metro-status":
|
|
@@ -1295,7 +1371,7 @@ async function resolveIosTarget(ios, metro, opts) {
|
|
|
1295
1371
|
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
1296
1372
|
};
|
|
1297
1373
|
}
|
|
1298
|
-
async function resolveAndroidTarget(android,
|
|
1374
|
+
async function resolveAndroidTarget(android, metro, opts) {
|
|
1299
1375
|
const serial = await selectSerial(opts.device);
|
|
1300
1376
|
await requireBooted(serial);
|
|
1301
1377
|
const deviceName = (await capture("adb", ["-s", serial, "shell", "getprop", "ro.product.model"])).trim();
|
|
@@ -1308,7 +1384,8 @@ async function resolveAndroidTarget(android, _metro, opts) {
|
|
|
1308
1384
|
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
1309
1385
|
tokenFile,
|
|
1310
1386
|
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
1311
|
-
instrumentationTarget: instrumentationTarget(android)
|
|
1387
|
+
instrumentationTarget: instrumentationTarget(android),
|
|
1388
|
+
initialUrl: android.launch.initialUrl ?? metro.url
|
|
1312
1389
|
},
|
|
1313
1390
|
deviceName
|
|
1314
1391
|
};
|
|
@@ -1413,6 +1490,7 @@ var IOS_KEYS = /* @__PURE__ */ new Set([
|
|
|
1413
1490
|
var ANDROID_KEYS = /* @__PURE__ */ new Set([
|
|
1414
1491
|
"packageName",
|
|
1415
1492
|
"activity",
|
|
1493
|
+
"scheme",
|
|
1416
1494
|
"gradleTasks",
|
|
1417
1495
|
"appApkPath",
|
|
1418
1496
|
"testApkPath",
|
|
@@ -1514,6 +1592,8 @@ function validateAndroid(android, errors) {
|
|
|
1514
1592
|
reportUnknownKeys("config.android", android, ANDROID_KEYS, errors);
|
|
1515
1593
|
requireAndroidPackage("config.android.packageName", android.packageName, errors);
|
|
1516
1594
|
requireAndroidActivity("config.android.activity", android.activity, errors);
|
|
1595
|
+
if (android.scheme !== void 0)
|
|
1596
|
+
requireAppScheme("config.android.scheme", android.scheme, errors);
|
|
1517
1597
|
optionalString("config.android.appApkPath", android.appApkPath, errors);
|
|
1518
1598
|
optionalString("config.android.testApkPath", android.testApkPath, errors);
|
|
1519
1599
|
if (android.instrumentationTarget !== void 0)
|
|
@@ -1526,7 +1606,10 @@ function validateAndroid(android, errors) {
|
|
|
1526
1606
|
errors.push("config.android.gradleTasks: expected an array of strings");
|
|
1527
1607
|
}
|
|
1528
1608
|
validateCompanion("config.android.companion", android.companion, errors);
|
|
1529
|
-
validateLaunch("config.android.launch", android.launch, errors);
|
|
1609
|
+
const launch = validateLaunch("config.android.launch", android.launch, errors);
|
|
1610
|
+
if (launch?.kind === "expo-dev-client" && android.scheme === void 0) {
|
|
1611
|
+
errors.push('config.android.scheme: required when android.launch.kind is "expo-dev-client"');
|
|
1612
|
+
}
|
|
1530
1613
|
}
|
|
1531
1614
|
function validateCompanion(path4, companion, errors) {
|
|
1532
1615
|
if (companion === void 0) return;
|
|
@@ -1572,6 +1655,7 @@ function requireString(path4, value, errors) {
|
|
|
1572
1655
|
}
|
|
1573
1656
|
var ANDROID_PACKAGE_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
1574
1657
|
var ANDROID_ACTIVITY_RE = /^\.?[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
1658
|
+
var APP_SCHEME_RE = /^[a-z][a-z0-9+.-]*$/;
|
|
1575
1659
|
function requireAndroidPackage(path4, value, errors) {
|
|
1576
1660
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1577
1661
|
errors.push(`${path4}: required non-empty string`);
|
|
@@ -1588,6 +1672,17 @@ function requireAndroidActivity(path4, value, errors) {
|
|
|
1588
1672
|
if (!ANDROID_ACTIVITY_RE.test(value))
|
|
1589
1673
|
errors.push(`${path4}: expected an activity name (e.g. .MainActivity)`);
|
|
1590
1674
|
}
|
|
1675
|
+
function requireAppScheme(path4, value, errors) {
|
|
1676
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1677
|
+
errors.push(`${path4}: required non-empty string`);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (!APP_SCHEME_RE.test(value)) {
|
|
1681
|
+
errors.push(
|
|
1682
|
+
`${path4}: expected a valid URL scheme (lowercase letter, then lowercase letters, digits, +, ., or -)`
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1591
1686
|
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_]*)*$/;
|
|
1592
1687
|
function requireInstrumentationTarget(path4, value, errors) {
|
|
1593
1688
|
if (typeof value !== "string" || value.trim() === "") {
|