iOS Simulator + Appium lifecycle for the application provider.
The provider binds XCUITest to an installed application by bundle id (or installs a simulator-built .app supplied by path). It normally starts in native context; callers can enter an inspectable WKWebView temporarily to use DOM, CSS, and JavaScript operations.
Responsibilities:
xcrun simctl list devices --jsonUnit tests inject a command runner via *command-runner* so no real
xcrun process is required.
iOS Simulator + Appium lifecycle for the application provider. The provider binds XCUITest to an installed application by bundle id (or installs a simulator-built .app supplied by path). It normally starts in native context; callers can enter an inspectable WKWebView temporarily to use DOM, CSS, and JavaScript operations. Responsibilities: - Validate platform prerequisites (macOS, xcrun/simctl, Appium + XCUITest) - Discover available simulators via `xcrun simctl list devices --json` - Select one deterministically (udid > exact name > newest iPhone) - Acquire a per-device (UDID) lock so unrelated spel sessions never share a simulator - Boot and wait for the simulator - Start (or connect to) Appium on per-session loopback ports - Create an XCUITest application WebDriver session - Track ownership of processes/simulator state and close ONLY owned resources (never a user's Appium or an already-booted simulator) Unit tests inject a command runner via `*command-runner*` so no real `xcrun` process is required.
Command runner fn: (fn [cmd-vector]) -> {:exit :out :err}. Rebind in tests to avoid executing real xcrun/simctl processes.
Command runner fn: (fn [cmd-vector]) -> {:exit :out :err}.
Rebind in tests to avoid executing real xcrun/simctl processes.(acquire-lock! udid session)Acquires the per-UDID simulator lock for session.
Returns the lock map on success.
Acquires the per-UDID simulator lock for `session`. - No lock, or a stale lock whose owning PID is dead → acquire. - A live lock owned by the SAME session → keep (re-entrant). - A live lock owned by another session → throws ex-info. Never steals. Returns the lock map on success.
(activate-app! {:keys [webdriver context* native-refs*] :as ios-session}
bundle-id)Activates an installed application by bundle identifier without creating a new session. The session returns to NATIVE_APP so stale webview state is never carried across applications. Returns the bundle identifier.
Activates an installed application by bundle identifier without creating a new session. The session returns to NATIVE_APP so stale webview state is never carried across applications. Returns the bundle identifier.
(app-capabilities {:keys [device-name udid wda-port platform-version bundle-id
app auto-webview extra]})Builds W3C-compliant XCUITest application capabilities.
Unlike a Safari session, this intentionally omits browserName. Supply
either :bundle-id to bind to an installed app or :app to install and
launch a simulator-built .app. With neither target, XCUITest starts on the Home
screen so the caller can activate an app later.
The non-destructive defaults preserve an already-running app when binding: no reset, no forced relaunch, and no termination when the session closes.
Params:
opts - Map:
:device-name - String. Simulator name.
:udid - String. Simulator UDID.
:wda-port - Long. WebDriverAgent local port.
:platform-version - String, optional.
:bundle-id - String, optional. Installed app bundle identifier.
:app - String, optional. Absolute simulator .app path or URL.
:auto-webview - Boolean. Enter the first inspectable webview.
:extra - Map, optional. Merged last.
Builds W3C-compliant XCUITest application capabilities. Unlike a Safari session, this intentionally omits `browserName`. Supply either `:bundle-id` to bind to an installed app or `:app` to install and launch a simulator-built .app. With neither target, XCUITest starts on the Home screen so the caller can activate an app later. The non-destructive defaults preserve an already-running app when binding: no reset, no forced relaunch, and no termination when the session closes. Params: `opts` - Map: :device-name - String. Simulator name. :udid - String. Simulator UDID. :wda-port - Long. WebDriverAgent local port. :platform-version - String, optional. :bundle-id - String, optional. Installed app bundle identifier. :app - String, optional. Absolute simulator .app path or URL. :auto-webview - Boolean. Enter the first inspectable webview. :extra - Map, optional. Merged last.
(app-installed? {:keys [webdriver]} bundle-id)Returns true when a bundle identifier is installed on the simulator.
Returns true when a bundle identifier is installed on the simulator.
(app-state {:keys [webdriver]} bundle-id)Returns a descriptive application-state keyword for a bundle identifier.
Returns a descriptive application-state keyword for a bundle identifier.
Appium numeric application-state values mapped to descriptive keywords.
Appium numeric application-state values mapped to descriptive keywords.
(background-app! {:keys [webdriver]} seconds)Moves the active application to the background for seconds.
Moves the active application to the background for `seconds`.
(boot-device! {:keys [udid state]})Boots the simulator when not already booted and waits for boot completion.
Returns {:booted-by-spel? boolean} — true only when spel initiated the boot (ownership drives shutdown policy).
Boots the simulator when not already booted and waits for boot completion.
Returns {:booted-by-spel? boolean} — true only when spel initiated the
boot (ownership drives shutdown policy).(context-details {:keys [webdriver]})Returns detailed native/webview context records. Webview records include URL, title, and bundle metadata when XCUITest can inspect them.
Returns detailed native/webview context records. Webview records include URL, title, and bundle metadata when XCUITest can inspect them.
(contexts {:keys [webdriver]})Returns the available automation contexts for an iOS session. The result always contains NATIVE_APP and may contain inspectable WEBVIEW_* entries.
Returns the available automation contexts for an iOS session. The result always contains NATIVE_APP and may contain inspectable WEBVIEW_* entries.
(current-context ios-session)Returns the active automation context name.
Returns the active automation context name.
(doctor)Runs iOS provider diagnostics. Returns a vector of check maps: {:check string :ok boolean :detail string?}.
Does NOT install anything — reports actionable state only.
Runs iOS provider diagnostics. Returns a vector of check maps:
{:check string :ok boolean :detail string?}.
Does NOT install anything — reports actionable state only.(doctor-ok? checks)Returns true when every doctor check passes.
Returns true when every doctor check passes.
(element-attribute {:keys [webdriver] :as ios-session} selector attribute)Returns an element attribute. Native attributes include name, label, value, type, visible, enabled, selected, rect, and accessibilityContainer.
Returns an element attribute. Native attributes include name, label, value, type, visible, enabled, selected, rect, and accessibilityContainer.
(element-count ios-session selector)Returns the number of matching native/webview elements.
Returns the number of matching native/webview elements.
(element-rect {:keys [webdriver] :as ios-session} selector)Returns an element rectangle.
Returns an element rectangle.
(element-state {:keys [webdriver] :as ios-session} selector)Returns displayed, enabled, selected, and editable state for an element.
Returns displayed, enabled, selected, and editable state for an element.
(element-text {:keys [webdriver] :as ios-session} selector)Returns element text/accessibility label.
Returns element text/accessibility label.
(find-element {:keys [webdriver] :as ios-session} selector)Finds one native/webview element using selectors or @refs.
Finds one native/webview element using selectors or @refs.
(find-elements {:keys [webdriver] :as ios-session} selector)Finds all matching native/webview elements.
Finds all matching native/webview elements.
(find-free-port)Allocates a free loopback TCP port via bind(0).
Allocates a free loopback TCP port via bind(0).
(get-permission ios-session service)(get-permission {:keys [webdriver] :as ios-session} bundle-id service)Returns the current permission value for an application service.
Returns the current permission value for an application service.
(hide-keyboard! {:keys [webdriver]})Dismisses the iOS software keyboard.
Dismisses the iOS software keyboard.
(install-app! {:keys [webdriver]} app-path)Installs a simulator-compatible .app path through Appium.
Installs a simulator-compatible .app path through Appium.
(kill-process-tree! proc)Destroys a process and ALL of its descendants. Best-effort: TERM everything, wait briefly, then KILL survivors.
A plain Process.destroy on Appium leaves its xcodebuild/WebDriverAgent children running — a surviving WDA startup from a FAILED attempt later kills any newer WDA on the same UDID, breaking the retry's native gestures (mobile: tap / swipe) while web-context commands keep working.
Destroys a process and ALL of its descendants. Best-effort: TERM everything, wait briefly, then KILL survivors. A plain Process.destroy on Appium leaves its xcodebuild/WebDriverAgent children running — a surviving WDA startup from a FAILED attempt later kills any newer WDA on the same UDID, breaking the retry's native gestures (mobile: tap / swipe) while web-context commands keep working.
(launch-app! ios-session)(launch-app! ios-session bundle-id)(launch-app! {:keys [webdriver] :as ios-session} bundle-id opts)Launches the session-bound or requested installed application.
Launches the session-bound or requested installed application.
(list-devices)Lists available iOS Simulators via simctl. Returns vector of IosDevice.
Lists available iOS Simulators via simctl. Returns vector of IosDevice.
(lock-path udid)Returns the per-UDID simulator lock file path.
Returns the per-UDID simulator lock file path.
(macos?)Returns true when running on macOS.
Returns true when running on macOS.
(native-context? {:keys [context*]})Returns true when the iOS session is in NATIVE_APP context.
Returns true when the iOS session is in NATIVE_APP context.
XCTest types that always receive native snapshot refs.
XCTest types that always receive native snapshot refs.
(native-snapshot {:keys [webdriver native-refs*]})Captures and compacts the current XCTest hierarchy, updating native refs.
Captures and compacts the current XCTest hierarchy, updating native refs.
(native-snapshot-from-xml source)Converts Appium's XCTest XML source into a compact semantic tree with snapshot-scoped native refs. Each ref stores an XPath locator.
Returns {:tree :refs :counter :native true :context "NATIVE_APP"}.
Converts Appium's XCTest XML source into a compact semantic tree with
snapshot-scoped native refs. Each ref stores an XPath locator.
Returns {:tree :refs :counter :native true :context "NATIVE_APP"}.XCTest element types mapped to compact semantic snapshot roles.
XCTest element types mapped to compact semantic snapshot roles.
(open-url! ios-session url)(open-url! {:keys [webdriver] :as ios-session} url bundle-id)Opens a deep or universal URL, optionally targeting a bundle identifier.
Opens a deep or universal URL, optionally targeting a bundle identifier.
(parse-devices json-str)Parses xcrun simctl list devices --json output into IosDevice records.
Only iOS runtimes are included; real devices are never in simctl output.
Parses `xcrun simctl list devices --json` output into IosDevice records. Only iOS runtimes are included; real devices are never in simctl output.
Friendly permission actions mapped to XCUITest access values.
Friendly permission actions mapped to XCUITest access values.
(press-key! {:keys [webdriver]} key)(press-key! {:keys [webdriver] :as ios-session} selector key)Presses a key on a selected or currently focused element.
Presses a key on a selected or currently focused element.
(read-lock udid)Reads a simulator lock file. Returns the lock map or nil.
Reads a simulator lock file. Returns the lock map or nil.
(release-lock! udid session)Releases the per-UDID lock, but only when owned by session.
Idempotent — missing or foreign locks are left untouched.
Releases the per-UDID lock, but only when owned by `session`. Idempotent — missing or foreign locks are left untouched.
(run-command cmd)Default command runner. Executes cmd (vector of strings) and returns
{:exit long :out string :err string}. Blocks until the process exits.
Default command runner. Executes `cmd` (vector of strings) and returns
{:exit long :out string :err string}. Blocks until the process exits.(scroll ios-session direction)(scroll ios-session direction amount)(scroll {:keys [webdriver] :as ios-session}
direction
amount
{:keys [selector smooth?]})Scrolls content in a semantic direction. Native contexts use a touch gesture (scroll down means finger swipe up); webview element scrolling uses DOM scrollBy. Options accept :selector and :smooth?.
Scrolls content in a semantic direction. Native contexts use a touch gesture (scroll down means finger swipe up); webview element scrolling uses DOM scrollBy. Options accept :selector and :smooth?.
(select-device devices {:keys [udid device platform-version]})Selects a simulator deterministically.
Params:
devices - Seq of IosDevice (from list-devices/parse-devices).
opts - Map:
:udid - String. Exact UDID match (wins over everything).
:device - String. Exact case-insensitive name match.
:platform-version - String. Narrows duplicate names.
Rules:
Returns an IosDevice. Throws ex-info when nothing matches.
Selects a simulator deterministically. Params: `devices` - Seq of IosDevice (from list-devices/parse-devices). `opts` - Map: :udid - String. Exact UDID match (wins over everything). :device - String. Exact case-insensitive name match. :platform-version - String. Narrows duplicate names. Rules: 1. :udid exact match wins. 2. :device prefers an exact case-insensitive name. 3. :platform-version narrows duplicates. 4. Remaining duplicates → ex-info listing matches (never guess). 5. No request → prefer an already Booted iPhone; else the newest available iPhone runtime (deterministic tiebreak by name). Returns an IosDevice. Throws ex-info when nothing matches.
(selector-locator {:keys [native-refs*] :as ios-session} selector)Resolves a native/web selector or snapshot ref to WebDriver strategy data.
Resolves a native/web selector or snapshot ref to WebDriver strategy data.
(set-permission! ios-session service access)(set-permission! {:keys [webdriver] :as ios-session} bundle-id service access)Sets, revokes, or resets an application permission.
access accepts :grant/:revoke/:reset and XCUITest access values.
Sets, revokes, or resets an application permission. `access` accepts :grant/:revoke/:reset and XCUITest access values.
Human-facing setup instructions for the iOS provider.
Human-facing setup instructions for the iOS provider.
(shutdown-device! udid)Shuts down a simulator by UDID. Best-effort.
Shuts down a simulator by UDID. Best-effort.
(start! {:keys [session device udid platform-version bundle-id app auto-webview
extra-capabilities appium-url startup-timeout-ms
session-timeout-ms]
:as _opts})Starts a full iOS application session: select → lock → boot → Appium → WebDriver session.
Params:
opts - Map:
:session - String. Named spel session (required for locking).
:device - String, optional. Simulator name.
:udid - String, optional. Simulator UDID.
:platform-version - String, optional.
:bundle-id - String, optional. Bind an installed application.
:app - String, optional. Install/bind a simulator .app.
:auto-webview - Boolean. Enter the first inspectable WKWebView.
:extra-capabilities - Map, optional. Advanced Appium capabilities.
:appium-url - String, optional. Connect to an EXTERNAL Appium —
spel will never kill it.
:startup-timeout-ms - Long, optional. Appium startup timeout.
:session-timeout-ms - Long, optional. WebDriver session-creation timeout.
Returns an IosSession record.
Starts a full iOS application session: select → lock → boot → Appium →
WebDriver session.
Params:
`opts` - Map:
:session - String. Named spel session (required for locking).
:device - String, optional. Simulator name.
:udid - String, optional. Simulator UDID.
:platform-version - String, optional.
:bundle-id - String, optional. Bind an installed application.
:app - String, optional. Install/bind a simulator .app.
:auto-webview - Boolean. Enter the first inspectable WKWebView.
:extra-capabilities - Map, optional. Advanced Appium capabilities.
:appium-url - String, optional. Connect to an EXTERNAL Appium —
spel will never kill it.
:startup-timeout-ms - Long, optional. Appium startup timeout.
:session-timeout-ms - Long, optional. WebDriver session-creation timeout.
Returns an IosSession record.(start-appium! {:keys [session port startup-timeout-ms]})Starts the installed appium executable on a free loopback port.
npx appium (implicit network install).--relaxed-security.:startup-timeout-ms expires.Params:
opts - Map:
:session - String. Named spel session (log naming).
:port - Long, optional. Defaults to a free port.
:startup-timeout-ms - Long, default 30000.
Returns {:process Process :port long :url string :log-path string}. Throws ex-info when Appium fails to become ready (process is killed).
Starts the installed `appium` executable on a free loopback port.
- Never runs `npx appium` (implicit network install).
- Never passes `--relaxed-security`.
- Output is redirected to the per-session Appium log file (avoids pipe
backpressure blocking a verbose process).
- Polls GET /status until ready or `:startup-timeout-ms` expires.
Params:
`opts` - Map:
:session - String. Named spel session (log naming).
:port - Long, optional. Defaults to a free port.
:startup-timeout-ms - Long, default 30000.
Returns {:process Process :port long :url string :log-path string}.
Throws ex-info when Appium fails to become ready (process is killed).(stop! ios-session)(stop! {:keys [device webdriver appium-process appium-owned?
simulator-booted-by-spel? session-name]
:as ios-session}
{:keys [shutdown-simulator?]})Stops an iOS session idempotently, releasing ONLY owned resources:
shutdown-simulator? is requested
AND spel booted it.Safe to call multiple times and from shutdown hooks.
Stops an iOS session idempotently, releasing ONLY owned resources: 1. Delete the WebDriver session. 2. Kill the ENTIRE Appium process tree (incl. xcodebuild/WDA children) only when :appium-owned? is true (never external). 3. Release the simulator lock. 4. Shut down the simulator only when `shutdown-simulator?` is requested AND spel booted it. Safe to call multiple times and from shutdown hooks.
(swipe {:keys [webdriver] :as ios-session}
{:keys [direction distance from to duration]})Performs a low-level native touch swipe on an iOS session.
Two forms: (swipe session {:direction :up :distance 500 :duration 800}) (swipe session {:from [200 600] :to [200 100] :duration 800})
Direction-based swipes derive coordinates from the live web viewport.
Performs a low-level native touch swipe on an iOS session.
Two forms:
(swipe session {:direction :up :distance 500 :duration 800})
(swipe session {:from [200 600] :to [200 100] :duration 800})
Direction-based swipes derive coordinates from the live web viewport.(swipe-coordinates direction distance {:keys [width height]})Derives safe swipe start/end coordinates for a direction inside a viewport. Keeps the gesture inside the middle band of the screen so it works on iPads and landscape viewports.
Params:
direction - Keyword. :up, :down, :left, or :right.
distance - Long. Swipe distance in px (clamped to viewport).
viewport - Map. {:width w :height h}.
Returns {:from [x y] :to [x y]}.
Derives safe swipe start/end coordinates for a direction inside a
viewport. Keeps the gesture inside the middle band of the screen so it
works on iPads and landscape viewports.
Params:
`direction` - Keyword. :up, :down, :left, or :right.
`distance` - Long. Swipe distance in px (clamped to viewport).
`viewport` - Map. {:width w :height h}.
Returns {:from [x y] :to [x y]}.(terminate-app! ios-session)(terminate-app! {:keys [webdriver] :as ios-session} bundle-id)Terminates the session-bound or requested application.
Terminates the session-bound or requested application.
(type-element! {:keys [webdriver] :as ios-session} selector text)Types text into an element without clearing its existing value.
Types text into an element without clearing its existing value.
(type-keys! {:keys [webdriver]} text)Types text into the currently focused element.
Types text into the currently focused element.
(uninstall-app! {:keys [webdriver]} bundle-id)Uninstalls an application by bundle identifier.
Uninstalls an application by bundle identifier.
(use-context! ios-session requested)(use-context! {:keys [webdriver] :as ios-session}
requested
{:keys [timeout-ms]})Switches to a native or webview automation context.
requested may be :native, :webview (first available webview), or an
exact context name. Waits up to :timeout-ms (default 10000) for a
matching webview to appear. Returns the selected context name. Does not
issue a WebDriver switch when the requested context is already active.
Switches to a native or webview automation context. `requested` may be :native, :webview (first available webview), or an exact context name. Waits up to `:timeout-ms` (default 10000) for a matching webview to appear. Returns the selected context name. Does not issue a WebDriver switch when the requested context is already active.
(viewport-size wd-session)Returns {:width :height} of the web viewport via JavaScript evaluation.
Returns {:width :height} of the web viewport via JavaScript evaluation.
(wait-for-element ios-session selector)(wait-for-element ios-session selector {:keys [timeout-ms interval-ms]})Waits for a native/webview selector or ref and returns its element id.
Waits for a native/webview selector or ref and returns its element id.
(with-context ios-session requested callback)(with-context ios-session requested opts callback)Runs callback in a requested Appium context and restores the exact prior context before returning. The complete switch/callback/restore sequence is serialized by the session operation lock.
requested accepts the same aliases and exact names as use-context!.
Options currently support :timeout-ms. Callback failures remain primary
when restoration also fails; the restoration error is attached as a
suppressed exception.
Runs callback in a requested Appium context and restores the exact prior context before returning. The complete switch/callback/restore sequence is serialized by the session operation lock. `requested` accepts the same aliases and exact names as `use-context!`. Options currently support `:timeout-ms`. Callback failures remain primary when restoration also fails; the restoration error is attached as a suppressed exception.
(with-operation ios-session callback)Runs callback while holding the iOS session's reentrant operation lock. Sessions created before operation locking (and lightweight test sessions) may omit the lock; those callbacks execute directly.
Runs callback while holding the iOS session's reentrant operation lock. Sessions created before operation locking (and lightweight test sessions) may omit the lock; those callbacks execute directly.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |