iOS Simulator Target (Experimental)
Prowl can drive native iOS apps on a booted iOS Simulator, in addition to the web. You point target.type at ios, write the same portable steps you already know, and Prowl runs them against a real app in the simulator instead of a browser page.
It follows the same "external agent + JSON protocol" shape as the macOS and Android targets: xcrun simctl handles simulator lifecycle and screenshots, and the on-simulator WebDriverAgent (Apache-2.0) handles UI interaction over its W3C-shaped HTTP/JSON API driven with raw fetch.
The iOS target shipped in Prowl 0.1.5 and is experimental. Its API, selector dialect, and step coverage may still change. Real iOS devices are out of scope (tracked as PROWL-062 in the CLI repo) — the target drives booted simulators only.
Requirements
- macOS with a full Xcode (not just the command-line tools) installed and selected (
xcode-select -p), soxcrun simctlandxcodebuildare available. - At least one booted simulator — check with
xcrun simctl list devices | grep Booted, or boot one withxcrun simctl boot <udid>(or from Xcode).
The one-time WebDriverAgent build
UI interaction is handled by WebDriverAgent (Apache-2.0). Its runner app is built once from the appium-webdriveragent npm dependency with xcodebuild build-for-testing, then cached under:
~/.prowl/wda/<wda-version>-xcode<xcode-version>/
The cache key includes the WDA and Xcode versions, so the build is reused across runs and rebuilt only when either changes. The first run prints a one-time "building WebDriverAgent…" notice and can take a few minutes. Simulators need no code signing.
To skip the build entirely — for example in CI with a cached runner — set PROWL_WDA_RUNNER to a prebuilt WebDriverAgentRunner-Runner.app:
export PROWL_WDA_RUNNER="/path/to/WebDriverAgentRunner-Runner.app"
The on-simulator agent
The runner above is the appium-webdriveragent npm dependency. As of Prowl 0.1.5 it is an optionalDependency (pinned to appium-webdriveragent@16.4.0). Default npm install -g prowl-tools installs it, so the iOS target works out of the box. A lean, web-only install can skip it:
npm install -g prowl-tools --omit=optional # skips the mobile agents
If an iOS hunt runs without the agent present, Prowl fails with a clear message and the exact command to restore it:
# Global install (matches `npm install -g prowl-tools`):
npm install -g appium-webdriveragent
# Local project install:
npm install appium-webdriveragent
Enabling it
Point your config at an iOS target:
# .prowl/config.yml
target:
type: ios
app: "com.example.App" # a bundle id, or a path to a built .app to install
# udid: "ABCD-1234" # optional; required only when several simulators are booted
# coldStart: true # optional; uninstall+reinstall before launch (requires a .app path)
guardrails:
allowedApps: # optional scope; empty = allow the target app
- "com.example.App"
Then run a hunt exactly as you would for the web:
prowl run my-ios-hunt
On launch Prowl selects the booted simulator (failing with an actionable error, listing candidates, if several are booted and no udid is set), installs the app (when given a .app, reading its bundle id from the bundle's root Info.plist), builds/caches and installs the WebDriverAgent runner, launches it on a dynamically allocated port (passed via SIMCTL_CHILD_USE_PORT so parallel sessions and CI jobs don't collide), launches the target app, and waits for WDA to report ready. Everything is torn down (WDA session, simctl terminate of the runner and the app) after the run.
Configuration
The target block carries a discriminant, type. On the iOS target:
| Option | Type | Required | Description |
|---|---|---|---|
type | "ios" | yes | Selects the iOS simulator target. |
app | string | yes | A bundle id (e.g. com.example.App) or a path to a built .app to install. |
udid | string | no | The simulator UDID. Required only when several simulators are booted — selection otherwise fails with an error listing the candidates. |
coldStart | boolean | no (default false) | When true, uninstall + reinstall before launch for a deterministic start. Requires the .app path (a bare bundle id cannot be reinstalled). |
url is not accepted on this target. Omitting type still selects the web target, so existing web configs are unchanged.
.app path vs bundle idA bare target.app ending in .app is treated as a bundle id unless a directory of that name exists on disk, so bundle ids like com.company.app are not mistaken for paths.
guardrails.allowedApps
allowedApps is the native-scope analog of allowedDomains — it restricts which app a hunt may drive.
guardrails:
allowedApps:
- "com.example.App" # a bundle id…
- "/abs/path/to/App.app" # …or a .app path
- An empty or omitted
allowedAppslist leaves the scope unset, and the target app is implicitly allowed — mirroring the wayallowedDomainsauto-includes the web target's own host. - Entries are iOS bundle ids or
.apppaths. A.apppath is authorized by its path, its bundle name, or the bundle id read from the bundle's rootInfo.plist. forbiddenSelectorsstill applies, using the same case-sensitive substring semantics as the web target.
prowl login and the allowedDomains / URL guardrails do not apply on the iOS target.
Selector dialect
Native selectors address accessibility ids, labels, visible text, and element type. Semantics match the macOS and Android targets, so a selector means the same thing across native targets. Prefer id= (set accessibilityIdentifier in your app — the native analog of data-testid).
| Selector | Matches |
|---|---|
id=save | element whose accessibility id (accessibilityIdentifier) is save |
label="Submit" | element whose accessibilityLabel equals Submit (exact — compiles to a label == NSPredicate) |
role=XCUIElementTypeButton | element of that type (shorthand role=Button works too) |
role=Button[name="Save"] | that type whose visible label/value contains Save |
text="Save" or bare Save | element whose label or value contains the text (substring) |
:focus | the element with keyboard focus (hasKeyboardFocus == 1) |
Text/label/role+name selectors compile to WDA NSPredicate strings (quotes and backslashes are escaped). forbiddenSelectors still applies on this target.
Step compatibility
Portable steps run on the iOS target; web-only steps in the top-level hunt are rejected up front at validation time — before anything launches — with a clear, iOS-labelled error. A runHunt step validates its referenced hunt when that step executes, before the nested hunt starts.
| Portable (iOS) | Not supported on iOS |
|---|---|
click, fill, type, press | navigate, waitForUrl, waitForNetworkIdle |
wait, waitForSelector | mockRoute / unmockRoute, evalScript, runScript |
assert: visible / notVisible | onDialog, select / selectOption, setInputFiles |
screenshot, assertScreenshot | waitForDownload, scroll, assert: urlIncludes / urlEquals |
repeat, if, runHunt, copyText | hover, scrollTo (no touch equivalent yet) |
The web-only rejection names the offending step, for example:
Step "navigate" is not supported by the iOS target. It is web-only;
use a portable step (click, fill, type, press, wait, assert visible, screenshot, etc.).
Notes:
typeandfillset text on the focused / matched field via WDA'selement/value.presssupports a small, honest key set and rejects other keys with the supported-keys message:enter/returnanddelete/backspace/del— sent through WDA's key endpoint to the focused element;home— returns to the springboard.
- Screenshots are captured with
simctl(not WDA), soscreenshot/assertScreenshotartifacts still work even if the agent wedges. hoverandscrollTohave no touch equivalent yet and are rejected with a clear message; scroll-gesture support is a follow-up. (On the macOS target these two are portable — the rejection is specific to touch targets.)- URL assertions (
urlIncludes/urlEquals) are web-only; use inlineassert: visible/notVisiblesteps for checks on this target. - Hunt-level
assertions:blocks are rejected before launch on this target; use inlineassert: visible/assert: notVisiblesteps for native UI checks.
Worked example
Open the iOS Settings app, assert a known row is visible, and screenshot it:
# .prowl/config.yml
target:
type: ios
app: "com.apple.Preferences"
guardrails:
allowedApps:
- "com.apple.Preferences"
# .prowl/hunts/settings-smoke.yml
name: settings-smoke
steps:
- waitForSelector:
selector: label="General"
timeout: 10000
- assert:
visible: label="General"
- screenshot: settings-home
prowl run settings-smoke