@unrulysystems/rn-playwright-driver-runner 0.1.0 → 0.1.1
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 +11 -0
- package/README.md +79 -6
- package/bin/rn-driver.mjs +9 -0
- package/dist/cli.js +63 -25
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +63 -25
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +59 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +59 -22
- 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,16 @@
|
|
|
1
1
|
# @unrulysystems/rn-playwright-driver-runner
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
The runner now ships a Node-compatible `rn-driver` bin, supports Android
|
|
10
|
+
Expo dev-client deep-link launch, and documents runner-owned lifecycle
|
|
11
|
+
boundaries. The driver Playwright fixture resolves `@playwright/test` from the
|
|
12
|
+
consumer project so npm and Yarn installs use the app's Playwright instance.
|
|
13
|
+
|
|
3
14
|
## 0.1.0
|
|
4
15
|
|
|
5
16
|
### 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
|
@@ -216,9 +216,10 @@ function planAndroid(input) {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
});
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
push(
|
|
219
|
+
const launch1Command = launchCommandFor(android, resolved, serial, { forceStopBefore: true });
|
|
220
|
+
const launch2Command = launchCommandFor(android, resolved, serial, { forceStopBefore: false });
|
|
221
|
+
push(launchStep("android.launch-1", android, launch1Command));
|
|
222
|
+
push(hermesStep("android.hermes-1", android, metro, resolved, hermesDeviceName, launch1Command));
|
|
222
223
|
push({
|
|
223
224
|
id: "android.forward-clean",
|
|
224
225
|
stage: "companion",
|
|
@@ -275,8 +276,8 @@ function planAndroid(input) {
|
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
});
|
|
278
|
-
push(launchStep("android.launch-2", android,
|
|
279
|
-
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));
|
|
280
281
|
const cleanup = [
|
|
281
282
|
{
|
|
282
283
|
type: "kill-process",
|
|
@@ -331,22 +332,34 @@ function planAndroid(input) {
|
|
|
331
332
|
playwright: playwrightCommand(playwright, specs, passthrough)
|
|
332
333
|
};
|
|
333
334
|
}
|
|
334
|
-
function launchCommandFor(android, serial) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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) {
|
|
345
358
|
return {
|
|
346
359
|
id,
|
|
347
360
|
stage: "app-launch",
|
|
348
361
|
description: `Launch ${android.packageName}/${android.activity}`,
|
|
349
|
-
action: { type: "command", command
|
|
362
|
+
action: { type: "command", command }
|
|
350
363
|
};
|
|
351
364
|
}
|
|
352
365
|
function hermesStep(id, android, metro, resolved, deviceNameMatch, launchCommand) {
|
|
@@ -380,6 +393,12 @@ function debugHostXml(host) {
|
|
|
380
393
|
function adb(serial, args) {
|
|
381
394
|
return { command: "adb", args: ["-s", serial, ...args] };
|
|
382
395
|
}
|
|
396
|
+
function devClientUrl(scheme, initialUrl) {
|
|
397
|
+
return `${scheme}://expo-development-client/?url=${initialUrl}`;
|
|
398
|
+
}
|
|
399
|
+
function shellSingleQuote(value) {
|
|
400
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
401
|
+
}
|
|
383
402
|
function adbShellScript(serial, remote) {
|
|
384
403
|
return { command: "adb", args: ["-s", serial, "shell", remote] };
|
|
385
404
|
}
|
|
@@ -425,8 +444,7 @@ function planIos(input) {
|
|
|
425
444
|
// Pass the resolved UI-test scheme so a custom `ios.uitestScheme` scaffolds
|
|
426
445
|
// the SAME target that companion startup later builds (default is
|
|
427
446
|
// `${appScheme}UITests`).
|
|
428
|
-
command:
|
|
429
|
-
"rn-driver-xctest-scaffold",
|
|
447
|
+
command: cmd("node_modules/.bin/rn-driver-xctest-scaffold", [
|
|
430
448
|
"--ios-dir",
|
|
431
449
|
"ios",
|
|
432
450
|
"--project-name",
|
|
@@ -720,7 +738,7 @@ function placeholderIos(ios, metro) {
|
|
|
720
738
|
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
721
739
|
};
|
|
722
740
|
}
|
|
723
|
-
function placeholderAndroid(android,
|
|
741
|
+
function placeholderAndroid(android, metro) {
|
|
724
742
|
return {
|
|
725
743
|
serial: "<android-serial>",
|
|
726
744
|
touchPort: android.companion?.port ?? DEFAULTS.companionPort,
|
|
@@ -728,7 +746,8 @@ function placeholderAndroid(android, _metro) {
|
|
|
728
746
|
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
729
747
|
tokenFile: SECRET_PLACEHOLDER,
|
|
730
748
|
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
731
|
-
instrumentationTarget: instrumentationTarget(android)
|
|
749
|
+
instrumentationTarget: instrumentationTarget(android),
|
|
750
|
+
initialUrl: android.launch.initialUrl ?? metro.url
|
|
732
751
|
};
|
|
733
752
|
}
|
|
734
753
|
|
|
@@ -755,7 +774,7 @@ function buildDryRunPlan(config, platform, opts = {}) {
|
|
|
755
774
|
return planAndroid({
|
|
756
775
|
android,
|
|
757
776
|
metro,
|
|
758
|
-
resolved: placeholderAndroid(android),
|
|
777
|
+
resolved: placeholderAndroid(android, metro),
|
|
759
778
|
playwright: config.playwright,
|
|
760
779
|
timeoutMs: config.timeoutMs,
|
|
761
780
|
specs,
|
|
@@ -1295,7 +1314,7 @@ async function resolveIosTarget(ios, metro, opts) {
|
|
|
1295
1314
|
initialUrl: ios.launch.initialUrl ?? metro.url
|
|
1296
1315
|
};
|
|
1297
1316
|
}
|
|
1298
|
-
async function resolveAndroidTarget(android,
|
|
1317
|
+
async function resolveAndroidTarget(android, metro, opts) {
|
|
1299
1318
|
const serial = await selectSerial(opts.device);
|
|
1300
1319
|
await requireBooted(serial);
|
|
1301
1320
|
const deviceName = (await capture("adb", ["-s", serial, "shell", "getprop", "ro.product.model"])).trim();
|
|
@@ -1308,7 +1327,8 @@ async function resolveAndroidTarget(android, _metro, opts) {
|
|
|
1308
1327
|
hermesTimeoutMs: DEFAULTS.hermesTargetTimeoutMs,
|
|
1309
1328
|
tokenFile,
|
|
1310
1329
|
deviceTokenFileName: DEFAULTS.androidTokenFileName,
|
|
1311
|
-
instrumentationTarget: instrumentationTarget(android)
|
|
1330
|
+
instrumentationTarget: instrumentationTarget(android),
|
|
1331
|
+
initialUrl: android.launch.initialUrl ?? metro.url
|
|
1312
1332
|
},
|
|
1313
1333
|
deviceName
|
|
1314
1334
|
};
|
|
@@ -1413,6 +1433,7 @@ var IOS_KEYS = /* @__PURE__ */ new Set([
|
|
|
1413
1433
|
var ANDROID_KEYS = /* @__PURE__ */ new Set([
|
|
1414
1434
|
"packageName",
|
|
1415
1435
|
"activity",
|
|
1436
|
+
"scheme",
|
|
1416
1437
|
"gradleTasks",
|
|
1417
1438
|
"appApkPath",
|
|
1418
1439
|
"testApkPath",
|
|
@@ -1514,6 +1535,8 @@ function validateAndroid(android, errors) {
|
|
|
1514
1535
|
reportUnknownKeys("config.android", android, ANDROID_KEYS, errors);
|
|
1515
1536
|
requireAndroidPackage("config.android.packageName", android.packageName, errors);
|
|
1516
1537
|
requireAndroidActivity("config.android.activity", android.activity, errors);
|
|
1538
|
+
if (android.scheme !== void 0)
|
|
1539
|
+
requireAppScheme("config.android.scheme", android.scheme, errors);
|
|
1517
1540
|
optionalString("config.android.appApkPath", android.appApkPath, errors);
|
|
1518
1541
|
optionalString("config.android.testApkPath", android.testApkPath, errors);
|
|
1519
1542
|
if (android.instrumentationTarget !== void 0)
|
|
@@ -1526,7 +1549,10 @@ function validateAndroid(android, errors) {
|
|
|
1526
1549
|
errors.push("config.android.gradleTasks: expected an array of strings");
|
|
1527
1550
|
}
|
|
1528
1551
|
validateCompanion("config.android.companion", android.companion, errors);
|
|
1529
|
-
validateLaunch("config.android.launch", android.launch, errors);
|
|
1552
|
+
const launch = validateLaunch("config.android.launch", android.launch, errors);
|
|
1553
|
+
if (launch?.kind === "expo-dev-client" && android.scheme === void 0) {
|
|
1554
|
+
errors.push('config.android.scheme: required when android.launch.kind is "expo-dev-client"');
|
|
1555
|
+
}
|
|
1530
1556
|
}
|
|
1531
1557
|
function validateCompanion(path4, companion, errors) {
|
|
1532
1558
|
if (companion === void 0) return;
|
|
@@ -1572,6 +1598,7 @@ function requireString(path4, value, errors) {
|
|
|
1572
1598
|
}
|
|
1573
1599
|
var ANDROID_PACKAGE_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
1574
1600
|
var ANDROID_ACTIVITY_RE = /^\.?[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
1601
|
+
var APP_SCHEME_RE = /^[a-z][a-z0-9+.-]*$/;
|
|
1575
1602
|
function requireAndroidPackage(path4, value, errors) {
|
|
1576
1603
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1577
1604
|
errors.push(`${path4}: required non-empty string`);
|
|
@@ -1588,6 +1615,17 @@ function requireAndroidActivity(path4, value, errors) {
|
|
|
1588
1615
|
if (!ANDROID_ACTIVITY_RE.test(value))
|
|
1589
1616
|
errors.push(`${path4}: expected an activity name (e.g. .MainActivity)`);
|
|
1590
1617
|
}
|
|
1618
|
+
function requireAppScheme(path4, value, errors) {
|
|
1619
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1620
|
+
errors.push(`${path4}: required non-empty string`);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (!APP_SCHEME_RE.test(value)) {
|
|
1624
|
+
errors.push(
|
|
1625
|
+
`${path4}: expected a valid URL scheme (lowercase letter, then lowercase letters, digits, +, ., or -)`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1591
1629
|
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
1630
|
function requireInstrumentationTarget(path4, value, errors) {
|
|
1593
1631
|
if (typeof value !== "string" || value.trim() === "") {
|