prowl-tools 0.1.4 → 0.1.6
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/NOTICE +10 -0
- package/README.md +519 -2
- package/dist/{chunk-ZEFVTKQT.js → chunk-5KQR3IR3.js} +3321 -346
- package/dist/chunk-5KQR3IR3.js.map +1 -0
- package/dist/{chunk-MBAIGNVO.js → chunk-WHAMB4TY.js} +43 -3
- package/dist/chunk-WHAMB4TY.js.map +1 -0
- package/dist/index.cjs +3527 -481
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +325 -67
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +3444 -325
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +1236 -11
- package/dist/lib.d.ts +1236 -11
- package/dist/lib.js +194 -2
- package/dist/{loader-XMTDB6OL.js → loader-PBBYV3U7.js} +2 -2
- package/package.json +5 -1
- package/dist/chunk-MBAIGNVO.js.map +0 -1
- package/dist/chunk-ZEFVTKQT.js.map +0 -1
- /package/dist/{loader-XMTDB6OL.js.map → loader-PBBYV3U7.js.map} +0 -0
package/NOTICE
CHANGED
|
@@ -12,6 +12,16 @@ attributions are provided for the benefit of downstream users.
|
|
|
12
12
|
Direct Runtime Dependencies
|
|
13
13
|
-------------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
+
appium-uiautomator2-server
|
|
16
|
+
License: Apache-2.0
|
|
17
|
+
Copyright (c) Appium Contributors
|
|
18
|
+
https://github.com/appium/appium-uiautomator2-server
|
|
19
|
+
|
|
20
|
+
appium-webdriveragent
|
|
21
|
+
License: Apache-2.0
|
|
22
|
+
Copyright (c) Appium Contributors
|
|
23
|
+
https://github.com/appium/WebDriverAgent
|
|
24
|
+
|
|
15
25
|
chalk
|
|
16
26
|
License: MIT
|
|
17
27
|
https://github.com/chalk/chalk
|
package/README.md
CHANGED
|
@@ -55,6 +55,29 @@ Prowl uses Playwright under the hood. Install the browser:
|
|
|
55
55
|
npx playwright install chromium
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
The on-device agents for the experimental Android/iOS targets
|
|
59
|
+
(`appium-uiautomator2-server`, `appium-webdriveragent`) are **optional
|
|
60
|
+
dependencies** — installed by default so mobile testing works out of the box,
|
|
61
|
+
but skippable for a leaner web-only install:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install -g prowl-tools --omit=optional
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If a mobile target later needs an omitted agent, it fails with exact recovery
|
|
68
|
+
commands for global and local installs. For a global npm install, restore the
|
|
69
|
+
agents with:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g appium-uiautomator2-server@10.6.2 appium-webdriveragent@16.4.0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For a local project install, omit `-g`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install appium-uiautomator2-server@10.6.2 appium-webdriveragent@16.4.0
|
|
79
|
+
```
|
|
80
|
+
|
|
58
81
|
### 2. Initialize
|
|
59
82
|
|
|
60
83
|
```bash
|
|
@@ -424,11 +447,21 @@ Config lives at `.prowl/config.yml`. All options with defaults:
|
|
|
424
447
|
```yaml
|
|
425
448
|
# Execution target. Defaults to the web target; existing configs work unchanged.
|
|
426
449
|
target:
|
|
427
|
-
type: "web" # "web" (default) or "
|
|
450
|
+
type: "web" # "web" (default), "macos", "android", or "ios" (experimental)
|
|
428
451
|
url: "http://localhost:3000" # Required for web targets
|
|
429
452
|
# For the experimental macOS target instead:
|
|
430
453
|
# type: "macos"
|
|
431
454
|
# app: "com.example.App" # bundle id or /path/to/App.app (see "macOS Target")
|
|
455
|
+
# For the experimental Android target instead:
|
|
456
|
+
# type: "android"
|
|
457
|
+
# app: "com.example.app" # package name or /path/to/app.apk (see "Android Target")
|
|
458
|
+
# deviceSerial: "emulator-5554" # optional; required only with several devices attached
|
|
459
|
+
# coldStart: false # optional; pm clear before launch
|
|
460
|
+
# For the experimental iOS simulator target instead:
|
|
461
|
+
# type: "ios"
|
|
462
|
+
# app: "com.example.App" # bundle id or /path/to/App.app (see "iOS Target")
|
|
463
|
+
# udid: "..." # optional; required only with several booted simulators
|
|
464
|
+
# coldStart: false # optional; uninstall+reinstall before launch (needs a .app)
|
|
432
465
|
|
|
433
466
|
# Browser settings
|
|
434
467
|
browser:
|
|
@@ -458,7 +491,7 @@ guardrails:
|
|
|
458
491
|
forbiddenSelectors: # selectors that steps cannot use
|
|
459
492
|
- "[data-danger]"
|
|
460
493
|
- ".delete-btn"
|
|
461
|
-
allowedApps: [] #
|
|
494
|
+
allowedApps: [] # native targets only: macOS bundle IDs/names/.app paths, Android package IDs/canonical APK paths, or iOS bundle IDs/.app paths
|
|
462
495
|
|
|
463
496
|
# Auth state from `prowl login`
|
|
464
497
|
auth:
|
|
@@ -902,6 +935,43 @@ Templates cover auth flows (OAuth, 2FA), e-commerce (Stripe), admin panels, SaaS
|
|
|
902
935
|
|
|
903
936
|
---
|
|
904
937
|
|
|
938
|
+
## Native Selector Dialect (compatibility matrix)
|
|
939
|
+
|
|
940
|
+
Android and iOS now consume one shared selector dialect implementation, so `id=`
|
|
941
|
+
/ `label=` / `text=` / `role=` mean the same *shape* of thing on both mobile
|
|
942
|
+
targets. That shared grammar, per-platform attribute mapping, ranking order, and
|
|
943
|
+
host-side matching live in `src/selector/native.ts`. macOS remains on its existing
|
|
944
|
+
driver/analyzer implementation for now, with migration deferred, but follows the
|
|
945
|
+
same documented selector shape. The web target speaks Playwright's own selector
|
|
946
|
+
engines and is shown for contrast.
|
|
947
|
+
|
|
948
|
+
| Kind | Web (Playwright) | macOS (AX) | Android (uiautomator2) | iOS (WebDriverAgent) |
|
|
949
|
+
|---|---|---|---|---|
|
|
950
|
+
| `id=` | use CSS `#id` / `[data-testid]` | `AXIdentifier`, exact | `resource-id`, exact (bare names are package-qualified: `save` → `<pkg>:id/save`) | accessibility id (the `name` attribute), exact |
|
|
951
|
+
| `label=` | *(no native kind; analyzer surfaces the associated `<label>` text)* | `title`/`description`, **exact** | `content-desc`, **exact** | `accessibilityLabel`, **exact** |
|
|
952
|
+
| `text=` (or bare) | text engine, substring, case-insensitive | `title`/`description`/`value`, substring | visible `text`, substring | `label` **or** `value`, substring |
|
|
953
|
+
| `role=` | ARIA role engine | AX role (e.g. `AXButton`) | widget class (e.g. `android.widget.Button`) | element type (`XCUIElementType…`; shorthand `Button` accepted) |
|
|
954
|
+
| `role=X[name="Y"]` | role + accessible name (substring) | role + name (substring) | class + visible-text (substring) | type + (`label` or `value`) substring |
|
|
955
|
+
| `:focus` | *(n/a)* | focused element | `UiSelector().focused(true)` | `hasKeyboardFocus == 1` |
|
|
956
|
+
|
|
957
|
+
**The `label=`-in-assertions trap.** On every native target `label=` is an **exact**
|
|
958
|
+
match on the accessibility label — unlike `text=`, which is a substring match, and
|
|
959
|
+
unlike the web, where text matching is forgiving. So `assert: selectorExists:
|
|
960
|
+
label="Save"` will **not** match an element whose real label is "Save changes"; it
|
|
961
|
+
silently fails rather than partially matching. Use `text=` when you want substring
|
|
962
|
+
behavior in an assertion, and keep `label=` for the exact accessibility label. On
|
|
963
|
+
iOS there is a second trap: WDA's page source exposes a single `name` attribute that
|
|
964
|
+
is the `accessibilityIdentifier` when one is set and otherwise the label. Prowl's
|
|
965
|
+
analyzer only recommends `id=` when `name` differs from `label`, so it does not emit
|
|
966
|
+
label-shaped ids, but runtime and host-side matching still resolve `id=` against
|
|
967
|
+
WDA's `name`.
|
|
968
|
+
|
|
969
|
+
Prefer `id=` on every native target — the native analog of `data-testid`. Per-target
|
|
970
|
+
specifics (escaping, `statusItem`/`menu=` on macOS, the Compose `testTagsAsResourceId`
|
|
971
|
+
caveat on Android) follow in each target's own section below.
|
|
972
|
+
|
|
973
|
+
---
|
|
974
|
+
|
|
905
975
|
## macOS Target (Experimental)
|
|
906
976
|
|
|
907
977
|
> **Experimental (PROWL-048).** Prowl can drive **native macOS apps** — including
|
|
@@ -970,6 +1040,8 @@ at self-hosted / MDM-managed runners for now.
|
|
|
970
1040
|
|
|
971
1041
|
### Selector dialect (macOS)
|
|
972
1042
|
|
|
1043
|
+
See the [Native Selector Dialect matrix](#native-selector-dialect-compatibility-matrix)
|
|
1044
|
+
for how these compare across native targets (and the `label=` exact-match trap).
|
|
973
1045
|
Native selectors address accessibility identifiers, roles, and labels:
|
|
974
1046
|
|
|
975
1047
|
| Selector | Matches |
|
|
@@ -985,6 +1057,52 @@ Native selectors address accessibility identifiers, roles, and labels:
|
|
|
985
1057
|
semantics as the web target). Prefer `id=` (accessibility identifiers) — the native
|
|
986
1058
|
analog of `data-testid`.
|
|
987
1059
|
|
|
1060
|
+
### Finding selectors
|
|
1061
|
+
|
|
1062
|
+
Don't guess selectors — dump them. `prowl analyze` works on the macOS target the
|
|
1063
|
+
same way it does on the web: it launches/attaches to the app, walks the
|
|
1064
|
+
Accessibility tree, and prints every interactive element with **ranked selector
|
|
1065
|
+
candidates** (best first) plus the app's windows and status-item menu contents.
|
|
1066
|
+
It is read-only (the only interaction is opening and closing the status menu),
|
|
1067
|
+
honors `guardrails.allowedApps`, and leaves the app running when done.
|
|
1068
|
+
|
|
1069
|
+
```bash
|
|
1070
|
+
# Uses the macOS target from .prowl/config.yml:
|
|
1071
|
+
prowl analyze
|
|
1072
|
+
|
|
1073
|
+
# …or point it at any app without a config:
|
|
1074
|
+
prowl analyze --app com.example.App
|
|
1075
|
+
prowl analyze --app "/Applications/Example.app"
|
|
1076
|
+
|
|
1077
|
+
# Machine-readable output for agents:
|
|
1078
|
+
prowl analyze --app com.example.App --json
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
Example (human-readable) output:
|
|
1082
|
+
|
|
1083
|
+
```text
|
|
1084
|
+
App Analysis: com.example.App
|
|
1085
|
+
|
|
1086
|
+
Windows:
|
|
1087
|
+
"Main Window" id=mainWindow
|
|
1088
|
+
|
|
1089
|
+
Interactive Elements:
|
|
1090
|
+
AXButton id=saveButton "Save"
|
|
1091
|
+
AXTextField label="Email" "Email"
|
|
1092
|
+
AXCheckBox label="Remember me" "Remember me" (disabled)
|
|
1093
|
+
|
|
1094
|
+
Menu Bar:
|
|
1095
|
+
AXMenuItem id=preferences "Preferences…"
|
|
1096
|
+
AXMenuItem label="Quit" "Quit"
|
|
1097
|
+
|
|
1098
|
+
3 elements, 1 windows, 2 menu items
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
Selectors are ranked `id=` > `label=` > `role=…[name="…"]` > `text=` — copy the
|
|
1102
|
+
first (most durable) candidate into your hunt. Status-item menu identifiers
|
|
1103
|
+
(`id=preferences` above) are especially valuable, since menu titles often carry
|
|
1104
|
+
ellipses or localized text that are awkward to match by substring.
|
|
1105
|
+
|
|
988
1106
|
### Step compatibility
|
|
989
1107
|
|
|
990
1108
|
Portable steps run on **both** targets; web-only steps are rejected up front on the
|
|
@@ -1009,6 +1127,405 @@ after the run.
|
|
|
1009
1127
|
|
|
1010
1128
|
---
|
|
1011
1129
|
|
|
1130
|
+
## Android Target (Experimental)
|
|
1131
|
+
|
|
1132
|
+
> **Experimental (PROWL-058).** Prowl can drive **native Android apps** on an
|
|
1133
|
+
> emulator or a USB-connected device, in addition to the web and macOS targets.
|
|
1134
|
+
> It follows the same "external agent + JSON protocol" shape as the macOS target:
|
|
1135
|
+
> `adb` handles device lifecycle and the on-device
|
|
1136
|
+
> [`appium-uiautomator2-server`](https://github.com/appium/appium-uiautomator2-driver)
|
|
1137
|
+
> (Apache-2.0) handles UI interaction over a plain HTTP/JSON API driven with raw
|
|
1138
|
+
> `fetch`. The API, selector dialect, and step coverage may change.
|
|
1139
|
+
|
|
1140
|
+
### Requirements
|
|
1141
|
+
|
|
1142
|
+
- **`adb`** on your `PATH` (from the Android SDK platform-tools), plus at least
|
|
1143
|
+
one **booted emulator or device** (`adb devices -l` should list it as `device`).
|
|
1144
|
+
- The two prebuilt agent APKs ship inside the `appium-uiautomator2-server` npm
|
|
1145
|
+
dependency and are installed onto the device automatically — **nothing to build**.
|
|
1146
|
+
No Appium server, no JVM, no gRPC.
|
|
1147
|
+
- If `target.app` is an **`.apk`**, Android build-tools **`aapt`/`aapt2`** must be
|
|
1148
|
+
on `PATH` so Prowl can read the package name (or set `target.app` to the package
|
|
1149
|
+
name directly and install the APK yourself).
|
|
1150
|
+
|
|
1151
|
+
### Enabling it
|
|
1152
|
+
|
|
1153
|
+
Point your config at an Android target:
|
|
1154
|
+
|
|
1155
|
+
```yaml
|
|
1156
|
+
target:
|
|
1157
|
+
type: android
|
|
1158
|
+
app: "com.example.app" # a package name, or a path to an .apk to install
|
|
1159
|
+
# deviceSerial: "emulator-5554" # required only when several devices are attached
|
|
1160
|
+
# coldStart: true # `pm clear` before launch for a deterministic start (default off)
|
|
1161
|
+
guardrails:
|
|
1162
|
+
allowedApps: # optional scope; empty = allow the target app
|
|
1163
|
+
- "com.example.app"
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
Then run a hunt as usual: `prowl run my-android-hunt`.
|
|
1167
|
+
|
|
1168
|
+
On launch Prowl selects the device (failing with an actionable error, listing
|
|
1169
|
+
serials, if several are attached and no `deviceSerial` is set), installs the app
|
|
1170
|
+
(when given an `.apk`) and the agent, starts the agent via `am instrument`,
|
|
1171
|
+
port-forwards it on a **dynamically allocated** local port (so parallel sessions /
|
|
1172
|
+
CI jobs don't collide), and waits for the agent to report ready. Everything is
|
|
1173
|
+
torn down (agent session, instrumentation, port forward, `am force-stop`) after
|
|
1174
|
+
the run. `guardrails.allowedApps` accepts Android package IDs or canonical full
|
|
1175
|
+
`.apk` paths; when `target.app` is an APK, Prowl resolves its package ID and
|
|
1176
|
+
validates it before installing.
|
|
1177
|
+
|
|
1178
|
+
### Selector dialect (Android)
|
|
1179
|
+
|
|
1180
|
+
Native selectors address `resource-id`, `content-desc`, visible text, and widget
|
|
1181
|
+
class. Semantics match the macOS and iOS targets so a selector means the same thing
|
|
1182
|
+
across native targets — see the
|
|
1183
|
+
[Native Selector Dialect matrix](#native-selector-dialect-compatibility-matrix)
|
|
1184
|
+
(and the `label=` exact-match trap):
|
|
1185
|
+
|
|
1186
|
+
| Selector | Matches |
|
|
1187
|
+
|---|---|
|
|
1188
|
+
| `id=save` | element whose `resource-id` is `save` — a bare name is auto-qualified with the target app's package (`com.pkg:id/save`); ids in other namespaces need the full form (e.g. `id=android:id/title`) |
|
|
1189
|
+
| `label="Submit"` | element whose `content-desc` equals `Submit` (exact) |
|
|
1190
|
+
| `role=android.widget.Button` | element of that widget class |
|
|
1191
|
+
| `role=android.widget.Button[name="Save"]` | that widget class whose visible text contains `Save` |
|
|
1192
|
+
| `text="Save"` or bare `Save` | element whose visible text contains the text (substring) |
|
|
1193
|
+
|
|
1194
|
+
Prefer `id=` (the native analog of `data-testid`). **Jetpack Compose caveat:**
|
|
1195
|
+
Compose nodes only expose a `resource-id` when the app sets
|
|
1196
|
+
`Modifier.testTag(...)` **and** enables `testTagsAsResourceId = true`; otherwise
|
|
1197
|
+
match Compose UI with `text=` or `label=` (from `Modifier.semantics { contentDescription = ... }`).
|
|
1198
|
+
`forbiddenSelectors` still applies (text patterns use the same substring semantics
|
|
1199
|
+
as the other targets).
|
|
1200
|
+
|
|
1201
|
+
### Step compatibility
|
|
1202
|
+
|
|
1203
|
+
Portable steps run on the Android target; web-only steps are rejected up front
|
|
1204
|
+
(with a clear error), and `prowl login` / URL guardrails do not apply.
|
|
1205
|
+
|
|
1206
|
+
| Portable (Android) | Not supported on Android |
|
|
1207
|
+
|---|---|
|
|
1208
|
+
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
|
|
1209
|
+
| `wait`, `waitForSelector` | `mockRoute` / `unmockRoute`, `evalScript`, `runScript` |
|
|
1210
|
+
| `assert: visible` / `notVisible` | `onDialog`, `select` / `selectOption`, `setInputFiles` |
|
|
1211
|
+
| `screenshot`, `assertScreenshot` | `waitForDownload`, `scroll`, `assert: urlIncludes` / `urlEquals` |
|
|
1212
|
+
| `repeat`, `if`, `runHunt`, `copyText` | `hover`, `scrollTo` (no touch equivalent yet — see below) |
|
|
1213
|
+
|
|
1214
|
+
Notes: `type` and `fill` set text on the focused / matched field **unicode-safely**
|
|
1215
|
+
(via the agent's `element/value`, not `adb shell input text`); `press` maps key
|
|
1216
|
+
names (`Enter`, `Tab`, `Backspace`, `Back`, `Home`, arrow keys, …) onto Android key
|
|
1217
|
+
codes and dispatches them to the focused view. `hover` and `scrollTo` have no touch
|
|
1218
|
+
equivalent yet and are rejected with a clear message; scroll-gesture support is a
|
|
1219
|
+
follow-up. A degraded pure-`adb` fallback (`uiautomator dump` + `input tap`) is a
|
|
1220
|
+
possible future diagnostic mode, not the primary path.
|
|
1221
|
+
|
|
1222
|
+
### Finding selectors (Android)
|
|
1223
|
+
|
|
1224
|
+
Don't guess selectors — dump them. `prowl analyze` works on the Android target the
|
|
1225
|
+
same way it does on the web and macOS: it attaches to the running app, reads the
|
|
1226
|
+
uiautomator UI hierarchy, and prints every interactive element with **ranked
|
|
1227
|
+
selector candidates** (best first). It is read-only, honors
|
|
1228
|
+
`guardrails.allowedApps`, and leaves the app running when done. Point it at a
|
|
1229
|
+
booted emulator/device:
|
|
1230
|
+
|
|
1231
|
+
```bash
|
|
1232
|
+
# Uses the Android target from .prowl/config.yml:
|
|
1233
|
+
prowl analyze
|
|
1234
|
+
|
|
1235
|
+
# …or force the Android target explicitly:
|
|
1236
|
+
prowl analyze --app com.android.settings --platform android
|
|
1237
|
+
prowl analyze --app ./app-debug.apk # an .apk implies Android
|
|
1238
|
+
prowl analyze --app com.example.app --device emulator-5556 # pick a device
|
|
1239
|
+
|
|
1240
|
+
# Machine-readable output for agents:
|
|
1241
|
+
prowl analyze --app com.android.settings --platform android --json
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
Ranking (best → last resort): `id=` (the package-qualified `resource-id`, the
|
|
1245
|
+
native `data-testid`) > `label=` (content-desc) > `role=<class>[name="<text>"]` >
|
|
1246
|
+
`text=`. Example (human-readable) output:
|
|
1247
|
+
|
|
1248
|
+
```text
|
|
1249
|
+
App Analysis: com.android.settings
|
|
1250
|
+
|
|
1251
|
+
Interactive Elements:
|
|
1252
|
+
android.widget.EditText id=com.android.settings:id/search_src_text "Search settings"
|
|
1253
|
+
android.widget.LinearLayout text="Network & internet" "Network & internet"
|
|
1254
|
+
android.widget.Switch id=com.android.settings:id/switch_widget (disabled)
|
|
1255
|
+
|
|
1256
|
+
3 elements
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
> Platform selection for `--app`: an `.apk` implies Android; otherwise pass
|
|
1260
|
+
> `--platform android` (a bare bundle-id / package is ambiguous with the macOS and
|
|
1261
|
+
> iOS targets, which default to macOS unless a config `target.type` or `--platform`
|
|
1262
|
+
> says otherwise). With an Android `target.type` in `.prowl/config.yml`, a bare
|
|
1263
|
+
> `prowl analyze` needs no flag.
|
|
1264
|
+
>
|
|
1265
|
+
> Out of scope for PROWL-058 (tracked separately): the unified native selector
|
|
1266
|
+
> engine (PROWL-060) and real iOS devices (PROWL-062). `prowl analyze` for Android
|
|
1267
|
+
> and the CI recipes shipped in PROWL-061 (above, and see "Mobile targets in CI").
|
|
1268
|
+
|
|
1269
|
+
---
|
|
1270
|
+
|
|
1271
|
+
## iOS Target (Experimental)
|
|
1272
|
+
|
|
1273
|
+
> **Experimental (PROWL-059).** Prowl can drive **native iOS apps** on a **booted
|
|
1274
|
+
> iOS Simulator**, in addition to the web, macOS, and Android targets. It follows
|
|
1275
|
+
> the same "external agent + JSON protocol" shape: `xcrun simctl` handles simulator
|
|
1276
|
+
> lifecycle and screenshots, and the on-simulator
|
|
1277
|
+
> [WebDriverAgent](https://github.com/appium/WebDriverAgent) (Apache-2.0) handles UI
|
|
1278
|
+
> interaction over its W3C-shaped HTTP/JSON API driven with raw `fetch`. The API,
|
|
1279
|
+
> selector dialect, and step coverage may change. **Real devices are out of scope**
|
|
1280
|
+
> (PROWL-062) — simulators only.
|
|
1281
|
+
|
|
1282
|
+
### Requirements
|
|
1283
|
+
|
|
1284
|
+
- **macOS with a full Xcode** (not just the command-line tools) installed and
|
|
1285
|
+
selected (`xcode-select -p`), so `xcrun simctl` and `xcodebuild` are available.
|
|
1286
|
+
- At least one **booted simulator** (`xcrun simctl list devices | grep Booted`, or
|
|
1287
|
+
boot one with `xcrun simctl boot <udid>` / from Xcode).
|
|
1288
|
+
- The **WebDriverAgent** runner is built **once** from the `appium-webdriveragent`
|
|
1289
|
+
npm dependency (`xcodebuild build-for-testing`) and cached under
|
|
1290
|
+
`~/.prowl/wda/<wda-version>-xcode<xcode-version>/`; the first run prints a one-time
|
|
1291
|
+
"building WebDriverAgent…" notice and can take a few minutes. Simulators need **no
|
|
1292
|
+
code signing**. Set `PROWL_WDA_RUNNER` to a prebuilt `WebDriverAgentRunner-Runner.app`
|
|
1293
|
+
to skip the build (e.g. in CI with a cached runner).
|
|
1294
|
+
|
|
1295
|
+
### Enabling it
|
|
1296
|
+
|
|
1297
|
+
Point your config at an iOS target:
|
|
1298
|
+
|
|
1299
|
+
```yaml
|
|
1300
|
+
target:
|
|
1301
|
+
type: ios
|
|
1302
|
+
app: "com.example.App" # a bundle id, or a path to a built .app to install
|
|
1303
|
+
# udid: "ABCD-1234" # required only when several simulators are booted
|
|
1304
|
+
# coldStart: true # uninstall+reinstall before launch (requires a .app path)
|
|
1305
|
+
guardrails:
|
|
1306
|
+
allowedApps: # optional scope; empty = allow the target app
|
|
1307
|
+
- "com.example.App"
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
Then run a hunt as usual: `prowl run my-ios-hunt`.
|
|
1311
|
+
|
|
1312
|
+
On launch Prowl selects the booted simulator (failing with an actionable error,
|
|
1313
|
+
listing candidates, if several are booted and no `udid` is set), installs the app
|
|
1314
|
+
(when given a `.app`, reading its bundle id from the bundle's **root** `Info.plist`),
|
|
1315
|
+
builds/caches and installs the WebDriverAgent runner, launches it on a
|
|
1316
|
+
**dynamically allocated** port (passed via `SIMCTL_CHILD_USE_PORT` so parallel
|
|
1317
|
+
sessions / CI jobs don't collide), launches the target app, and waits for WDA to
|
|
1318
|
+
report ready. Everything is torn down (WDA session, `simctl terminate` of the runner
|
|
1319
|
+
and the app) after the run. `guardrails.allowedApps` accepts iOS bundle ids or
|
|
1320
|
+
`.app` paths; a `.app` path is authorized by its path, bundle name, or the bundle id
|
|
1321
|
+
read from its root `Info.plist`. Note: a bare `target.app` ending in `.app` is
|
|
1322
|
+
treated as a **bundle id** unless a directory of that name exists, so bundle ids like
|
|
1323
|
+
`com.company.app` are not mistaken for paths.
|
|
1324
|
+
|
|
1325
|
+
### Selector dialect (iOS)
|
|
1326
|
+
|
|
1327
|
+
Native selectors address accessibility ids, labels, visible text, and element type.
|
|
1328
|
+
Semantics match the macOS/Android targets so a selector means the same thing across
|
|
1329
|
+
native targets — see the
|
|
1330
|
+
[Native Selector Dialect matrix](#native-selector-dialect-compatibility-matrix)
|
|
1331
|
+
(and the `label=` exact-match trap):
|
|
1332
|
+
|
|
1333
|
+
| Selector | Matches |
|
|
1334
|
+
|---|---|
|
|
1335
|
+
| `id=save` | element whose accessibility id (`accessibilityIdentifier` / name) is `save` |
|
|
1336
|
+
| `label="Submit"` | element whose `accessibilityLabel` equals `Submit` (exact) |
|
|
1337
|
+
| `role=XCUIElementTypeButton` | element of that type (shorthand `role=Button` works too) |
|
|
1338
|
+
| `role=Button[name="Save"]` | that type whose visible `label`/`value` contains `Save` |
|
|
1339
|
+
| `text="Save"` or bare `Save` | element whose `label` or `value` contains the text (substring) |
|
|
1340
|
+
| `:focus` | the element with keyboard focus (`hasKeyboardFocus == 1`) |
|
|
1341
|
+
|
|
1342
|
+
Prefer `id=` (set `accessibilityIdentifier` in your app — the native analog of
|
|
1343
|
+
`data-testid`). Text/label/role+name selectors compile to WDA NSPredicate strings
|
|
1344
|
+
(quotes and backslashes are escaped). `forbiddenSelectors` still applies.
|
|
1345
|
+
|
|
1346
|
+
### Step compatibility
|
|
1347
|
+
|
|
1348
|
+
Portable steps run on the iOS target; web-only steps are rejected up front (with a
|
|
1349
|
+
clear error), and URL guardrails do not apply.
|
|
1350
|
+
|
|
1351
|
+
| Portable (iOS) | Not supported on iOS |
|
|
1352
|
+
|---|---|
|
|
1353
|
+
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
|
|
1354
|
+
| `wait`, `waitForSelector` | `mockRoute` / `unmockRoute`, `evalScript`, `runScript` |
|
|
1355
|
+
| `assert: visible` / `notVisible` | `onDialog`, `select` / `selectOption`, `setInputFiles` |
|
|
1356
|
+
| `screenshot`, `assertScreenshot` | `waitForDownload`, `scroll`, `assert: urlIncludes` / `urlEquals` |
|
|
1357
|
+
| `repeat`, `if`, `runHunt`, `copyText` | `hover`, `scrollTo` (no touch equivalent yet) |
|
|
1358
|
+
|
|
1359
|
+
Notes: `type` and `fill` set text on the focused / matched field via WDA's
|
|
1360
|
+
`element/value`; `press` supports a small honest key set — `enter`/`return` and
|
|
1361
|
+
`delete`/`backspace` (sent through WDA's key endpoint to the focused element) and
|
|
1362
|
+
`home` (returns to the springboard) — and rejects other keys with the supported-keys
|
|
1363
|
+
message. Screenshots are captured with `simctl` (not WDA), so artifacts still work
|
|
1364
|
+
even if the agent wedges. `hover` and `scrollTo` have no touch equivalent yet and are
|
|
1365
|
+
rejected with a clear message; scroll-gesture support is a follow-up.
|
|
1366
|
+
|
|
1367
|
+
### Finding selectors (iOS)
|
|
1368
|
+
|
|
1369
|
+
Don't guess selectors — dump them. `prowl analyze` works on the iOS target the
|
|
1370
|
+
same way it does on the web, macOS, and Android: it attaches to the running app on
|
|
1371
|
+
a booted simulator, reads WebDriverAgent's UI hierarchy, and prints every
|
|
1372
|
+
interactive element (plus the app's windows) with **ranked selector candidates**
|
|
1373
|
+
(best first). It is read-only, honors `guardrails.allowedApps`, and leaves the app
|
|
1374
|
+
running when done:
|
|
1375
|
+
|
|
1376
|
+
```bash
|
|
1377
|
+
# Uses the iOS target from .prowl/config.yml:
|
|
1378
|
+
prowl analyze
|
|
1379
|
+
|
|
1380
|
+
# …or force the iOS target explicitly:
|
|
1381
|
+
prowl analyze --app com.apple.Preferences --platform ios
|
|
1382
|
+
prowl analyze --app com.example.App --platform ios --udid <SIM-UDID>
|
|
1383
|
+
|
|
1384
|
+
# Machine-readable output for agents:
|
|
1385
|
+
prowl analyze --app com.apple.Preferences --platform ios --json
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
Ranking (best → last resort): `id=` (accessibility id) > `label=` > `role=<Type>
|
|
1389
|
+
[name="<text>"]` > `text=`. Because WDA's page source exposes only a single `name`
|
|
1390
|
+
attribute — the accessibility identifier when set, otherwise the label — `id=` is
|
|
1391
|
+
offered only when that `name` differs from the element's label. Example output:
|
|
1392
|
+
|
|
1393
|
+
```text
|
|
1394
|
+
App Analysis: com.apple.Preferences
|
|
1395
|
+
|
|
1396
|
+
Windows:
|
|
1397
|
+
(untitled) role=Window
|
|
1398
|
+
|
|
1399
|
+
Interactive Elements:
|
|
1400
|
+
XCUIElementTypeButton id=general_button "General"
|
|
1401
|
+
XCUIElementTypeCell label="Wi-Fi" "Wi-Fi"
|
|
1402
|
+
XCUIElementTypeSwitch label="Airplane Mode" "Airplane Mode" (disabled)
|
|
1403
|
+
|
|
1404
|
+
3 elements, 1 windows
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
> A bare bundle-id `--app` is ambiguous with the macOS target (which is the
|
|
1408
|
+
> default), so pass `--platform ios`. With an iOS `target.type` in
|
|
1409
|
+
> `.prowl/config.yml`, a bare `prowl analyze` needs no flag.
|
|
1410
|
+
>
|
|
1411
|
+
> Out of scope for PROWL-059 (tracked separately): the unified native selector
|
|
1412
|
+
> engine (PROWL-060) and real iOS devices (PROWL-062). `prowl analyze` for iOS and
|
|
1413
|
+
> the CI recipes shipped in PROWL-061 (above, and see "Mobile targets in CI").
|
|
1414
|
+
|
|
1415
|
+
---
|
|
1416
|
+
|
|
1417
|
+
## Mobile targets in CI
|
|
1418
|
+
|
|
1419
|
+
The Android and iOS targets run in continuous integration, either on GitHub-hosted
|
|
1420
|
+
runners or on a self-hosted Mac. Every recipe runs the real `prowl` CLI against a
|
|
1421
|
+
booted emulator/simulator and uploads run artifacts (screenshots, JUnit, reports).
|
|
1422
|
+
|
|
1423
|
+
### Android on `ubuntu-latest` (GitHub-hosted)
|
|
1424
|
+
|
|
1425
|
+
GitHub's Linux runners support KVM, so a hardware-accelerated emulator boots in the
|
|
1426
|
+
job via [`reactivecircus/android-emulator-runner`](https://github.com/ReactiveCircus/android-emulator-runner).
|
|
1427
|
+
Prowl installs the uiautomator2 agent APKs from its optional dependency automatically.
|
|
1428
|
+
|
|
1429
|
+
```yaml
|
|
1430
|
+
name: Android E2E
|
|
1431
|
+
on: [push, pull_request]
|
|
1432
|
+
jobs:
|
|
1433
|
+
android:
|
|
1434
|
+
runs-on: ubuntu-latest
|
|
1435
|
+
steps:
|
|
1436
|
+
- uses: actions/checkout@v4
|
|
1437
|
+
- uses: actions/setup-node@v4
|
|
1438
|
+
with:
|
|
1439
|
+
node-version: 20
|
|
1440
|
+
- run: npm ci
|
|
1441
|
+
- run: npm run build
|
|
1442
|
+
|
|
1443
|
+
# KVM must be accessible for a fast emulator.
|
|
1444
|
+
- name: Enable KVM
|
|
1445
|
+
run: |
|
|
1446
|
+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
|
|
1447
|
+
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
1448
|
+
sudo udevadm control --reload-rules
|
|
1449
|
+
sudo udevadm trigger --name-match=kvm
|
|
1450
|
+
|
|
1451
|
+
- name: Run hunts against the emulator
|
|
1452
|
+
uses: reactivecircus/android-emulator-runner@v2
|
|
1453
|
+
with:
|
|
1454
|
+
api-level: 34
|
|
1455
|
+
arch: x86_64
|
|
1456
|
+
force-avd-creation: false
|
|
1457
|
+
emulator-options: -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader_indirect
|
|
1458
|
+
disable-animations: true
|
|
1459
|
+
# `prowl` is your installed CLI (e.g. `npx prowl` or a global install);
|
|
1460
|
+
# the emulator is booted and on adb by the time this runs.
|
|
1461
|
+
script: npx prowl ci --junit
|
|
1462
|
+
|
|
1463
|
+
- name: Upload artifacts
|
|
1464
|
+
if: always()
|
|
1465
|
+
uses: actions/upload-artifact@v4
|
|
1466
|
+
with:
|
|
1467
|
+
name: android-artifacts
|
|
1468
|
+
path: .prowl/runs/**
|
|
1469
|
+
if-no-files-found: ignore
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
### iOS simulators on `macos-*` (GitHub-hosted)
|
|
1473
|
+
|
|
1474
|
+
macOS runners ship Xcode and the iOS simulator runtimes. Boot a simulator with
|
|
1475
|
+
`xcrun simctl`, and cache the one-time WebDriverAgent build (`~/.prowl/wda/`, keyed
|
|
1476
|
+
on the WDA + Xcode versions) so subsequent runs skip the ~2-minute `xcodebuild`.
|
|
1477
|
+
|
|
1478
|
+
```yaml
|
|
1479
|
+
name: iOS E2E
|
|
1480
|
+
on: [push, pull_request]
|
|
1481
|
+
jobs:
|
|
1482
|
+
ios:
|
|
1483
|
+
runs-on: macos-15
|
|
1484
|
+
steps:
|
|
1485
|
+
- uses: actions/checkout@v4
|
|
1486
|
+
- uses: actions/setup-node@v4
|
|
1487
|
+
with:
|
|
1488
|
+
node-version: 20
|
|
1489
|
+
- run: npm ci
|
|
1490
|
+
- run: npm run build
|
|
1491
|
+
|
|
1492
|
+
# Cache the built WebDriverAgent runner across runs.
|
|
1493
|
+
- name: Cache WebDriverAgent
|
|
1494
|
+
uses: actions/cache@v4
|
|
1495
|
+
with:
|
|
1496
|
+
path: ~/.prowl/wda
|
|
1497
|
+
key: prowl-wda-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
|
1498
|
+
|
|
1499
|
+
- name: Boot a simulator
|
|
1500
|
+
run: |
|
|
1501
|
+
xcrun simctl boot "iPhone 16" || true
|
|
1502
|
+
xcrun simctl bootstatus "iPhone 16"
|
|
1503
|
+
|
|
1504
|
+
- name: Run hunts against the simulator
|
|
1505
|
+
run: npx prowl ci --junit
|
|
1506
|
+
|
|
1507
|
+
- name: Upload artifacts
|
|
1508
|
+
if: always()
|
|
1509
|
+
uses: actions/upload-artifact@v4
|
|
1510
|
+
with:
|
|
1511
|
+
name: ios-artifacts
|
|
1512
|
+
path: .prowl/runs/**
|
|
1513
|
+
if-no-files-found: ignore
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
### Self-hosted device-verification gate
|
|
1517
|
+
|
|
1518
|
+
This repo also ships `.github/workflows/mobile-e2e.yml`, a real end-to-end gate on
|
|
1519
|
+
the Prowl Tools self-hosted Mac (labels `self-hosted, macOS, prowl-mobile`) that
|
|
1520
|
+
boots both a headless emulator and a simulator and drives Settings on each through
|
|
1521
|
+
the real CLI. It runs on `workflow_dispatch` (the owner's post-merge verification)
|
|
1522
|
+
and on same-repo pull requests, skipping cleanly (green) for forks/outside PRs that
|
|
1523
|
+
can't reach the runner, and uses its own `concurrency` group so it never collides
|
|
1524
|
+
with other jobs on the shared box. It is the machine-run version of the manual
|
|
1525
|
+
smoke tests that caught the uiautomator2 wire-shape bug and the WDA readiness hang.
|
|
1526
|
+
|
|
1527
|
+
---
|
|
1528
|
+
|
|
1012
1529
|
## Troubleshooting
|
|
1013
1530
|
|
|
1014
1531
|
### "Could not find .prowl/config.yml"
|