prowl-tools 0.1.5 → 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/README.md CHANGED
@@ -935,6 +935,43 @@ Templates cover auth flows (OAuth, 2FA), e-commerce (Stripe), admin panels, SaaS
935
935
 
936
936
  ---
937
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
+
938
975
  ## macOS Target (Experimental)
939
976
 
940
977
  > **Experimental (PROWL-048).** Prowl can drive **native macOS apps** — including
@@ -1003,6 +1040,8 @@ at self-hosted / MDM-managed runners for now.
1003
1040
 
1004
1041
  ### Selector dialect (macOS)
1005
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).
1006
1045
  Native selectors address accessibility identifiers, roles, and labels:
1007
1046
 
1008
1047
  | Selector | Matches |
@@ -1139,8 +1178,10 @@ validates it before installing.
1139
1178
  ### Selector dialect (Android)
1140
1179
 
1141
1180
  Native selectors address `resource-id`, `content-desc`, visible text, and widget
1142
- class. Semantics match the macOS target so a selector means the same thing on both
1143
- native targets:
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):
1144
1185
 
1145
1186
  | Selector | Matches |
1146
1187
  |---|---|
@@ -1178,9 +1219,52 @@ equivalent yet and are rejected with a clear message; scroll-gesture support is
1178
1219
  follow-up. A degraded pure-`adb` fallback (`uiautomator dump` + `input tap`) is a
1179
1220
  possible future diagnostic mode, not the primary path.
1180
1221
 
1181
- > Out of scope for PROWL-058 (tracked separately): `prowl analyze` for Android and
1182
- > CI recipes (PROWL-061), the unified native selector engine (PROWL-060), and real
1183
- > iOS devices (PROWL-062). The iOS **simulator** target ships below.
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").
1184
1268
 
1185
1269
  ---
1186
1270
 
@@ -1242,7 +1326,9 @@ treated as a **bundle id** unless a directory of that name exists, so bundle ids
1242
1326
 
1243
1327
  Native selectors address accessibility ids, labels, visible text, and element type.
1244
1328
  Semantics match the macOS/Android targets so a selector means the same thing across
1245
- native targets:
1329
+ native targets — see the
1330
+ [Native Selector Dialect matrix](#native-selector-dialect-compatibility-matrix)
1331
+ (and the `label=` exact-match trap):
1246
1332
 
1247
1333
  | Selector | Matches |
1248
1334
  |---|---|
@@ -1278,9 +1364,165 @@ message. Screenshots are captured with `simctl` (not WDA), so artifacts still wo
1278
1364
  even if the agent wedges. `hover` and `scrollTo` have no touch equivalent yet and are
1279
1365
  rejected with a clear message; scroll-gesture support is a follow-up.
1280
1366
 
1281
- > Out of scope for PROWL-059 (tracked separately): `prowl analyze` for iOS and CI
1282
- > recipes (PROWL-061), the unified native selector engine (PROWL-060), and real iOS
1283
- > devices (PROWL-062).
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.
1284
1526
 
1285
1527
  ---
1286
1528