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 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 "macos" (experimental)
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: [] # macOS target only: bundle IDs, bundle names, or .app paths
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"