# Create folder Source: https://docs.mobileboost.io/api-reference/create-folder /openapi.json post /tests/folders Create a folder. Pass `parentId` to nest it inside another folder, or leave it out for a top-level one. Folder names are for people to read: two folders may share a name, and everything that refers to a folder uses its id. # Create test Source: https://docs.mobileboost.io/api-reference/create-test /openapi.json post /tests Create a new test case with natural-language commands. # Delete folder Source: https://docs.mobileboost.io/api-reference/delete-folder /openapi.json delete /tests/folders Delete a folder. **No test is deleted**: the folder's subfolders move up to its own parent and its tests become unfiled, and the response says which. To delete the tests too, call `DELETE /tests` with their ids first. # Delete test Source: https://docs.mobileboost.io/api-reference/delete-test /openapi.json delete /tests Delete one or more test cases by ID. # Execute tests Source: https://docs.mobileboost.io/api-reference/execute-tests /openapi.json post /tests/execute Start a run of your tests on MobileBoost devices. Each test runs the automation code MobileBoost wrote and verified for it (see `POST /tests/generate`). The call returns as soon as the run is created. Take `runId` from the response and pass it to `GET /runs/{runId}` to follow the run until its `status` is `completed` or `cancelled`. **Choose the tests** with `testIds`, `tags`, or both. A test is runnable on the run's platform once its automation has been written and verified, which `GET /tests` shows as `platforms..runnable`. - A test you name in `testIds` must be runnable. If it is not, the request is refused with 400. - A test that only matches `tags` and is not runnable yet is skipped, and the rest of the run starts. The response lists each skipped test in `skippedTests` with the reason, and `warnings` says how many were left out. If none of the tests matching `tags` is runnable, the request is refused with 400 and the message lists them. Skipped tests are not part of the run: they are not counted in `totalTests` and never change the run's `status`. **Name the app** with one of: - `buildId`: the `buildId` that `POST /uploadBuild` returned. - `build.meta.name`: the newest build uploaded under that name. Add `platform` if you upload your iOS and Android apps under the same name. - `bundleId` and `platform`: an app that is already installed on your reserved device, with no build uploaded. This has to be enabled for your organisation. **Choose the device** with `usePhysicalDevice`. MobileBoost picks the device itself, so `deviceConfigs`, `deviceProviderSettings`, `launchParams`, `testInputs`, `tagsQuery`, `iterations`, `metaData`, `stealthMode`, `dependencyTestIds` and the `ephemeralTest*` fields are not supported. A request that sends any of them is refused with 400 rather than run on a device you did not ask for. **Pass values to your tests** with `variables`: `NAME: value` pairs, such as the backend a canary test signs in against. Each test in the run reads them as environment variables, and a value here wins over your organisation's environment variable of the same name for this run only. `variables` replaces `testInputs`. It is not for passwords or keys: the values are kept with the run for 14 days, so keep credentials in your organisation's environment variables (ENV Variables in the web app). **Authenticate** with your API key: `Authorization: Bearer mb_live_...`. `organisationId` must be the organisation the key belongs to. A request that sends `variables` or `tunnelName` needs the key even if your organisation does not otherwise require one. # Generate test automation Source: https://docs.mobileboost.io/api-reference/generate-test-automation /openapi.json post /tests/generate Write the automation code for a test that has a description but no working test file yet. Explores the app on a device, writes the test, verifies it by running it several times in a row, and commits it to the organisation's test repository, after which the test is runnable via `POST /tests/execute`. By default this targets the newest build uploaded for every platform the organisation has one for, writing the platforms one after the other: the first creates the file, the next extends it with its own selectors. Pass `uploadId`, `bundleId`, or `platform` to narrow that. Generation is fire-and-forget: it queues on the organisation's existing device slots and the response returns as soon as the work is dispatched, not once it finishes; follow it under `platforms` on `GET /tests`. **Retrying one platform.** When `GET /tests` shows a test `ready` on one platform and `failed` or `blocked` on the other, call this with `platform` set to the one that needs writing again. The existing file is kept and extended for that platform. Both platforms share one file, so a test has one agent at a time: a request while either platform is still `generating` is refused with 409, naming the busy platform. # Get run status Source: https://docs.mobileboost.io/api-reference/get-run-status /openapi.json get /runs/{runId} Get the status and per-test results of a run. Pass the `runId` that `POST /tests/execute` returned. Poll until `status` is `completed` or `cancelled`. Each test is then in `succeededTests`, `failedTests` or `blockedTests`, unless it was cancelled. While the run is in progress, `runningTests` lists the tests that are on a device and `queuedTests` the ones waiting for one. A test that is being set up, or re-checked after a failure, is in none of the lists for that time, so the lists can add up to less than `totalTests`. `skippedTests` lists the tests that matched `tags` when the run was started but were not runnable on its platform yet, so they were left out. They are not counted in `totalTests` and never change the run's `status`. **When to start polling.** `POST /tests/execute` returns just before the run is saved. The run is usually saved within half a second, so wait 2 seconds before your first call. If it answers 404, retry every 2 seconds. A 404 that lasts longer than 30 seconds means the run was not created: check the `runId`, then start the run again. While `status` is `initial`, tests are still being added to the run, so `totalTests` can still grow. This takes under a second for a run of a few tests, and up to about 10 seconds for a run of 100 tests. Once the run is `running`, polling every 15 to 30 seconds is enough. The organisation is taken from your API key. A run that belongs to another organisation answers 404, the same as a run that does not exist. # Get SDK session usage Source: https://docs.mobileboost.io/api-reference/get-sdk-session-usage /openapi.json get /usage/sdk-sessions Retrieve how many AI steps your organisation's SDK sessions used over the last `days` days, grouped by the test ID each session was started with. These are the numbers the **Usage by test** table on the usage dashboard shows. Each session's steps are counted from its step log: - An **AI step** is a step the AI model had to decide. - A **cached step** was replayed from the cache instead. - Steps your own code performed are free and count as neither. The cache hit rate is `cachedSteps / (cachedSteps + aiSteps)`, as a percentage. A test ID is the value your SDK passed when it started the session, returned exactly as it was recorded. Sessions started by a MobileBoost Platform test run carry that test's ID. Sessions started without a test ID share one row, with `testId` set to `null`. By default, sessions started with any of your API keys are included. Pass `apiKeyId` to see only the sessions one key started. Any API key of your organisation can call this endpoint, including read-only `audit` keys. The organisation is taken from the API key. Results can be up to one minute old. These numbers won't add up to the monthly allowance on the usage dashboard, because the two measure different things: - **Monthly allowance:** counts AI model calls from all your runs in the calendar month, including runs that don't go through an SDK session. Vision and non-vision calls are weighted by the step factors in your plan. - **This endpoint:** counts AI steps of SDK sessions only, unweighted, over the last `days` days. A session counts in the window it started in. An AI step usually takes one model call, but not always, so even for SDK sessions alone the two can differ. # Get tests Source: https://docs.mobileboost.io/api-reference/get-tests /openapi.json get /tests Retrieve test cases for your organisation. Filter by test ID, title, tags, or folder. On the Platform test library, each test also reports its generated automation: the file, the rolled-up `automationState`, and under `platforms` the state on iOS and on Android separately, so a client can tell that a test is ready on one platform and failed on the other, and retry only that platform with `POST /tests/generate`. # List audit logs Source: https://docs.mobileboost.io/api-reference/list-audit-logs /openapi.json get /audit/logs Retrieve your organisation’s audit log events as a paginated, cursor-based stream. This endpoint is intended for SIEM ingestion, such as a Splunk REST or modular input. Poll with `since=` from the previous response to fetch only new events. Audit logging must be enabled for your organisation. If audit logging is not enabled, the endpoint returns `403`. The organisation is derived from the API key which can be created in your organisations account. Set the `Accept` header to `application/x-ndjson` to receive newline-delimited JSON instead of a single JSON object. In this mode, the next cursor is returned in the `X-Next-Cursor` response header rather than in a response field. # List folders Source: https://docs.mobileboost.io/api-reference/list-folders /openapi.json get /tests/folders List the folders your tests are organised into. Each folder carries its full path (`Checkout / Payments`) and how many tests are in it, counting its subfolders, so one call is enough to find the folder you want. `unfiledTestCount` is how many tests are in no folder at all. # Move tests into a folder Source: https://docs.mobileboost.io/api-reference/move-tests-into-a-folder /openapi.json post /tests/folders/move File several tests into a folder at once, or unfile them with `folderId: null`. Ids that no longer exist are skipped rather than failing the call, and a test already in that folder is left alone. To move a single test, `PATCH /tests` takes `folderId` too. # Rename or move folder Source: https://docs.mobileboost.io/api-reference/rename-or-move-folder /openapi.json patch /tests/folders Rename a folder, move it under another folder, or both in one call. `parentId: null` moves it back to the top level; omitting `parentId` leaves it where it is. Moving a folder takes its subfolders and its tests with it. Both halves are applied together, so a refused move never leaves the rename it came with applied. # Update test Source: https://docs.mobileboost.io/api-reference/update-test /openapi.json patch /tests Update an existing test. Send `testId` plus the fields you want to change; anything you omit is left as it is. Only a change to `commands` re-triggers automation generation, because only that changes what the test should do. Renaming a test, re-tagging it or moving it to another folder leaves the generated automation alone, and the response says so with `automation.requested: false`. # Upload build Source: https://docs.mobileboost.io/api-reference/upload-build /openapi.json post /uploadBuild Upload an application build artifact to use in test executions. Accepted files: - **Android** - `.apk`, for both emulators and physical devices. - **iOS, physical devices** - `.ipa`. - **iOS, simulators** - a `.zip` or `.tar.gz` of the simulator `.app` bundle. An `.ipa` cannot be installed on a simulator. A `.zip` or `.tar.gz` holding a single `.ipa` is also accepted: it is unwrapped and treated as that `.ipa`, so a CI artifact can be uploaded as it comes. Any other extension is rejected with a 400. # Espresso Source: https://docs.mobileboost.io/api/espresso-packaging Package your compiled Espresso test suite and execute it on the MobileBoost device grid This guide covers how to build and package your compiled Espresso instrumentation suite so it runs correctly on the MobileBoost device grid, and how to trigger and monitor runs via the API. Unlike iOS/XCUITest, Android does not split builds into a physical vs. virtual "slice" - the same pair of APKs runs on both real devices and emulators, as long as the APKs contain the right ABIs (see [ABI coverage](#2-abi-coverage-physical-vs-virtual)). > [!NOTE] > > You do not need to solve signing for our devices. Our grid re-signs both the app under test and the test APK with a common certificate before installing, so instrumentation always runs with matching signatures. All API endpoints below authenticate with your `mb_live_` API key passed as a Bearer token in the `Authorization` header. ## Prerequisites * Android SDK / Gradle wrapper (the project's own `./gradlew` is all you need). * Your project has an `androidTest` source set with Espresso tests and a `testInstrumentationRunner` configured (usually `androidx.test.runner.AndroidJUnitRunner` or a subclass). * You know three identifiers - you'll need them at upload time: * **Application ID** of the app under test (must match the APK you upload) * **Test application ID** (usually `.test`) * **Instrumentation runner class** (from `defaultConfig.testInstrumentationRunner`) ## 1. Build the two APKs Espresso runs need exactly two artifacts, built from the same variant: | Artifact | Gradle task | Output | | - | - | - | | App under test | `assemble` | `app/build/outputs/apk//app-.apk` | | Test APK | `assembleAndroidTest` | `app/build/outputs/apk/androidTest//app--androidTest.apk` | Build both in one go: ```bash theme={null} ./gradlew assembleDebug assembleDebugAndroidTest ``` Replace `Debug` with the variant you test against (e.g. `assembleStagingDebug assembleStagingDebugAndroidTest` for a `stagingDebug` variant). > [!WARNING] > > Both APKs must come from the same variant of the same build - the test APK's `targetPackage` is baked in at compile time and must match the application ID of the app APK you upload. Notes: * Upload **APKs, not AABs**. If your CI produces an `.aab`, either build the APK variant directly or generate a universal APK with `bundletool build-apks --mode=universal`. * The app under test should be **debuggable** (`debuggable true`), which every `*Debug` variant is by default. Instrumenting a non-debuggable build relies on signature matching, which our re-signing handles, but a debuggable build also enables better failure artifacts (screenshots, hierarchy dumps). * If minification is enabled on the variant, make sure your ProGuard/R8 rules keep the classes your tests reference - a common source of `ClassNotFoundException` at instrumentation start. ## 2. ABI coverage (physical vs. virtual) There is no separate "simulator build" on Android, but ABI splits can bite you: | Target | Required ABI | | - | - | | Physical devices | `arm64-v8a` | | Virtual devices (emulators) | `x86_64` (our emulators also run arm64 translation, but native `x86_64` is faster and more reliable) | If your build uses `splits.abi` or per-ABI APKs, upload the APK that contains the ABI of the target device - or disable splits for the test build and upload a universal APK. An APK with pure Java/Kotlin code and no native libraries runs everywhere. Uploading an arm64-only APK for an emulator run is the Android equivalent of the iOS "wrong slice" mistake: it installs fine on a physical device but fails on x86\_64 emulators. ## 3. Verify the APKs before uploading ```bash theme={null} # Application ID of the app under test aapt dump badging app-debug.apk | grep package # Test APK: confirm its instrumentation target matches the app's application ID aapt dump xmltree app-debug-androidTest.apk AndroidManifest.xml | grep -A2 instrumentation # Confirm which ABIs are included (empty output = no native libs = runs everywhere) unzip -l app-debug.apk | grep 'lib/' ``` The `android:targetPackage` in the test APK's `` element must equal the `package` of the app APK. If they differ, the instrumentation will install but refuse to start - the single most common failure. ## 4. Signing - what you do and don't need to do You do not need to share keystores or match our certificates. We strip the signatures from both APKs and re-sign them with the same certificate before installing, so the app/test signature pair always matches. Building with the default debug keystore is fine. Because we re-sign, avoid features in the **test APK** that depend on your specific signing certificate (e.g. signature-level permissions against other apps of yours, Google API keys restricted to your release SHA-1). The app under test is re-signed too - if it verifies its own certificate at runtime (tamper detection, Play Integrity, Firebase App Check in enforced mode), disable that check in the variant you upload. ## 5. Optional: get your test list for sharding If you want to split tests across devices, we can shard automatically, or you can send us the test identifiers. Identifiers use `package.ClassName#testMethod` notation, e.g. `com.mycompany.app.LoginTests#testValidLogin`. Class-level (`com.mycompany.app.LoginTests`) and package-level (`com.mycompany.app.checkout`) identifiers are also accepted in filters. You can list them locally with: ```bash theme={null} adb shell am instrument -w -e log true \ / ``` (`-e log true` dry-runs the suite and logs test names without executing them.) ## 6. What to upload | File | Purpose | Upload endpoint | | - | - | - | | `app-.apk` | App under test | `POST /uploadBuild` | | `app--androidTest.apk` | Compiled Espresso test suite | `POST /app-automate/espresso/test-suite` | Each upload returns an ID (`app_id` / `test_suite_id`) that you reference when triggering a run. Uploads are reusable across runs - upload once per CI build, trigger as many runs as you like against the same pair. **Managing uploaded test suites** ``` GET /app-automate/espresso/test-suites # list recent uploads (id, name, upload time) DELETE /app-automate/espresso/test-suite/ # remove an upload permanently ``` ### Common mistakes checklist * Test APK built against a different variant/flavor than the uploaded app APK. → `targetPackage` mismatch; build both from the same variant in the same Gradle invocation. * Uploaded an `.aab` instead of an `.apk`. → Build the APK, or convert with `bundletool --mode=universal`. * ABI-split arm64 APK uploaded for an emulator run (or x86\_64 for a physical run). → Upload a universal APK or the matching ABI split. * R8/ProGuard stripped classes the tests reference. → Add keep rules or test against a non-minified variant. * App verifies its own signing certificate at runtime and detects our re-sign. → Disable tamper/integrity checks in the uploaded variant. * Ran `./gradlew connectedAndroidTest` looking for the test APK. → That task installs and runs locally; use `assembleAndroidTest` to produce the uploadable APK. ## 7. Pick target devices List the Android devices available to your organisation, with their exact names to use in the `devices` field of the build request: ``` GET /app-automate/espresso/devices ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/devices" ``` Device identifiers combine model and OS version, e.g. `Google Pixel 8-15.0` or `Samsung Galaxy S23-14.0`. Virtual devices (emulators) are listed alongside physical ones and flagged as such. ## 8. Trigger a test run Once both files are uploaded, start a run with the IDs returned by the two upload calls: the `app_id` from `POST /uploadBuild` and the `test_suite_id` from `POST /app-automate/espresso/test-suite`. **Endpoint** ``` POST /app-automate/espresso/build ``` **Minimal request** ```bash theme={null} curl -X POST "https://api.mobileboost.io/app-automate/espresso/build" \ -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "app": "", "testSuite": "", "devices": ["Google Pixel 8-15.0"] }' ``` **Required parameters** | Field | Description | | - | - | | `app` | The `app_id` returned by `POST /uploadBuild`. | | `testSuite` | The `test_suite_id` returned by `POST /app-automate/espresso/test-suite`. | | `devices` | List of device identifiers from `GET /app-automate/espresso/devices`. | **Optional parameters - test selection** These map directly onto `AndroidJUnitRunner` instrumentation arguments and apply to the whole run: | Field | Description | | - | - | | `class` | Run only these classes/methods. Values are `package.ClassName` or `package.ClassName#testMethod`. | | `package` | Run only tests in these Java packages. | | `annotation` / `skip-annotation` | Run or exclude tests carrying a given annotation (e.g. `com.mycompany.app.SmokeTest`). | | `size` | Run only tests annotated `@SmallTest` / `@MediumTest` / `@LargeTest` (`small`, `medium`, `large`). | **Optional parameters - execution control** | Field | Description | | - | - | | `shards` | Split the suite across devices and run in parallel (see below). | | `clearPackageData` | Clear the app's data between test classes (Test Orchestrator `clearPackageData`), isolating state between tests. | | `singleRunnerInvocation` | Run all tests in one instrumentation invocation instead of one per test - much faster for large suites, at the cost of per-test isolation. | | `instrumentationOptions` | Extra `-e key value` pairs passed verbatim to the instrumentation runner (custom runner arguments). | **Optional parameters - environment & reporting** | Field | Description | | - | - | | `project`, `buildTag` | Free-text labels for grouping and filtering runs. | | `gpsLocation` | Location applied to every device before tests start. | | `locale`, `language` | Set device locale/language before the run (e.g. `de_DE`). | | `video` | Record a video per session (default `true`). | | `deviceLogs` | Capture logcat for the run (default `true`). | | `networkLogs` | Capture the app's network traffic as a HAR file (default `false`). | | `coverage` | Collect Jacoco code coverage; download it per session after the run (default `false`). Requires the variant to be built with `testCoverageEnabled true`. | | `callbackURL` | We `POST` the final build status to this URL when the run reaches a terminal state - use instead of (or in addition to) polling. | **Full request with GPS and sharding** ```bash theme={null} curl -X POST "https://api.mobileboost.io/app-automate/espresso/build" \ -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "app": "", "testSuite": "", "devices": ["Google Pixel 8-15.0"], "project": "My Android App", "buildTag": "main-2026-07-15", "gpsLocation": { "latitude": 40.698979, "longitude": -73.9958661 }, "shards": { "numberOfShards": 2, "mapping": [ { "name": "shard-1", "strategy": "class", "values": ["com.mycompany.app.LoginTests", "com.mycompany.app.CheckoutTests#testAddToCart"] }, { "name": "shard-2", "strategy": "package", "values": ["com.mycompany.app.search"] } ] } }' ``` ### GPS location `gpsLocation` overrides the device's location for the whole run; it is applied on each device before your first test starts and cleared automatically at teardown. ```json theme={null} { "latitude": 40.698979, "longitude": -73.9958661, "altitude": 0.0 } ``` | Field | Required | Notes | | - | - | - | | `latitude` | yes | Decimal degrees. | | `longitude` | yes | Decimal degrees. | | `altitude` | no | Meters, default `0.0`. | If you omit `gpsLocation`, physical devices keep their real GPS and virtual devices use our neutral default. The mock location is injected at the platform level, so `FusedLocationProviderClient` and `LocationManager` both observe it - no changes to your app or tests needed. ### Sharding Sharding is optional. Provide a `shards` object to split the suite across devices and run them in parallel. **Auto-distribute** - give only `numberOfShards`; we spread the discovered tests evenly across that many shards: ```json theme={null} { "numberOfShards": 3 } ``` **Explicit test cases per shard** - provide a `mapping` array. Each entry names a shard and selects its tests with a `strategy` and a `values` list. `numberOfShards` must match the number of `mapping` entries. | Strategy | Values | | - | - | | `class` | `package.ClassName` or `package.ClassName#testMethod` identifiers. | | `package` | Java package names; the shard runs every test under them. | | `annotation` | Fully-qualified annotation class names. | ```json theme={null} { "numberOfShards": 2, "mapping": [ { "name": "shard-1", "strategy": "class", "values": ["com.mycompany.app.LoginTests"] }, { "name": "shard-2", "strategy": "package", "values": ["com.mycompany.app.search"] } ] } ``` **Response** ```json theme={null} { "message": "Success", "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3" } ``` Store the `build_id`; it identifies the run when you poll status and download results. ## 9. Check run status Poll the build with the `build_id` from the launch response (or skip polling entirely by passing `callbackURL` at trigger time). ``` GET /app-automate/espresso/build/ ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID" ``` The build carries an overall status plus one session per device/shard combination. Each session has its own `session_id` (used in the next section to download artifacts). ```json theme={null} { "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3", "status": "running", "sessions": [ { "session_id": "9f1c2a7b6e3d4f018a2b5c6d7e8f9012", "device": "Google Pixel 8-15.0", "shard": "shard-1", "status": "passed", "tests": { "total": 12, "passed": 12, "failed": 0, "skipped": 0 } }, { "session_id": "1a2b3c4d5e6f70819aabbccddeeff0011", "device": "Google Pixel 8-15.0", "shard": "shard-2", "status": "running", "tests": { "total": 8, "passed": 3, "failed": 0, "skipped": 0 } } ] } ``` Status values (both build and session): `queued`, `running`, `passed`, `failed`, `error` (infrastructure/setup problem, e.g. install or instrumentation-start failure), `timedout`. **Poll until terminal** ```bash theme={null} while :; do STATUS=$(curl -s -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID" | jq -r '.status') echo "status: $STATUS" case "$STATUS" in queued|running) sleep 10 ;; *) break ;; esac done ``` **Stop a running build** Abort a build early (e.g. a newer commit superseded it in CI). Sessions already finished keep their results; running sessions are terminated and marked as stopped. ``` POST /app-automate/espresso/build//stop ``` ```bash theme={null} curl -X POST -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/stop" ``` ## 10. Retrieve results and artifacts Once the build reaches a terminal status, fetch the machine-readable report at the build level and the raw artifacts per session. **Build-level test report** ``` GET /app-automate/espresso/build//report?format= ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/report?format=junit" \ -o results.xml ``` `junit` returns a standard JUnit XML your CI can ingest; `json` returns the same pass/fail data as structured JSON. **Session details and artifacts** Each session (one per device/shard, listed in the build status response) exposes its per-test results and download URLs for the raw artifacts: ``` GET /app-automate/espresso/build//session/ ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/session/$SESSION_ID" ``` ```json theme={null} { "session_id": "9f1c2a7b6e3d4f018a2b5c6d7e8f9012", "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3", "device": "Google Pixel 8-15.0", "shard": "shard-1", "status": "failed", "tests": { "total": 12, "passed": 11, "failed": 1, "skipped": 0 }, "testcases": [ { "class": "com.mycompany.app.LoginTests", "name": "testValidLogin", "status": "passed", "duration_seconds": 14.2 }, { "class": "com.mycompany.app.LoginTests", "name": "testInvalidPassword", "status": "failed", "duration_seconds": 31.7, "failure_message": "androidx.test.espresso.NoMatchingViewException: ..." } ], "artifacts": { "video_url": "https://...", "device_log_url": "https://...", "instrumentation_log_url": "https://...", "network_log_url": "https://..." } } ``` | Artifact | Contents | | - | - | | `video_url` | Screen recording of the session (present unless `video: false`). | | `device_log_url` | Full logcat for the session (present unless `deviceLogs: false`). | | `instrumentation_log_url` | Raw `am instrument` output - the first place to look when a session ends in `error` (instrumentation crashed or never started). | | `network_log_url` | HAR capture of the app's traffic (only when `networkLogs: true`). | Artifact URLs are pre-signed and expire; download promptly or re-fetch the session to get fresh ones. **Code coverage** If the build was triggered with `coverage: true`, download the Jacoco execution data per session and merge/report it with your local Jacoco tooling: ``` GET /app-automate/espresso/build//session//coverage ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/espresso/build/$BUILD_ID/session/$SESSION_ID/coverage" \ -o shard-1-coverage.ec ``` # Folders Source: https://docs.mobileboost.io/api/folders Organise your test library into folders, and file tests into them Folders group the tests in your library. They form a tree: every folder has a `parentId`, which is `null` at the top level, and a folder can hold both tests and other folders. A folder is a **label on a test, not a container that owns it**. A test belongs to exactly one folder (or none), and deleting a folder never deletes a test. > [!NOTE] > > Folders apply to the MobileBoost Platform test library. On a QA Studio organisation these endpoints answer `400`, and `folderId` is refused on `POST`/`PATCH /tests`. All requests below authenticate with your `mb_live_` API key passed as a Bearer token in the `Authorization` header. ## See the tree `GET /tests/folders` returns every folder with its full path and how many tests are in it, counting subfolders, so one call is enough to find the folder you want: ```bash theme={null} curl https://api.mobileboost.io/tests/folders \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json theme={null} { "folders": [ { "id": "kFq2b8s1TzYwm0Xd4eNc", "name": "Checkout", "parentId": null, "path": "Checkout", "testCount": 12 }, { "id": "p7Rw3nVa5LcQe1Zt9uHb", "name": "Payments", "parentId": "kFq2b8s1TzYwm0Xd4eNc", "path": "Checkout / Payments", "testCount": 4 } ], "unfiledTestCount": 3 } ``` `testCount` counts subfolders too: the four tests in **Payments** are part of the twelve in **Checkout**. ## Create and nest folders ```bash theme={null} curl -X POST https://api.mobileboost.io/tests/folders \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Payments", "parentId": "kFq2b8s1TzYwm0Xd4eNc" }' ``` Names are for people to read, up to 60 characters. Two folders may share a name — everything that refers to a folder uses its id. ## Rename and move `PATCH /tests/folders` renames a folder, moves it, or both in one call. `parentId: null` moves it back to the top level; omitting `parentId` leaves it where it is. ```bash theme={null} curl -X PATCH https://api.mobileboost.io/tests/folders \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "folderId": "p7Rw3nVa5LcQe1Zt9uHb", "name": "Card payments", "parentId": null }' ``` Moving a folder takes its subfolders and its tests with it. Moving one inside its own subtree is refused with a `400`, because that would detach the subtree from the tree. ## File tests into a folder **1. Create a test directly into one** ```bash theme={null} curl -X POST https://api.mobileboost.io/tests \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Pays with a saved card", "commands": ["Open the basket", "Pay with the saved card", "Verify the receipt"], "folderId": "p7Rw3nVa5LcQe1Zt9uHb" }' ``` **2. Move one test** ```bash theme={null} curl -X PATCH https://api.mobileboost.io/tests \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "testId": "xMIt964RUpAB95b6YI2F", "folderId": "p7Rw3nVa5LcQe1Zt9uHb" }' ``` `folderId: null` takes the test out of its folder. Omitting the field leaves the folder as it is. **3. Move many at once** ```bash theme={null} curl -X POST https://api.mobileboost.io/tests/folders/move \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "testIds": ["xMIt964RUpAB95b6YI2F", "b3Kd9pQw2ZxLm7Yt1Sof"], "folderId": "p7Rw3nVa5LcQe1Zt9uHb" }' ``` Ids that no longer exist come back under `skippedTestIds` rather than failing the call, and a test already in that folder comes back under `unchangedTestIds`. Changing a test's folder never re-triggers automation generation: it does not change what the test should do. ## List the tests in a folder ```bash theme={null} curl "https://api.mobileboost.io/tests?folderId=kFq2b8s1TzYwm0Xd4eNc&includeSubfolders=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Without `includeSubfolders`, you get only the tests filed directly in that folder. ## Delete a folder ```bash theme={null} curl -X DELETE https://api.mobileboost.io/tests/folders \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "folderId": "kFq2b8s1TzYwm0Xd4eNc" }' ``` Deleting a folder deletes **no test**. Its subfolders move up to its own parent, its tests become unfiled, and the response says exactly which: ```json theme={null} { "message": "Folder 'Checkout' was deleted. 1 subfolder(s) moved up, 3 test(s) unfiled. No test was deleted.", "folderId": "kFq2b8s1TzYwm0Xd4eNc", "promotedFolderIds": ["p7Rw3nVa5LcQe1Zt9uHb"], "unfiledTestIds": ["xMIt964RUpAB95b6YI2F"] } ``` To delete the tests as well, call `DELETE /tests` with their ids first. > [card] [Platform MCP server](/engineer-tooling/platform-mcp) > > The same folder operations as tools your coding agent can call. # Getting started Source: https://docs.mobileboost.io/api/getting-started Authenticate and make your first API request The MobileBoost API lets you manage tests, upload builds, trigger executions, and retrieve run results programmatically. ## Base URL All API requests use the following base URL: ``` https://api.mobileboost.io ``` ## Authentication Authenticate every request by including a Bearer token in the `Authorization` header: ```bash theme={null} curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.mobileboost.io/tests ``` Create an API key in the MobileBoost Platform under **Settings > API keys**. Keys start with `mb_live_`. ## Quick example Create a test, upload a build, run the test, and read the results: **1. Create a test** ```bash theme={null} curl -X POST https://api.mobileboost.io/tests \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Login flow", "commands": ["Tap Log in", "Enter credentials", "Verify home screen"] }' ``` **2. Upload a build** ```bash theme={null} curl -X POST https://api.mobileboost.io/uploadBuild/ \ -H "Content-Type: multipart/form-data" \ -F "build=@app.apk" \ -F "organisation_key=org123" \ -F "platform=android" \ -F "metadata={}" ``` **3. Execute tests** Creating a test starts writing its automation code. Wait until `GET /tests` shows the test as runnable on your platform (`platforms.android.runnable` is `true`), then start a run: ```bash theme={null} curl -X POST https://api.mobileboost.io/tests/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "organisationId": "org123", "buildId": "BUILD_ID_FROM_STEP_2", "tags": ["smoke"] }' ``` The response carries the ID of the run in `runId`: ```json theme={null} { "message": "Autotest run created", "runId": "b1111111df174791a93e560390ea7e65", "status": "running" } ``` **4. Get the results** Pass `runId` to `GET /runs/{runId}`. Repeat the call until `status` is `completed` or `cancelled`: ```bash theme={null} curl https://api.mobileboost.io/runs/b1111111df174791a93e560390ea7e65 \ -H "Authorization: Bearer YOUR_API_KEY" ``` Each test is listed in `succeededTests`, `failedTests` or `blockedTests`, with a link to its report. See [Get run status](/api-reference/get-run-status) for the full response. # XCUITest Source: https://docs.mobileboost.io/api/xcui-packaging Package your compiled XCUITest runner and execute it on the MobileBoost device grid This guide covers how to build and package your compiled XCUITest suite (`*-Runner.app`) so it runs correctly on the MobileBoost device grid, and how to trigger and monitor runs via the API. Follow the section that matches where you want to run: physical devices or virtual devices (simulators). The two are not interchangeable - a runner built for one will not launch on the other. > [!NOTE] > > You do not need to solve code signing or provisioning for our devices. Our grid re-signs the test runner with the certificate and device profile for the specific hardware it runs on. All API endpoints below authenticate with your `mb_live_` API key passed as a Bearer token in the `Authorization` header. ## Prerequisites * Xcode (matching or newer than the OS versions you target). * Your project has a UI Testing bundle target and a scheme that includes it in its Test action. * You know three bundle identifiers - you'll need them at upload time: * App under test (must match the `.ipa` you upload) * Test runner (`*-Runner`) * Test bundle (`*UITests`) ## 1. Why physical vs. virtual matters `build-for-testing` compiles your runner and its `.xctest` bundle for one platform slice at a time: | Target | Destination | Output folder | Runs on | | - | - | - | - | | Physical | `generic/platform=iOS` | `Debug-iphoneos/` | Real iPhones/iPads | | Virtual | `generic/platform=iOS Simulator` | `Debug-iphonesimulator/` | Simulators | A simulator build links the simulator XCTest frameworks and CPU slice; a device build links the device ones. Uploading the wrong slice is the single most common failure - it will install but the test harness won't start. If you want to run on both, build and upload two separate zips. ## 2. Build the runner > [tab] Physical devices > > ```bash theme={null} > xcodebuild build-for-testing \ > -scheme \ > -destination 'generic/platform=iOS' \ > -derivedDataPath ./build > ``` > > Output lands in: > > ``` > ./build/Build/Products/Debug-iphoneos/UITests-Runner.app > ./build/Build/Products/<...>.xctestrun > ``` > [tab] Virtual devices (simulators) > > ```bash theme={null} > xcodebuild build-for-testing \ > -scheme \ > -destination 'generic/platform=iOS Simulator' \ > -derivedDataPath ./build > ``` > > Output lands in: > > ``` > ./build/Build/Products/Debug-iphonesimulator/UITests-Runner.app > ./build/Build/Products/<...>.xctestrun > ``` ## 3. Package the runner zip Zip from inside the Products slice folder so the `.app` is at the root of the archive, and use `--symlinks` (framework bundles contain symlinks - omitting this bloats the zip and can break the embedded frameworks). > [tab] Physical devices > > ```bash theme={null} > cd ./build/Build/Products/Debug-iphoneos > zip --symlinks -r ~/MyAppUITests-physical.zip \ > *-Runner.app \ > ../*.xctestrun > ``` > [tab] Virtual devices (simulators) > > ```bash theme={null} > cd ./build/Build/Products/Debug-iphonesimulator > zip --symlinks -r ~/MyAppUITests-simulator.zip \ > *-Runner.app \ > ../*.xctestrun > ``` Zip contents (required + recommended): * `*-Runner.app` - required (contains `PlugIns/*.xctest`, your compiled tests) * `*.xctestrun` - recommended. It preserves your test configuration: launch environment variables, launch arguments, the target-app reference, and skip/only test lists. Include it and your tests run exactly as configured in your scheme. ## 4. Verify the zip before uploading ```bash theme={null} # Confirm the runner and the compiled test bundle are present unzip -l ~/MyAppUITests-physical.zip | grep -E 'Runner.app|\.xctest|\.xctestrun' ``` You should see the `*-Runner.app`, a `PlugIns/*UITests.xctest` inside it, and the `.xctestrun`. To sanity-check you built the device slice (not simulator) when targeting physical hardware, confirm it came out of the `Debug-iphoneos` folder - that folder is the source of truth for the slice. ## 5. Bundle-ID matching > [!WARNING] > > The app under test you upload as `.ipa` must have the same bundle identifier the test scheme was built against. If your tests use a bare `XCUIApplication()`, it launches the scheme's target app by bundle ID; a mismatch means the tests launch nothing (or the wrong app). * For physical runs, upload your enterprise-distribution `.ipa` as the app under test. Do not upload the debug `Debug-iphoneos/*.app` produced by `build-for-testing` as the app under test - that debug app and your test runner just need to share the bundle ID, not the binary. * If your tests target a specific bundle explicitly, use `XCUIApplication(bundleIdentifier: "")` and tell us that ID. ## 6. Code signing - what you do and don't need to do **Physical devices:** You do not need a provisioning profile that includes our devices. We re-sign the runner with our own Apple Development certificate and a profile scoped to the target hardware. To avoid any local provisioning friction, you can even build without signing: ```bash theme={null} xcodebuild build-for-testing \ -scheme \ -destination 'generic/platform=iOS' \ -derivedDataPath ./build \ CODE_SIGNING_ALLOWED=NO ``` (Building with your own team's automatic signing is also fine - we strip and re-sign either way.) **Virtual devices:** Simulators don't enforce code signing; no certificates or profiles are required. Because we re-sign the runner, please keep bundle identifiers stable and don't hard-code entitlements that depend on your own team's provisioning (e.g. keychain-sharing / app-group IDs tied to your Team ID) in the UI test target - those can break re-signing. Your app under test is unaffected; it keeps your enterprise signature. ## 7. Optional: get your test list for sharding If you want to split tests across devices, we can shard automatically, or you can send us the test identifiers: ```bash theme={null} xcodebuild -scheme -showTestPlans ``` Identifiers use `TargetName/ClassName/testMethod` and can be passed as `only-testing` / `skip-testing` filters at launch time. ## 8. What to upload You'll end up with, per target type: | File | Purpose | Upload endpoint | | - | - | - | | `MyApp.ipa` | App under test (enterprise-signed for physical; simulator `.app`-zip for virtual) | `POST /uploadBuild` | | `MyAppUITests-.zip` | Compiled test runner + `.xctestrun` | `POST /app-automate/xcui/test-suite` | ### Common mistakes checklist * Built for simulator, uploaded for a physical run (or vice-versa). → Build the matching slice; upload the matching zip. * Zipped from the wrong directory, so the `.app` is nested under `Build/Products/...` inside the archive. → `cd` into the slice folder first; the `.app` must be at the zip root. * Zipped without `--symlinks`. → Frameworks break / signing fails. * App-under-test bundle ID ≠ what the tests expect. → Keep them identical; tell us the ID. * Uploaded the debug `build-for-testing` app instead of the enterprise `.ipa` for physical runs. → Upload your enterprise IPA as the app under test. * Ran `test` instead of `build-for-testing` (produces no reusable runner). → Use `build-for-testing`. ## 9. Trigger a test run Once both files are uploaded, start a run with the IDs returned by the two upload calls: the `app_id` from `POST /uploadBuild` and the `test_suite_id` from `POST /app-automate/xcui/test-suite`. **Endpoint** ``` POST /app-automate/xcui/build ``` **Minimal request** ```bash theme={null} curl -X POST "https://api.mobileboost.io/app-automate/xcui/build" \ -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "app": "", "testSuite": "" }' ``` **Required parameters** | Field | Description | | - | - | | `app` | The `app_id` returned by `POST /uploadBuild`. | | `testSuite` | The `test_suite_id` returned by `POST /app-automate/xcui/test-suite`. | **Optional parameters** | Field | Description | | - | - | | `project`, `buildTag` | Free-text labels for grouping and filtering runs. | | `gpsLocation` | Location applied to every device before tests start. | | `only-testing` / `skip-testing` | Run or exclude specific tests (whole run, no sharding). Values are `TargetName/ClassName/testMethod` identifiers. | | `shards` | Split the suite across devices, optionally with an explicit test list per shard. | **Full request with GPS and sharding** ```bash theme={null} curl -X POST "https://api.mobileboost.io/app-automate/xcui/build" \ -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "app": "", "testSuite": "", "project": "My iOS App", "buildTag": "main-2026-07-01", "gpsLocation": { "latitude": 40.698979, "longitude": -73.9958661 }, "shards": { "numberOfShards": 2, "mapping": [ { "name": "shard-1", "strategy": "only-testing", "values": ["MyAppUITests/LoginTests", "MyAppUITests/CheckoutTests/testAddToCart"] }, { "name": "shard-2", "strategy": "only-testing", "values": ["MyAppUITests/SearchTests"] } ] } }' ``` ### GPS location `gpsLocation` overrides the device's location for the whole run; it is applied on each device before your first test starts and cleared automatically at teardown. ```json theme={null} { "latitude": 40.698979, "longitude": -73.9958661, "altitude": 0.0 } ``` | Field | Required | Notes | | - | - | - | | `latitude` | yes | Decimal degrees. | | `longitude` | yes | Decimal degrees. | | `altitude` | no | Meters, default `0.0`. Honored on virtual devices only; ignored on physical iOS (the simulated-location channel there is lat/long only). | If you omit `gpsLocation`, physical devices keep their real GPS and virtual devices use our neutral default. To read or change the location from inside a test, drive your app's own location flows as usual - `gpsLocation` sets the starting position for the run. ### Sharding Sharding is optional. Provide a `shards` object to split the suite across devices and run them in parallel. **Auto-distribute** - give only `numberOfShards`; we spread the discovered tests evenly across that many shards: ```json theme={null} { "numberOfShards": 3 } ``` **Explicit test cases per shard** - provide a `mapping` array. Each entry names a shard and lists the tests it runs via `strategy: "only-testing"` and a `values` list of `TargetName/ClassName[/testMethod]` identifiers. `numberOfShards` must match the number of `mapping` entries. ```json theme={null} { "numberOfShards": 2, "mapping": [ { "name": "shard-1", "strategy": "only-testing", "values": ["MyAppUITests/LoginTests"] }, { "name": "shard-2", "strategy": "only-testing", "values": ["MyAppUITests/SearchTests"] } ] } ``` A strategy of `skip-testing` is also accepted per shard if you prefer to express a shard as "everything except these". Use `-showTestPlans` to list valid identifiers. **Response** ```json theme={null} { "message": "Success", "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3" } ``` Store the `build_id`; it identifies the run when you poll status and download results. ## 10. Check run status Poll the build with the `build_id` from the launch response. ``` GET /app-automate/xcui/build/ ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/xcui/build/$BUILD_ID" ``` The build carries an overall status plus one session per device/shard combination. Each session has its own `session_id`. ```json theme={null} { "build_id": "4d2b4deb810af077d5aed98f479bfdd2e64f36c3", "status": "running", "sessions": [ { "session_id": "9f1c2a7b6e3d4f018a2b5c6d7e8f9012", "device": "iPhone 15 Pro-18.5", "shard": "shard-1", "status": "passed", "tests": { "total": 12, "passed": 12, "failed": 0, "skipped": 0 } }, { "session_id": "1a2b3c4d5e6f70819aabbccddeeff0011", "device": "iPhone 15 Pro-18.5", "shard": "shard-2", "status": "running", "tests": { "total": 8, "passed": 3, "failed": 0, "skipped": 0 } } ] } ``` Status values (both build and session): `queued`, `running`, `passed`, `failed`, `error` (infrastructure/setup problem, e.g. install or signing failure), `timedout`. **Poll until terminal** ```bash theme={null} while :; do STATUS=$(curl -s -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/xcui/build/$BUILD_ID" | jq -r '.status') echo "status: $STATUS" case "$STATUS" in queued|running) sleep 10 ;; *) break ;; esac done ``` ## 11. Retrieve results Once the build reaches a terminal status, fetch the machine-readable report at the build level. ``` GET /app-automate/xcui/build//report?format= ``` ```bash theme={null} curl -H "Authorization: Bearer $MOBILEBOOST_API_KEY" \ "https://api.mobileboost.io/app-automate/xcui/build/$BUILD_ID/report?format=junit" \ -o results.xml ``` `junit` returns a standard JUnit XML your CI can ingest; `json` returns the same pass/fail data as structured JSON. # Changelog Source: https://docs.mobileboost.io/changelog All updates that shipped in the Platform, the test agent, the SDKs and the API. # Getting started Source: https://docs.mobileboost.io/engineer-tooling/agent-skill Use MobileBoost with Claude Code, Cursor, Codex, and other AI coding agents The MobileBoost agent skill lets you create, run, and debug mobile tests directly from your AI coding agent. It works with any agent that supports custom skills or tool definitions. ## How it works The skill generates native **XCUITest** and **Espresso** code by default for fast, deterministic test execution. It also has built-in knowledge of the MobileBoost SDK (GptDriver) and uses it as a fallback where native automation alone isn't enough, for example when dealing with non-accessible elements like web views, third-party components, or graphical elements. This gives you the performance of native frameworks with the reliability of AI-powered automation where it matters. ## Installation Add the MobileBoost skill to your agent by providing the following URL: ``` https://www.mobileboost.io/mobileboost-skill.md ``` > [tab] Claude Code > > Add the skill URL to your Claude Code configuration: > > ```bash theme={null} > claude skill add https://www.mobileboost.io/mobileboost-skill.md > ``` > [tab] Cursor > > In Cursor, go to **Settings > AI > Custom Skills** and paste the skill URL: > > ``` > https://www.mobileboost.io/mobileboost-skill.md > ``` > [tab] Codex > > Add the skill URL to your Codex agent configuration: > > ```bash theme={null} > codex skill add https://www.mobileboost.io/mobileboost-skill.md > ``` ## Recommended models For best results, use a model with medium reasoning capacity: | Provider | Recommended model | | - | - | | Anthropic | Claude Sonnet 4.6 | | OpenAI | GPT 5.4 | ## Usage Invoke the skill using the `/gptdriver-ui-tests` slash command in your agent: ``` /gptdriver-ui-tests ``` ### Prompting tips The skill works best when you provide structured input: * **Step-by-step test definitions**: break your test into individual steps describing each action and expected outcome * **Screenshots**: attach screenshots of the flow you want to test so the agent can understand the UI context > [!NOTE] > > The more specific your test definition, the better the generated test. Instead of "test the login flow", provide individual steps like "tap the email field, enter [test@example.com](mailto:test@example.com), tap the password field, enter a password, tap the login button, verify the home screen appears". # Examples Source: https://docs.mobileboost.io/engineer-tooling/appium-java-examples Example tests using the MobileBoost Java Appium SDK ## Basic test with self healing ```java theme={null} import io.mobileboost.gptdriver.*; import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.android.options.UiAutomator2Options; public class LoginTest { public static void main(String[] args) throws Exception { UiAutomator2Options options = new UiAutomator2Options(); options.setCapability("gptdriver:apiKey", "YOUR_API_KEY"); // GptDriverAndroid wraps AndroidDriver with self-healing AndroidDriver driver = new GptDriverAndroid( new java.net.URL("http://127.0.0.1:4723"), options ); // These findElement calls are self-healing - // if an element ID changes, the SDK finds the right one driver.findElement(By.id("login-email")).sendKeys("test@example.com"); driver.findElement(By.id("login-password")).sendKeys("password123"); driver.findElement(By.id("login-button")).click(); // Use AI for complex assertions driver.aiAssert("The home screen is displayed with a welcome message"); driver.quit(); } } ``` ## AI commands for non-native elements ```java theme={null} GptDriverAndroid driver = new GptDriverAndroid( new java.net.URL("http://127.0.0.1:4723"), options, "When asked about Location Permissions, grant it." ); // AI handles web views and third-party components driver.aiExecute("Tap the 'Accept cookies' button in the web view"); driver.aiExecute("Scroll down and tap on 'View pricing'"); // Extract data from the screen Map data = driver.aiExtract("plan name", "monthly price"); // Strict check without self healing GptDriverAndroid gptDriver = (GptDriverAndroid) driver; WebElement el = gptDriver.findElement(By.id("success-banner"), false); Assert.assertTrue(el.isDisplayed()); ``` # Reference Source: https://docs.mobileboost.io/engineer-tooling/appium-java-reference API reference for the MobileBoost Java Appium SDK ## Driver classes The SDK provides drop-in replacements for Appium driver classes: | MobileBoost class | Extends | Platform | | - | - | - | | `GptDriverAndroid` | `AndroidDriver` | Android | | `GptDriverIOS` | `IOSDriver` | iOS | | `GptDriverAppium` | `AppiumDriver` | Cross-platform | ## Constructor options | Parameter | Type | Description | | - | - | - | | `url` | URL | Appium server URL | | `options` | Capabilities | Appium capabilities (must include `gptdriver:apiKey`) | | `additionalUserContext` | String | Optional context to guide AI behavior across all steps | | `enableSelfHealing` | Boolean | Enable/disable self healing globally (default: `true`) | ## Self healing By default, all `findElement` and `findElements` calls are wrapped with self-healing. When a `NoSuchElement` exception is thrown, the SDK attempts to resolve it automatically. ```java theme={null} // Self healing enabled (default) WebElement el = driver.findElement(By.id("login-button")); // Self healing disabled for this call WebElement el = driver.findElement(By.id("successMessage"), false); ``` See [self healing](/engineer-tooling/self-healing) for details. ## AI commands ### aiExecute Execute a natural language instruction on the current screen. ```java theme={null} driver.aiExecute("Tap the login button and wait for the home screen"); ``` ### aiAssert Verify a condition without taking any action. Throws an error if the assertion fails. ```java theme={null} driver.aiAssert("The home screen is displayed with a welcome message"); ``` ### aiAssertBulk Check multiple conditions in one call. Throws an error if any assertion fails. ```java theme={null} driver.aiAssertBulk( "The username is displayed", "The profile picture is visible", "The settings icon is in the top right" ); ``` ### aiCheckBulk Check multiple conditions in one call. Returns an object with `true`/`false` for each condition instead of throwing. ```java theme={null} Map results = driver.aiCheckBulk( "The username is displayed", "The notification badge is visible" ); ``` ### aiExtract Extract information from the current screen. Returns an object with the extracted values. ```java theme={null} Map data = driver.aiExtract( "username", "email address", "account balance" ); ``` ## Caching The SDK supports caching to reduce AI calls for repetitive executions: | Mode | Description | | - | - | | `NONE` | No caching | | `FULL_SCREEN` | Cache hit only if the full screen matches a previous successful execution | | `INTERACTION_REGION` | Cache hit if the interaction region matches (e.g., the area around a tapped element) | Set caching globally in the constructor or override per step. # Setup Source: https://docs.mobileboost.io/engineer-tooling/appium-java-setup Set up the MobileBoost SDK with your Java Appium test suite The MobileBoost SDK for Appium drops into your existing Java Appium suite. Your code-based tests run first, and AI kicks in as a fallback when a step fails. ## Prerequisites Install Appium and the platform drivers: ```bash theme={null} # Install Appium npm install -g appium # Install drivers appium driver install xcuitest # iOS appium driver install uiautomator2 # Android ``` ## Installation Add the dependency to your `pom.xml`: ```xml theme={null} io.mobileboost.gptdriver gptdriver-client 2.2.0 ``` ## Configuration Replace your Appium driver classes with the MobileBoost subclasses and add your API key as a capability: ```java theme={null} import io.mobileboost.gptdriver.*; UiAutomator2Options options = new UiAutomator2Options(); options.setCapability("gptdriver:apiKey", "YOUR_API_KEY"); // Use GptDriverAndroid instead of AndroidDriver AndroidDriver driver = new GptDriverAndroid( new java.net.URL("http://127.0.0.1:4723"), options ); ``` You can also pass additional context to enhance AI behavior: ```java theme={null} GptDriverAndroid driver = new GptDriverAndroid( new java.net.URL("http://127.0.0.1:4723"), options, "When asked about Location Permissions, grant it." ); ``` ## Start the Appium server Before running tests, start the Appium server: ```bash theme={null} appium ``` ## Next steps > [card] [Reference](/engineer-tooling/appium-java-reference) > > Driver classes, AI commands, and caching. > [card] [Examples](/engineer-tooling/appium-java-examples) > > Worked examples using the Java SDK. # Examples Source: https://docs.mobileboost.io/engineer-tooling/appium-python-examples Example tests using the MobileBoost Python Appium SDK ## Basic test ```python theme={null} from gptdriver_client import GptDriver gptd = GptDriver( api_key="YOUR_API_KEY", platform="android", device_name="Pixel 7", platform_version="15.0", ) gptd.execute("Tap on the Gmail app") gptd.assert_condition("The Gmail app is open") gptd.set_session_status("success") ``` ## With an existing Appium driver ```python theme={null} from gptdriver_client import GptDriver from appium import webdriver from appium.options.android import UiAutomator2Options options = UiAutomator2Options() options.load_capabilities({ "platformName": "Android", "platformVersion": "14.0", "deviceName": "emulator-5556", "automationName": "UiAutomator2", }) driver = webdriver.Remote( command_executor="http://127.0.0.1:4723", options=options ) gptd = GptDriver( api_key="YOUR_API_KEY", driver=driver ) gptd.execute("Tap on the Gmail app") gptd.assert_condition("The Gmail app is open") gptd.set_session_status("success") ``` ## PyTest ```python theme={null} # conftest.py import pytest from gptdriver_client import GptDriver @pytest.fixture def gptd(): driver = GptDriver( api_key="YOUR_API_KEY", platform="android", device_name="Pixel 7", platform_version="15.0", ) yield driver driver.set_session_status("success") ``` ```python theme={null} # test_example.py def test_login(gptd): gptd.execute("Tap the login button") gptd.execute("Enter 'test@example.com' in the email field") gptd.execute("Enter 'password123' in the password field") gptd.execute("Tap Sign In") gptd.assert_condition("The home screen is displayed") ``` # Reference Source: https://docs.mobileboost.io/engineer-tooling/appium-python-reference API reference for the MobileBoost Python Appium SDK ## Constructor ```python theme={null} from gptdriver_client import GptDriver gptd = GptDriver( api_key="YOUR_API_KEY", platform="android", device_name="Pixel 7", platform_version="15.0", ) ``` | Parameter | Type | Description | | - | - | - | | `api_key` | str | Your GPT Driver API key | | `platform` | str | `"android"` or `"ios"` (when the SDK starts its own session) | | `device_name` | str | Device name (when the SDK starts its own session) | | `platform_version` | str | Platform version (when the SDK starts its own session) | | `driver` | WebDriver | An existing Appium driver to attach to instead of starting a new session | | `enable_self_healing` | bool | Enable/disable self healing globally (default: `True`) | ## Self healing By default, all `find_element` calls are wrapped with self-healing. When an element can't be found, the SDK attempts to resolve it automatically. ```python theme={null} # Self healing is automatic for all find_element calls el = gptd.find_element(By.ID, "login-button") ``` Disable it globally via the constructor when you need strict assertions: ```python theme={null} gptd = GptDriver(api_key="...", enable_self_healing=False) ``` See [self healing](/engineer-tooling/self-healing) for details. ## AI commands ### execute Execute a natural language instruction on the current screen. ```python theme={null} gptd.execute("Tap the login button and wait for the home screen") ``` ### assert\_condition Verify a condition without taking any action. Raises an error if the condition fails. ```python theme={null} gptd.assert_condition("The home screen is displayed with a welcome message") ``` ## Session status Report the outcome of a session so it is recorded on the dashboard. ```python theme={null} gptd.set_session_status("success") # or "failed" ``` ## Caching The SDK supports caching to reduce AI calls for repetitive executions: | Mode | Description | | - | - | | `NONE` | No caching | | `FULL_SCREEN` | Cache hit only if the full screen matches a previous successful execution | | `INTERACTION_REGION` | Cache hit if the interaction region matches (e.g., the area around a tapped element) | # Setup Source: https://docs.mobileboost.io/engineer-tooling/appium-python-setup Set up the MobileBoost SDK with your Python Appium test suite The MobileBoost SDK for Appium drops into your existing Python Appium suite. Your code-based tests run first, and AI kicks in as a fallback when a step fails. ## Prerequisites Install Appium and the platform drivers: ```bash theme={null} # Install Appium npm install -g appium # Install drivers appium driver install xcuitest # iOS appium driver install uiautomator2 # Android ``` ## Installation ```bash theme={null} pip install gptdriver-client ``` To upgrade to the latest version: ```bash theme={null} pip install --upgrade gptdriver-client ``` ## Configuration Create a `GptDriver` instance with your API key: ```python theme={null} from gptdriver_client import GptDriver gptd = GptDriver( api_key="YOUR_API_KEY", platform="android", device_name="Pixel 7", platform_version="15.0", ) ``` Or pass an existing Appium driver: ```python theme={null} from gptdriver_client import GptDriver from appium import webdriver driver = webdriver.Remote( command_executor="http://127.0.0.1:4723", options=options ) gptd = GptDriver( api_key="YOUR_API_KEY", driver=driver ) ``` ## Start the Appium server Before running tests, start the Appium server: ```bash theme={null} appium ``` ## Next steps > [card] [Reference](/engineer-tooling/appium-python-reference) > > AI commands, self healing, and caching. > [card] [Examples](/engineer-tooling/appium-python-examples) > > Worked examples, including PyTest. # Deterministic execution Source: https://docs.mobileboost.io/engineer-tooling/appium-ts-deterministic-execution Run native Appium code first for speed and determinism, and fall back to AI only when it fails The fastest, most stable way to use the TypeScript SDK is **deterministic-first, AI-fallback**: every step tries a native Appium block first, and only if that block throws does the SDK fall back to running a natural-language instruction with AI. This gives you the best of both worlds: * **Speed and determinism** when selectors are stable: native Appium runs with no AI round-trip. * **Resilience** when the UI shifts, a popup appears, or a selector breaks: the step heals itself by falling back to AI instead of failing the test. ## The `appiumHandler` parameter `aiExecute` accepts an optional `appiumHandler`: a function that receives the live driver and runs your native Appium / WebdriverIO code. ```js theme={null} await gptDriver.aiExecute( "Tap the Sign In button", // AI fallback instruction { appiumHandler: async (driver) => { // native code, tried first await driver.$("~sign-in-button").click(); }, } ); ``` ### How it runs When you pass an `appiumHandler`, each call to `aiExecute` does the following: 1. **Runs the handler first** against the live driver. Fast, deterministic, no AI call. 2. **Logs the step** to the GPT Driver dashboard (a screenshot plus the handler's source) so the native step is visible alongside your AI steps for debugging. 3. **On success**, returns immediately. The AI is never called. 4. **On failure** (the handler throws), the SDK logs a warning and runs the natural-language instruction with AI to recover: ```text theme={null} Custom Appium handler failed, falling back to AI execution ``` That message is greppable in your run logs, so you can see exactly which steps fell back to AI. > [!NOTE] > > The `appiumHandler` and the instruction should express the **same intent**. The handler is the fast path; the instruction is the recovery path that must achieve the same result when the handler can't. ## A reusable step helper In practice you wrap the pattern in a small helper so every step reads as "native code, with an AI description to fall back to": ```js theme={null} async function step(title, appiumHandler, aiPrompt) { console.log(`▶ ${title}`); await gptDriver.aiExecute(aiPrompt, { appiumHandler }); } ``` Now each step is one line of intent, a deterministic block, and a natural-language fallback: ```js theme={null} // Native tap runs first; if the selector is gone, AI taps the button instead. await step( "tap Play lesson", async () => { await $("~Play lesson").click(); }, `In the action sheet, tap "Play lesson".` ); // Native assertion runs first; AI verifies the same thing if it throws. await step( "verify lesson launched", async () => { await $("~Saved lines").waitForDisplayed({ reverse: true, timeout: 25000 }); }, `Verify a lesson launches full-screen and replaces the list. Fail if no lesson appears.` ); ``` ## Worked example A full flow built this way. Native Appium drives every step; AI only engages when a native block throws (a moved selector, a slow load, an unexpected popup): ```js title="test/specs/saved-lines.spec.js" theme={null} import GptDriver from "gpt-driver-node"; const BUNDLE_ID = "com.example.app"; describe("Saved Lines", () => { let gptDriver; beforeEach(async function () { const { protocol, hostname, port, path } = browser.options; gptDriver = new GptDriver({ apiKey: browser.options.gptDriverApiKey, driver: browser, serverConfig: { url: `${protocol}://${hostname}:${port}${path}` }, cachingMode: "INTERACTION_REGION", testId: this.currentTest?.fullTitle(), appId: BUNDLE_ID, }); }); // Deterministic-first, AI-fallback. The handler runs first (fast, no AI); // on failure the SDK runs the natural-language prompt with AI. async function step(title, appiumHandler, aiPrompt) { console.log(`▶ ${title}`); await gptDriver.aiExecute(aiPrompt, { appiumHandler }); } it("plays a lesson and unsaves the line", async () => { await step( "open the action sheet on the first saved line", async () => { await $("~ellipsis").click(); await $("~Unsave").waitForDisplayed({ timeout: 5000 }); }, `Tap the three-dot "More" menu on the first saved line. An action sheet with "Play lesson" and "Unsave" should appear.` ); await step( "tap Play lesson", async () => { await $("~Play lesson").click(); }, `In the action sheet, tap "Play lesson".` ); await step( "verify the lesson launched", async () => { await $("~Saved lines").waitForDisplayed({ reverse: true, timeout: 25000 }); }, `Verify a lesson launches full-screen and replaces the Saved Lines list. Fail if no lesson appears.` ); await step( "exit back to Saved Lines", async () => { await $("~Back").click(); await $("~Saved lines").waitForDisplayed({ timeout: 6000 }); }, `Exit the lesson (tap the back, close, or X control, confirming to quit if asked) to return to the Saved Lines screen.` ); await step( "unsave the line", async () => { await $("~ellipsis").click(); await $("~Unsave").click(); }, `Open the "More" menu on the first saved line again and tap "Unsave".` ); await gptDriver.setSessionSucceeded(); }); }); ``` ## Falling back to AI for reads, too The same try-native-first idea works for data you read off the screen. Try a native selector, and fall back to `extract` (or `assert`) when the native read fails: ```js theme={null} let firstLine = ""; try { firstLine = await $("~saved-line-text").getText(); } catch (e) { const extracted = await gptDriver.extract(["firstSavedLineText"]); firstLine = extracted.firstSavedLineText ?? ""; } ``` ## When to reach for AI directly Skip the handler and call `aiExecute(prompt)` with no native code when: * The target is a **WebView, canvas, map, or other non-native element** that selectors can't reach reliably. See [non-native elements](/engineer-tooling/non-native-elements). * The step is **inherently visual or fuzzy** ("dismiss whatever popup is on screen", "tap the cheapest room option"). * You are prototyping and don't have stable selectors yet. Add `appiumHandler` blocks later to speed up the steps that run often. ## Tips * **Keep handlers fast and let them throw early.** Use short `waitFor*` timeouts inside handlers so a missing element falls back to AI quickly instead of burning the full default timeout. * **Pair the handler and the prompt.** They should do the same thing; the prompt is what runs when the handler can't. * **Combine with caching.** Set `cachingMode` so the AI fallback steps that do run also get cached. See the [reference](/engineer-tooling/appium-ts-reference#caching). * **Review fallbacks on the dashboard.** Every native step is logged with a screenshot, and every fallback emits the greppable warning above, so you can see where the suite is leaning on AI and tighten those selectors. # Examples Source: https://docs.mobileboost.io/engineer-tooling/appium-ts-examples Example tests using the GPT Driver TypeScript SDK ## Minimal end-to-end (WebdriverIO + Mocha) A complete spec. The SDK attaches to the WebdriverIO `browser`, the session starts on the first AI command, and the outcome is reported at the end. ```js title="test/specs/basic.spec.js" theme={null} import GptDriver from "gpt-driver-node"; describe("Chrome smoke test", () => { let gptDriver; beforeEach(async function () { const { protocol, hostname, port, path } = browser.options; gptDriver = new GptDriver({ apiKey: browser.options.gptDriverApiKey, driver: browser, serverConfig: { url: `${protocol}://${hostname}:${port}${path}` }, cachingMode: "INTERACTION_REGION", testId: this.currentTest?.fullTitle(), }); }); it("opens the Chrome app", async () => { await gptDriver.aiExecute("Open the Chrome app"); await gptDriver.assert("A new tab option is shown at the top of the screen"); await gptDriver.setSessionSucceeded(); }); }); ``` ## Native-first, AI fallback Run native Appium code first for speed and determinism, and let the SDK fall back to AI only when a native block throws. This is the recommended pattern for real suites. ```js theme={null} async function step(title, appiumHandler, aiPrompt) { console.log(`▶ ${title}`); await gptDriver.aiExecute(aiPrompt, { appiumHandler }); } await step( "enter the email", async () => { await $("~login-email").setValue("test@example.com"); }, `Type "test@example.com" into the email field.` ); await step( "submit the form", async () => { await $("~login-button").click(); }, `Tap the Sign In button.` ); ``` See [Deterministic execution](/engineer-tooling/appium-ts-deterministic-execution) for the full pattern and a complete worked example. ## Extract values, then assert on them Pull data off the screen with `extract`, then verify it with `assertBulk`: ```js theme={null} const order = await gptDriver.extract([ "totalPrice", "deliveryDate", ]); await gptDriver.assertBulk([ `The total price is ${order.totalPrice}`, "The VAT is calculated correctly", "The delivery date is shown in the format dd.mm.YYYY", ]); ``` ## Handle non-native elements The AI works visually, so it can drive WebViews and other elements that selectors struggle with: ```js theme={null} await gptDriver.aiExecute("Tap the 'Accept cookies' button in the cookie banner"); await gptDriver.aiExecute("Scroll down and tap on 'View pricing'"); ``` See [non-native elements](/engineer-tooling/non-native-elements) for details. # Reference Source: https://docs.mobileboost.io/engineer-tooling/appium-ts-reference API reference for the GPT Driver TypeScript SDK (gpt-driver-node) The TypeScript SDK exposes a single `GptDriver` class. Import the default export and construct it with your WebdriverIO `browser`: ```js theme={null} import GptDriver from "gpt-driver-node"; const gptDriver = new GptDriver({ apiKey: "YOUR_API_KEY", driver: browser, serverConfig: { url: "http://127.0.0.1:4723/" }, }); ``` ## Constructor options ```ts theme={null} new GptDriver(config: GptDriverConfig) ``` | Option | Type | Description | | - | - | - | | `apiKey` | `string` | **Required.** Your GPT Driver API key. | | `driver` | `WebDriver \| Browser` | An existing WebdriverIO / Appium session. The SDK runs on this session. | | `serverConfig.url` | `URL \| string` | The Appium server URL. Required whenever you pass a `driver`. | | `serverConfig.device` | `object` | `{ platform, deviceName, platformVersion }`. Required only when you do **not** pass a `driver` and want the SDK to start its own session. | | `cachingMode` | `CachingMode` | Default caching for AI steps. One of `"NONE"`, `"FULL_SCREEN"`, `"INTERACTION_REGION"`. Defaults to `"NONE"`. | | `testId` | `string` | A label for this run, shown on the dashboard. | | `appId` | `string` | App identifier (`appPackage` on Android / `bundleId` on iOS). Auto-read from the session capabilities when omitted; set it to override. | | `additionalUserContext` | `string` | Free-text guidance passed to the AI on every AI step. | | `maxWaitForStableScreenSecs` | `number` | How long to wait for the screen to settle before an AI step. | | `organisationId` | `string` | Organisation identifier used for cache scoping. | ## AI commands ### aiExecute Execute a natural-language instruction on the current screen. Optionally pass an `appiumHandler` to run native Appium code first and only fall back to AI if it throws (see [Deterministic execution](/engineer-tooling/appium-ts-deterministic-execution)). ```ts theme={null} aiExecute(command: string, options?: { appiumHandler?: (driver) => Promise; cachingMode?: CachingMode; }): Promise // Options-object form aiExecute(options: { command: string; appiumHandler?: (driver) => Promise; cachingMode?: CachingMode; }): Promise ``` ```js theme={null} // Natural language only await gptDriver.aiExecute("Tap the login button and wait for the home screen"); // Native-first, AI fallback await gptDriver.aiExecute("Tap the Sign In button", { appiumHandler: async (driver) => { await driver.$("~sign-in-button").click(); }, }); // Override caching for this step await gptDriver.aiExecute("Navigate to the settings page", { cachingMode: "FULL_SCREEN" }); ``` ### assert Verify a condition without taking any action. Resolves if the condition holds, throws otherwise. ```ts theme={null} assert(assertion: string, options?: { cachingMode?: CachingMode }): Promise ``` ```js theme={null} await gptDriver.assert(`The total price is ${price}`); ``` ### assertBulk Check multiple conditions in one call. Throws if **any** condition fails. ```ts theme={null} assertBulk(assertions: string[]): Promise ``` ```js theme={null} await gptDriver.assertBulk([ `The total price is ${price}`, "The VAT is calculated correctly", "The delivery date is shown in the format dd.mm.YYYY", ]); ``` ### checkBulk Check multiple conditions in one call. Returns an object with a `true` / `false` result per condition instead of throwing. ```ts theme={null} checkBulk(conditions: string[]): Promise> ``` ```js theme={null} const results = await gptDriver.checkBulk([ "The total price is shown", "The VAT is calculated correctly", ]); ``` ### extract Extract information from the current screen. Returns an object keyed by the values you asked for. ```ts theme={null} extract(keys: string[]): Promise> ``` ```js theme={null} const flight = await gptDriver.extract([ "departureAirportCode", "destinationAirportCode", "departureTime", ]); await gptDriver.assertBulk([ `departure airport is ${flight.departureAirportCode}`, `destination airport is ${flight.destinationAirportCode}`, ]); ``` ### openDeepLinkUrl Open a deep link in the app under test. ```ts theme={null} openDeepLinkUrl(params: { url: string; package?: string; bundleId?: string }): Promise ``` ```js theme={null} // Android await gptDriver.openDeepLinkUrl({ url: "myapp://product/123", package: "com.example.app" }); // iOS await gptDriver.openDeepLinkUrl({ url: "myapp://product/123", bundleId: "com.example.app" }); ``` ## Session lifecycle The session starts automatically on the first AI command. Report the outcome at the end of a test so it is recorded on the dashboard. ```ts theme={null} setSessionSucceeded(): Promise setSessionFailed(): Promise setSessionStatus(status: "succeeded" | "failed"): Promise ``` ```js theme={null} await gptDriver.setSessionSucceeded(); ``` ## Caching Caching reduces AI calls for repetitive executions. Set a default in the constructor, or override per call. | Mode | Description | | - | - | | `NONE` | No caching (default). | | `FULL_SCREEN` | Cache hit only if the full screen matches a previous successful execution. | | `INTERACTION_REGION` | Cache hit if the interaction region matches (for example, the area around a tapped element). Recommended. | ```js theme={null} // Default for the whole session const gptDriver = new GptDriver({ /* ... */, cachingMode: "INTERACTION_REGION" }); // Override for a single step await gptDriver.aiExecute("Navigate to settings", { cachingMode: "FULL_SCREEN" }); ``` # Setup Source: https://docs.mobileboost.io/engineer-tooling/appium-ts-setup Add the GPT Driver SDK to an existing WebdriverIO + Appium test suite (TypeScript / JavaScript) The TypeScript SDK (`gpt-driver-node`) wraps your existing **WebdriverIO + Appium** session. You keep writing normal Appium code, and the SDK adds AI commands (`aiExecute`, `assert`, `extract`, ...) on top of the same `browser` instance. Nothing about your existing setup has to change. > [!NOTE] > > This SDK is for engineers who already run Appium tests in code. If you are looking for the no-code recorder and cloud runner, see [QA Studio](/qa-studio/overview) instead. ## Prerequisites A working WebdriverIO + Appium project. If you are starting from scratch, install Appium and the platform drivers: ```bash theme={null} # Install Appium npm install -g appium # Install drivers appium driver install xcuitest # iOS appium driver install uiautomator2 # Android ``` A typical WebdriverIO project also has these dev dependencies: ```bash theme={null} npm install -D @wdio/cli @wdio/local-runner @wdio/mocha-framework @wdio/spec-reporter ``` ## Installation Add the SDK to your project: ```bash theme={null} npm install gpt-driver-node ``` ## Configure WebdriverIO Your `wdio.conf.js` (or `.ts`) stays a standard WebdriverIO config. Two things matter for the SDK: 1. Your Appium **capabilities** (the app and device under test). 2. Where you keep your **GPT Driver API key**. A convenient pattern is a custom top-level field so every spec can read it from `browser.options`: ```js title="wdio.conf.js" theme={null} export const config = { runner: 'local', protocol: 'http', hostname: '127.0.0.1', port: 4723, path: '/', // Your GPT Driver API key (or read it from process.env.GPT_DRIVER_API_KEY) gptDriverApiKey: process.env.GPT_DRIVER_API_KEY, specs: ['./test/**/*.spec.js'], maxInstances: 1, capabilities: [ { platformName: 'Android', 'appium:automationName': 'UiAutomator2', // 'appium:app': '/path/to/app.apk', // 'appium:deviceName': 'Pixel 7', }, ], framework: 'mocha', mochaOpts: { ui: 'bdd', timeout: 60000 * 10 }, }; ``` > [!NOTE] > > The long Mocha `timeout` matters: AI fallback steps can take several seconds, so keep the per-test timeout generous. ## Wire the SDK into a spec Construct a `GptDriver` and hand it the live WebdriverIO `browser`. The SDK attaches to that session and reuses it. A `beforeEach` keeps one instance per test: ```js title="test/specs/login.spec.js" theme={null} import GptDriver from "gpt-driver-node"; describe("Login", () => { let gptDriver; beforeEach(async function () { const { protocol, hostname, port, path } = browser.options; const baseUrl = `${protocol}://${hostname}:${port}${path}`; gptDriver = new GptDriver({ apiKey: browser.options.gptDriverApiKey, driver: browser, // the existing WebdriverIO session serverConfig: { url: baseUrl }, // the Appium server URL cachingMode: "INTERACTION_REGION", // "NONE" | "FULL_SCREEN" | "INTERACTION_REGION" testId: this.currentTest?.fullTitle(), // shows up on the dashboard appId: "com.example.app", // appPackage (Android) / bundleId (iOS) }); }); it("logs in", async () => { await gptDriver.aiExecute("Tap the login button and wait for the home screen"); await gptDriver.assert("The home screen is displayed with a welcome message"); await gptDriver.setSessionSucceeded(); }); }); ``` ### What each option does | Option | Required | Description | | - | - | - | | `apiKey` | yes | Your GPT Driver API key. | | `driver` | yes\* | An existing WebdriverIO / Appium `browser`. The SDK runs on this session. | | `serverConfig.url` | yes | The Appium server URL (required whenever you pass a `driver`). | | `cachingMode` | no | Default caching for AI steps. Defaults to `"NONE"`. | | `testId` | no | A label for this run, shown on the dashboard. | | `appId` | no | App identifier (`appPackage` / `bundleId`); auto-read from the session when omitted. | | `additionalUserContext` | no | Free-text guidance passed to the AI on every AI step (for example, `"When asked about Location Permissions, grant it."`). | > [!NOTE] > > \*If you do not pass an existing `driver`, provide `serverConfig.url` **and** `serverConfig.device.platform` and the SDK will start its own Appium session. Attaching to your existing `browser` is the common case and keeps the SDK in your normal WebdriverIO lifecycle. ## Session lifecycle You do **not** start the session manually. It starts automatically on the first AI command (`aiExecute`, `assert`, `extract`, ...). In case an AI step fails the test, we'll set the test status in the dashboard to failed automatically. For successful tests, report the outcome so it is recorded on the dashboard: ```js theme={null} await gptDriver.setSessionSucceeded(); ``` ## Run your tests Start the Appium server (or use the `appium` WebdriverIO service) and run WebdriverIO as usual: ```bash theme={null} appium # if you are not using the @wdio/appium-service npx wdio run wdio.conf.js ``` ## Next steps > [card] [Deterministic execution](/engineer-tooling/appium-ts-deterministic-execution) > > Run native Appium code first and only fall back to AI when it fails. The fastest, most stable way to use the SDK. > [card] [Reference](/engineer-tooling/appium-ts-reference) > > Every constructor option and command. # Examples Source: https://docs.mobileboost.io/engineer-tooling/espresso-examples Example tests using the MobileBoost Espresso SDK ## Jetpack Compose - basic test ```kotlin theme={null} import io.mobileboost.gptdriver_ktl.GptDriver import androidx.compose.ui.test.junit4.createComposeRule import org.junit.Rule import org.junit.Test class LoginTest { @get:Rule val composeTestRule = createComposeRule() @Test fun testLoginFlow() { val gptDriver = GptDriver( apiKey = "", deviceName = "Emulator", platformVersion = "N/A", cachingMode = "INTERACTION_REGION", testId = "testLoginFlow", composeTestRule = composeTestRule ) gptDriver.execute("Tap on the email field and type test@example.com") gptDriver.execute("Tap on the password field and type SecurePass123") gptDriver.execute("Tap the Sign In button") gptDriver.assert("The home screen is displayed with a welcome message") } } ``` ## With additional context ```kotlin theme={null} val gptDriver = GptDriver( apiKey = "", deviceName = "Pixel 8", platformVersion = "14.0", additionalUserContext = "When asked about notifications, tap Allow. Dismiss any marketing banners.", cachingMode = "INTERACTION_REGION", testId = "testOnboarding", composeTestRule = composeTestRule ) gptDriver.execute("Complete the onboarding by tapping Next on each screen") gptDriver.execute("Tap Get Started") gptDriver.assert("The main dashboard is visible") ``` ## Data extraction ```kotlin theme={null} val gptDriver = GptDriver( apiKey = "", deviceName = "Emulator", platformVersion = "N/A", composeTestRule = composeTestRule ) gptDriver.execute("Navigate to the profile screen") val values = gptDriver.extract(listOf("username", "email", "plan type")) val username = values["username"] as? String ``` ## Native-first with AI fallback Run your own Compose code first and let the SDK fall back to AI only when it throws. This is the pattern to reach for in a real suite: stable steps stay fast and deterministic, and a step that breaks because the UI moved keeps the test alive instead of failing it. ```kotlin theme={null} @Test fun testSearch() { val gptDriver = GptDriver( apiKey = "", composeTestRule = composeTestRule, testId = "testSearch" ) gptDriver.execute("Tap Search, type 'wireless headphones', and submit") { rule -> rule.onNodeWithTag("SearchField").performTextInput("wireless headphones") rule.onNodeWithContentDescription("Search").performClick() } gptDriver.assert("Search results for wireless headphones are shown") { rule -> rule.onNodeWithTag("SearchResults").assertExists() } } ``` Keep the natural language command descriptive of what the native code does. It is the AI fallback and the label the step carries in the session recording, so `"Tap the Sign In button"` is worth more than `"fallback"`. ## Bulk assertions and checks Verify several things against one screenshot rather than one call each. `assertBulk` fails the test when any condition is not met; `checkBulk` returns the results so you can branch on them. ```kotlin theme={null} @Test fun testCartTotals() { val gptDriver = GptDriver( apiKey = "", composeTestRule = composeTestRule, testId = "testCartTotals" ) gptDriver.execute("Open the cart") gptDriver.assertBulk( listOf( "The subtotal is 499 USD", "The grand total includes shipping", "The Place Order button is enabled" ) ) val results = gptDriver.checkBulk( listOf("A promo banner is visible", "A free shipping badge is visible") ) if (results["A promo banner is visible"] == true) { gptDriver.execute("Dismiss the promo banner") } } ``` ## Reporting the test result Report the outcome so each run is recorded as passed or failed rather than as `auto-completed`, and so the session's slot against your account's parallel session limit is released as soon as the test ends. A JUnit `TestWatcher` sees both outcomes, and its `failed` hook hands you the throwable, so the dashboard gets the reason rather than a bare "Failed". ```kotlin theme={null} import io.mobileboost.gptdriver_ktl.GptDriver import androidx.compose.ui.test.junit4.createComposeRule import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestWatcher import org.junit.runner.Description class CheckoutTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var gptDriver: GptDriver @get:Rule val reportOutcome = object : TestWatcher() { override fun succeeded(description: Description) { if (::gptDriver.isInitialized) gptDriver.setSessionSucceeded() } override fun failed(e: Throwable, description: Description) { if (::gptDriver.isInitialized) gptDriver.setSessionFailed(e) } } @Before fun setUp() { gptDriver = GptDriver( apiKey = "", composeTestRule = composeTestRule, testId = "checkout" ) } @Test fun testCheckout() { gptDriver.execute("Add the first product to the cart") gptDriver.execute("Open the cart and tap Checkout") gptDriver.assert("The order confirmation screen is displayed") } } ``` The `isInitialized` guard matters: if `setUp` itself fails there is no driver to report through, and without it the watcher would throw over the real failure. ## With CI metadata Record what your CI knows and the SDK does not, so a session can be found again by it and reporting can be sliced by it. ```kotlin theme={null} val gptDriver = GptDriver( apiKey = "", composeTestRule = composeTestRule, testId = "testCheckout", metadata = mapOf( "branch" to BuildConfig.GIT_BRANCH, "commit" to BuildConfig.GIT_SHA, "job" to BuildConfig.CI_JOB_ID ) ) ``` Values are strings, so convert anything else before passing it. Keys are free-form apart from `language` and `version`, which the SDK reserves to report itself. An instrumentation test cannot read the CI environment directly, so feed the values in through `buildConfigField` in your `build.gradle.kts`, or through `InstrumentationRegistry.getArguments()` if you pass them with `-Pandroid.testInstrumentationRunnerArguments`. # Reference Source: https://docs.mobileboost.io/engineer-tooling/espresso-reference API reference for the MobileBoost Espresso SDK ## GptDriver ### Constructor ```kotlin theme={null} GptDriver( apiKey: String, composeTestRule: ComposeTestRule, deviceName: String = "Emulator", platformVersion: String = "N/A", additionalUserContext: String = "", testId: String = "", cachingMode: String = "INTERACTION_REGION", waitForIdleTimeoutMs: Long = 2_000L, screenshotStabilityTimeoutSec: Float = 5.0f, metadata: Map = emptyMap() ) ``` | Parameter | Type | Description | | - | - | - | | `apiKey` | `String` | Your MobileBoost API key | | `composeTestRule` | `ComposeTestRule` | The Compose test rule from your test | | `deviceName` | `String` | Device name for logging | | `platformVersion` | `String` | Android version, for logging | | `additionalUserContext` | `String` | Context to guide AI behavior across all steps | | `testId` | `String` | Identifier for the test (used for caching and logging) | | `cachingMode` | `String` | `"NONE"`, `"FULL_SCREEN"`, or `"INTERACTION_REGION"` | | `waitForIdleTimeoutMs` | `Long` | How long to let Compose settle after each interaction. `0` skips the wait | | `screenshotStabilityTimeoutSec` | `Float` | How long to wait for two consecutive frames to match before sending the screenshot | | `metadata` | `Map` | Your own key/values, recorded on the session | Pass the arguments by name. Everything after `composeTestRule` has a default, and the order has changed between releases. > [!NOTE] > > A screen with an indefinite animation never reaches idle and never produces two matching frames, so it spends the full `waitForIdleTimeoutMs` and `screenshotStabilityTimeoutSec` budget on every step. Those two defaults are what to lower if such a screen makes a suite crawl. ### AI commands #### execute Execute a natural language instruction on the current screen. ```kotlin theme={null} sdkInstance.execute("Tap on the home button and verify it leads to the main screen") ``` #### assert Verify a condition without taking action. Throws on failure, and marks the session as failed. ```kotlin theme={null} sdkInstance.assert("The shopping cart shows 3 items") ``` #### assertBulk Verify several conditions against a single screenshot, in one round trip. Throws if any of them fails. ```kotlin theme={null} sdkInstance.assertBulk( listOf( "The subtotal is $499", "The grand total includes shipping", "The Place Order button is enabled" ) ) ``` #### checkBulk The same check as `assertBulk`, but it returns the results instead of throwing, so you can branch on what is on screen. ```kotlin theme={null} checkBulk( conditions: List, maxRetries: Int = 2, retryDelayMs: Long = 1000L ): Map ``` ```kotlin theme={null} val results = sdkInstance.checkBulk( listOf("A promo banner is visible", "The cart badge shows 1") ) if (results["A promo banner is visible"] == true) { sdkInstance.execute("Dismiss the promo banner") } ``` > [!NOTE] > > `assert`, `assertBulk` and `checkBulk` take `maxRetries` (default `2`) and `retryDelayMs` (default `1000`). A condition that is not met yet is re-checked against a fresh screenshot, which covers a screen that is still settling. #### extract Extract information from the current screen. Takes a list and returns a map keyed by the strings you asked for. ```kotlin theme={null} extract(extractions: List): Map ``` ```kotlin theme={null} val values = sdkInstance.extract(listOf("product name", "price", "quantity")) val price = values["price"] as? String ``` ## Native-first execution `execute`, `assert` and `assertBulk` take a trailing `ComposeHandler` lambda. The SDK runs your Compose or UiAutomator code first and only falls back to the natural language command when that code throws, so a step that works natively costs no AI round trip. ```kotlin theme={null} sdkInstance.execute("Tap the Login button") { rule -> rule.onNodeWithText("Login").performClick() } sdkInstance.assert("The home screen is displayed") { rule -> rule.onNodeWithTag("HomeScreen").assertExists() } ``` Write the command to describe what the native code does, not only what to do if it breaks. It is also the label the step carries in the session recording. Successful blocks are reported to the platform with a screenshot taken just before the lambda runs, so they appear as steps in the recording alongside AI steps. > [!NOTE] > > `checkBulk` takes no handler. Use `assertBulk` when you have native code to try, and `checkBulk` when you want the verdicts back. ## Session lifecycle The session starts automatically on the first AI command. Report the outcome at the end of a test so the dashboard records it. ```kotlin theme={null} setSessionSucceeded() setSessionFailed(errorMessage: String? = null) setSessionFailed(throwable: Throwable) ``` ```kotlin theme={null} sdkInstance.setSessionSucceeded() ``` Pass the `Throwable` overload the failure you already have in hand, from a JUnit `TestWatcher` or an `@After`. Without it, a timeout or an assertion raised outside the SDK reaches the dashboard as a bare "Failed" with no reason attached. A failed `assert` or `assertBulk` marks the session as failed for you. Nothing marks it as succeeded, so a session you never close is recorded as `auto-completed`, which reads as "the test stopped calling" rather than "the test passed". Closing a session also releases its slot against your account's parallel session limit straight away, instead of leaving it held until it ages out. ## Session metadata Pass `metadata` to record your own key/values on the session, anything your CI knows and the SDK does not. ```kotlin theme={null} val sdkInstance = GptDriver( apiKey = "", composeTestRule = composeTestRule, metadata = mapOf( "branch" to BuildConfig.GIT_BRANCH, "job" to BuildConfig.CI_JOB_ID ) ) ``` Nothing in the SDK or the platform interprets these. They are stored on the session so a run can be found again by them, and so reporting can be sliced by them, failure rates on `main` alone for instance. Keys are free-form and values are strings. `language` and `version` are reserved for the SDK's own identity: it reports `Android-Kotlin` and the library version it was built from, and a key of your own with either name is ignored rather than recorded. > [!NOTE] > > `metadata` requires SDK 2.9.0 or later. # Setup Source: https://docs.mobileboost.io/engineer-tooling/espresso-setup Set up the MobileBoost SDK with your Espresso test suite The MobileBoost SDK for Espresso supports both **Jetpack Compose** and **View/XML** based Android apps. ## Installation Add the MobileBoost Android library to your dependencies ([Maven Central](https://central.sonatype.com/artifact/io.mobileboost.gptdriver/gptdriver-compose)): ```gradle title="build.gradle.kts (:app)" theme={null} dependencies { // ... androidTestImplementation("io.mobileboost.gptdriver:gptdriver-compose:2.9.0") // ... } ``` If needed, add packaging exclude rules in the `android` section of your `build.gradle.kts (:app)` file to resolve build failures caused by duplicate metadata files from dependencies: ```kotlin title="build.gradle.kts (:app)" theme={null} android { // ... packaging { resources { excludes.addAll(listOf("META-INF/INDEX.LIST", "META-INF/io.netty.versions.properties")) } } // ... } ``` If not already present, add internet permissions to your `AndroidManifest.xml`: ```xml title="AndroidManifest.xml" theme={null} ``` ## Jetpack Compose Import the SDK and create a `GptDriver` instance in your test, passing the `ComposeTestRule`: ```kotlin theme={null} import io.mobileboost.gptdriver_ktl.GptDriver @get:Rule val composeTestRule = createComposeRule() val sdkInstance = GptDriver( apiKey = "", deviceName = "Emulator", platformVersion = "N/A", additionalUserContext = "", cachingMode = "INTERACTION_REGION", testId = "", composeTestRule = composeTestRule ) ``` ### Caching modes | Mode | Description | | - | - | | `NONE` | No caching | | `FULL_SCREEN` | Cache hit only if the full screen matches | | `INTERACTION_REGION` | Cache hit if the interaction region matches (recommended) | ## View/XML based apps For traditional View-based apps, the setup is similar but uses an `ActivityScenarioRule` instead of `ComposeTestRule`. # MCP server Source: https://docs.mobileboost.io/engineer-tooling/mcp Connect MobileBoost to Cursor, Claude Code, and other MCP clients The MobileBoost MCP server lets your AI coding agent call MobileBoost directly: list and edit tests, trigger test runs, and check run status without leaving your editor. Any client that supports the [Model Context Protocol](https://modelcontextprotocol.io) can connect to it. ## Prerequisites * A MobileBoost account * An MCP-compatible client (Cursor, Claude Code, Claude Desktop, or any other MCP client) ## Server details | Field | Value | | - | - | | URL | `https://api.mobileboost.io/mcp` | | Transport | Streamable HTTP | | Authentication | OAuth 2.1 (browser sign-in, no API key) | | Protocol revision | `2025-06-18` | ## Integrate the server Pick your client below and follow the steps. The first time you connect, your browser opens a MobileBoost sign-in page; after that, the client refreshes tokens automatically. > [tab] Cursor > > **1. Open the MCP configuration** > > In Cursor, open **Settings → MCP → Add new MCP server**, or edit `~/.cursor/mcp.json` directly. > > **2. Add the MobileBoost server** > > Paste the following entry: > > ```json theme={null} > { > "mcpServers": { > "mobileboost": { > "url": "https://api.mobileboost.io/mcp" > } > } > } > ``` > > **3. Sign in** > > Save the file. Cursor opens a browser tab — sign in with your MobileBoost account and approve the requested scopes. The server then shows up with a green status indicator under **Settings → MCP**. > [tab] Claude Code > > **1. Add the server with the CLI** > > ```bash theme={null} > claude mcp add --transport http mobileboost https://api.mobileboost.io/mcp > ``` > > **2. Sign in** > > Claude Code opens your browser on first use. Sign in to MobileBoost and approve access. > > **3. Verify the connection** > > ```bash theme={null} > claude mcp list > ``` > > The `mobileboost` entry should report as connected. > [tab] Claude Desktop > > Add MobileBoost as a [custom connector](https://support.claude.com/en/articles/11175166) - no config file editing needed. > > **1. Open Connectors settings** > > In Claude Desktop, click your profile icon and choose **Settings**, then go to **Connectors** in the sidebar. You can also open it directly at [claude.ai/customize/connectors](https://claude.ai/customize/connectors). > > **2. Add a custom connector** > > Scroll to the bottom of the connector list and click **Add custom connector**. > > **3. Enter the MobileBoost URL** > > In the **Remote MCP server URL** field, paste: > > ``` > https://api.mobileboost.io/mcp > ``` > > Leave **Advanced settings** untouched. MobileBoost uses Dynamic Client Registration, so no OAuth client ID or secret is required. > > **4. Save and connect** > > Click **Add** to save the connector, then click **Connect** next to MobileBoost in the list. A browser window opens for you to sign in to MobileBoost and approve the requested scopes. > > **5. Enable in chat** > > Open a new chat, click the **+** button in the message composer, choose **Connectors**, and toggle **MobileBoost** on. > > > [!NOTE] > > > > Custom connectors connect from Anthropic's cloud, not your local machine. The MobileBoost MCP server is publicly reachable, so this works out of the box. > > > [!NOTE] > > > > On Team and Enterprise plans, an Owner must first add the connector under **Organization settings → Connectors** before members can connect to it. > [tab] VS Code > > **1. Create the MCP config** > > In your project root, create `.vscode/mcp.json`: > > ```json theme={null} > { > "servers": { > "mobileboost": { > "type": "http", > "url": "https://api.mobileboost.io/mcp" > } > } > } > ``` > > **2. Start the server** > > Open the Command Palette and run **MCP: List Servers**, select **mobileboost**, and click **Start**. VS Code opens your browser to complete sign-in. > [tab] Other clients > > Any MCP-compatible client can connect to MobileBoost. Use: > > * **URL**: `https://api.mobileboost.io/mcp` > * **Transport**: Streamable HTTP > > For clients that only support stdio, use the `mcp-remote` bridge: > > ```bash theme={null} > npx -y mcp-remote https://api.mobileboost.io/mcp > ``` > > The client (or `mcp-remote`) discovers the authorization server, registers itself, and runs the browser sign-in flow automatically. ## Verify the integration Once connected, ask your agent something that requires MobileBoost data, for example: ``` List my MobileBoost tests. ``` The agent should call `list_tests` and return your tests. If the call fails with an authentication error, sign out and reconnect from your client's MCP settings. ## Available tools The server exposes the following tools, all scoped to your MobileBoost organisation: | Tool | Scope | Description | | - | - | - | | `list_tests` | `mcp:read` | List tests with id, title, tags, and last update time. | | `get_test` | `mcp:read` | Fetch a single test by id, including its commands. | | `create_test` | `mcp:write` | Create a new test with a title and a list of commands. | | `update_test` | `mcp:write` | Replace the commands of an existing test. | | `delete_test` | `mcp:write` | Delete one or more tests. | | `execute_tests` | `mcp:write` | Trigger a test suite run by `testIds` or `tags`. Returns a `testSuiteRunId`. | | `get_test_run_status` | `mcp:read` | Fetch the status and summary of a previously-started run. | Read-only clients that only request `mcp:read` see the four read tools and have the write tools hidden. ## Scopes The server defines two OAuth scopes: * `mcp:read` — list and read tests and runs * `mcp:write` — create, update, delete, and execute tests Most clients request both by default. To restrict an agent to read-only access, set the scope explicitly in your client config (see your client's documentation for the exact field). # Interact with non-native elements Source: https://docs.mobileboost.io/engineer-tooling/non-native-elements Test web views, hybrid components, and other non-native UI elements The MobileBoost SDK uses a visual, AI-powered approach that works regardless of the underlying UI technology. This means you can interact with elements that traditional test frameworks struggle with. ## Supported element types * **Web views**: HTML content rendered inside a native app * **Third-party components**: SDKs and libraries where you don't control the element identifiers * **Graphical elements**: charts, maps, canvas-based UI, and other visual components * **Cross-technology frameworks**: React Native, Flutter, and other hybrid frameworks ## How it works The SDK executes your code-based test steps first. When a step targets a non-native element that can't be located through standard selectors, the SDK falls back to AI-powered visual execution. The AI interprets the screen visually and performs the intended action. ```java theme={null} // Standard code execution handles native elements driver.findElement(By.id("native-button")).click(); // AI command handles web views and non-accessible elements driver.aiExecute("Tap the 'Accept cookies' button in the web view"); ``` > [!NOTE] > > WebView automation through commands currently has limited support. We recommend using AI-powered natural language steps (`aiExecute`) for WebView interactions. # Overview Source: https://docs.mobileboost.io/engineer-tooling/overview Integrate MobileBoost into your development workflow MobileBoost gives engineers multiple ways to create and run mobile tests, from AI-powered agent skills in your IDE to native SDKs for your existing test frameworks. > [card] [Agent skill](/engineer-tooling/agent-skill) > > Use MobileBoost from Claude Code, Cursor, or Codex. > [card] [SDK](/engineer-tooling/sdk-overview) > > Write tests in Appium, XCUI, or Espresso with the MobileBoost SDK. # Self healing Source: https://docs.mobileboost.io/engineer-tooling/self-healing Automatically adapt tests when the UI changes By default, the MobileBoost SDK intercepts failing `findElement` calls and attempts to self-heal them on the fly. This keeps your tests passing without manual intervention when the UI changes. > [!NOTE] > > **Automatic self healing is available in the Java and Python SDKs only.** The TypeScript SDK (`gpt-driver-node`) does not intercept or heal `findElement` calls. Instead, you opt into recovery per step by passing native Appium code to `aiExecute` via an `appiumHandler`: the native block runs first, and the SDK falls back to AI only when it throws. See [Deterministic execution](/engineer-tooling/appium-ts-deterministic-execution). ## How it works When a `findElement` call throws a `NoSuchElement` exception, the SDK catches it and attempts to resolve the issue by: 1. **Waiting** for the element to appear (handles dynamic loading times) 2. **Dismissing** unexpected popups or overlays 3. **Scrolling** to explore the screen for the element 4. **Inspecting** available elements for the same intent (handles changed element IDs) This allows the SDK to self-heal common flakiness causes: * Dynamic loading times * Changed element IDs * Changed UI layout * Changed text or copy * Unexpected popups and overlays ## Reviewing healed steps All caught exceptions and details about which locator was used to heal the test are available on the MobileBoost dashboard at [app.mobileboost.io/gpt-driver/sessions](https://app.mobileboost.io/gpt-driver/sessions). Each session shows: * The original locator that failed * The reasoning behind the self-heal attempt * Screenshots of the screen at the time of failure ## Disabling self healing You can disable self healing for specific commands when you need strict assertions: > [tab] Java > > ```java theme={null} > // Disable for a specific findElement call > WebElement el = driver.findElement(By.id("successMessage"), false); > Assert.assertTrue(el.isDisplayed()); > ``` > [tab] Python > > ```python theme={null} > # Disable globally via constructor > gptd = GptDriver( > api_key="...", > enable_self_healing=False > ) > ``` # Examples Source: https://docs.mobileboost.io/engineer-tooling/xcui-examples Example tests using the MobileBoost XCUI SDK ## Basic navigation test ```swift theme={null} import XCTest import gptd_swift @MainActor final class DemoUITests: XCTestCase { var app: XCUIApplication! var gptDriver: GptDriver! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() gptDriver = GptDriver( apiKey: "", nativeApp: app ) } override func tearDownWithError() throws { app = nil } func testNavigationToSecondScreen() async throws { try gptDriver.execute("Tap on the Go to Second Screen button") try gptDriver.execute("Check if you can see a screen with a second screen headline") } } ``` ## With caching enabled ```swift theme={null} override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() gptDriver = GptDriver( apiKey: "", nativeApp: app, cachingMode: CachingMode.interactionRegion, testId: self.name ) } func testLoginFlow() async throws { try gptDriver.execute("Tap the email field and enter test@example.com") try gptDriver.execute("Tap the password field and enter SecurePass123") try gptDriver.execute("Tap the Sign In button") try gptDriver.assert("The home screen is displayed with a welcome message") } ``` ## Native-first with AI fallback Run your own XCUITest code first and let the SDK fall back to AI only when it fails. This is the pattern to reach for in a real suite: stable steps stay fast and deterministic, and a step that breaks because the UI moved keeps the test alive instead of failing it. ```swift theme={null} func testSearchAndFilter() async throws { try gptDriver.execute("Tap on Search, type 'wireless headphones', and submit", nativeAction: { let searchField = app.textFields["Navigation.SearchBar.Input"] XCTAssertTrue(searchField.waitForExistence(timeout: 15)) searchField.tap() searchField.typeText("wireless headphones") app.keyboards.buttons["Search"].tap() }) try gptDriver.assert("Search results for wireless headphones are shown", nativeAction: { XCTAssertTrue(app.collectionViews["SearchResults"].waitForExistence(timeout: 10)) }) } ``` Keep the natural language command descriptive of what the native code does. It is the AI fallback and the label the step carries in the session recording, so `"Tap the Sign In button"` is worth more than `"fallback"`. ## Bulk assertions and checks Verify several things against one screenshot rather than one call each. `assertBulk` fails the test when any condition is not met; `checkBulk` returns the results so you can branch on them. ```swift theme={null} func testCartTotals() async throws { try gptDriver.execute("Open the cart") try gptDriver.assertBulk([ "The subtotal is $499", "The grand total includes shipping", "The Place Order button is enabled" ]) let results = try gptDriver.checkBulk([ "A promo banner is visible", "A free shipping badge is visible" ]) if results["A promo banner is visible"] == true { try gptDriver.execute("Dismiss the promo banner") } } ``` Both take a `nativeAction` closure too. If the closure succeeds, `checkBulk` reports every condition as `true`: ```swift theme={null} let results = try gptDriver.checkBulk(["The cart badge shows 1"], nativeAction: { XCTAssertEqual(app.buttons["TabBar.Cart"].value as? String, "1") }) ``` ## Reporting the test result Report the outcome in `tearDown` so each run is recorded as passed or failed rather than as `auto-completed`, and so the session's slot against your account's parallel session limit is released as soon as the test ends. ```swift theme={null} import XCTest import gptd_swift @MainActor final class CheckoutUITests: XCTestCase { var app: XCUIApplication! var gptDriver: GptDriver! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() gptDriver = GptDriver( apiKey: "", nativeApp: app, testId: self.name ) } override func tearDownWithError() throws { if testRun?.failureCount == 0 { try? gptDriver.setSessionSucceeded() } else { try? gptDriver.setSessionFailed() } app = nil } func testCheckout() async throws { try gptDriver.execute("Add the first product to the cart") try gptDriver.execute("Open the cart and tap Checkout") try gptDriver.assert("The order confirmation screen is displayed") } } ``` Use `testRun?.failureCount` rather than a flag you set yourself, so a failure raised anywhere in the test is reflected, including one from `assert`. Call both with `try?`: reporting the status is bookkeeping, and a network hiccup on the way out should not turn a passing test red. ## With CI metadata Record what your CI knows and the SDK does not, so a session can be found again by it and reporting can be sliced by it. ```swift theme={null} override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() let environment = ProcessInfo.processInfo.environment gptDriver = GptDriver( apiKey: "", nativeApp: app, testId: self.name, metadata: [ "branch": environment["BUILDKITE_BRANCH"] ?? "local", "commit": environment["BUILDKITE_COMMIT"] ?? "unknown", "job": environment["BUILDKITE_JOB_ID"] ?? "unknown" ] ) } ``` Values are strings, so convert anything else before passing it. Keys are free-form apart from `language` and `version`, which the SDK reserves to report itself. ## With additional context ```swift theme={null} gptDriver = GptDriver( apiKey: "", nativeApp: app, additionalUserContext: "When asked about Location Permissions, grant it. When a marketing popup appears, dismiss it." ) func testOnboarding() async throws { try gptDriver.execute("Complete the onboarding flow by tapping Next on each screen") try gptDriver.execute("Tap Get Started on the final screen") try gptDriver.assert("The main dashboard is visible") } ``` # Reference Source: https://docs.mobileboost.io/engineer-tooling/xcui-reference API reference for the MobileBoost XCUI SDK ## GptDriver ### Constructor ```swift theme={null} GptDriver( apiKey: String, nativeApp: XCUIApplication = XCUIApplication(), cachingMode: CachingMode = .none, testId: String = "", additionalUserContext: String = "", logNativeExecutions: Bool = true, charactersPerSecond: Int? = 10, metadata: [String: String] = [:], excludeFromBaseline: Bool = false ) ``` | Parameter | Type | Description | | - | - | - | | `apiKey` | `String` | Your MobileBoost API key | | `nativeApp` | `XCUIApplication` | The app instance to test | | `cachingMode` | `CachingMode` | Caching strategy (`.none`, `.fullScreen`, `.interactionRegion`) | | `testId` | `String` | Identifier for the test (used for caching and logging) | | `additionalUserContext` | `String` | Context to guide AI behavior across all steps | | `logNativeExecutions` | `Bool` | Report successful `nativeAction` blocks as steps in the session recording | | `charactersPerSecond` | `Int?` | How fast to type text. Pass `nil` to type each string in one call | | `metadata` | `[String: String]` | Your own key/values, recorded on the session | | `excludeFromBaseline` | `Bool` | Keep this session from becoming the caching baseline. See [Keeping a run out of the baseline](#keeping-a-run-out-of-the-baseline) | ### AI commands #### execute Execute a natural language instruction on the current screen. ```swift theme={null} try gptDriver.execute("Tap on the Go to Second Screen button") ``` #### assert Verify a condition without taking action. Throws on failure, and marks the session as failed. ```swift theme={null} try gptDriver.assert("The home screen is displayed") ``` #### assertBulk Verify several conditions against a single screenshot, in one round trip. Throws if any of them fails. ```swift theme={null} try gptDriver.assertBulk([ "The subtotal is $499", "The grand total includes shipping", "The Place Order button is enabled" ]) ``` #### checkBulk The same check as `assertBulk`, but it returns the results instead of throwing, so you can branch on what is on screen. ```swift theme={null} checkBulk(_ conditions: [String], maxRetries: Int = 2, retryDelay: TimeInterval = 1.0, timeout: TimeInterval = 60) throws -> [String: Bool] ``` ```swift theme={null} let results = try gptDriver.checkBulk([ "A promo banner is visible", "The cart badge shows 1" ]) if results["A promo banner is visible"] == true { try gptDriver.execute("Dismiss the promo banner") } ``` > [!NOTE] > > `assert`, `assertBulk` and `checkBulk` take `maxRetries` (default `2`) and `retryDelay` (default `1.0` seconds). A condition that is not met yet is re-checked against a fresh screenshot, which covers a screen that is still settling. #### extract Extract information from the current screen. Takes an array and returns a dictionary keyed by the strings you asked for. ```swift theme={null} extract(_ extractions: [String], timeout: TimeInterval = 60) throws -> [String: Any] ``` ```swift theme={null} let values = try gptDriver.extract(["username", "account balance"]) let username = values["username"] as? String ``` ## Native-first execution Every AI command has an overload taking a `nativeAction` closure. The SDK runs your XCUITest code first and only falls back to the natural language command when that code fails, so a step that works natively costs no AI round trip. ```swift theme={null} execute(_ command: String, nativeAction: () throws -> Void, timeout: TimeInterval = 600, cachingMode: CachingMode? = nil) throws assert(_ assertion: String, nativeAction: () throws -> Void, maxRetries: Int = 2, retryDelay: TimeInterval = 1.0, timeout: TimeInterval = 60) throws assertBulk(_ assertions: [String], nativeAction: () throws -> Void, maxRetries: Int = 2, retryDelay: TimeInterval = 1.0, timeout: TimeInterval = 60) throws checkBulk(_ conditions: [String], nativeAction: () throws -> Void, maxRetries: Int = 2, retryDelay: TimeInterval = 1.0, timeout: TimeInterval = 60) throws -> [String: Bool] ``` ```swift theme={null} try gptDriver.execute("Tap on Search, type 'wireless headphones', and submit", nativeAction: { let searchField = app.textFields["Navigation.SearchBar.Input"] XCTAssertTrue(searchField.waitForExistence(timeout: 15)) searchField.tap() searchField.typeText("wireless headphones") app.keyboards.buttons["Search"].tap() }) ``` The SDK falls back to AI when the closure throws, raises an Objective-C exception, or hits a failing `XCTAssert`. Assertion failures inside the closure are intercepted and are **not** recorded as test failures, since the fallback takes over. Only a failure of the fallback itself fails the test. Write the command to describe what the native code does, not only what to do if it breaks. It is also the label the step carries in the session recording. For `checkBulk`, a closure that succeeds reports every condition as `true`. > [!NOTE] > > Successful blocks are reported to the platform with a screenshot taken just before the closure runs, so they appear as steps in the session recording alongside AI steps. Reporting is best effort and never fails a passing test. Set `logNativeExecutions: false` in the constructor to skip it and trade the recording for speed. ## Session lifecycle The session starts automatically on the first AI command. Report the outcome at the end of a test so the dashboard records it. ```swift theme={null} setSessionSucceeded(timeout: TimeInterval = 15) throws setSessionFailed(timeout: TimeInterval = 15) throws ``` ```swift theme={null} try gptDriver.setSessionSucceeded() ``` A failed `assert` or `assertBulk` marks the session as failed for you. Nothing marks it as succeeded, so a session you never close is recorded as `auto-completed`, which reads as "the test stopped calling" rather than "the test passed". Closing a session also releases its slot against your account's parallel session limit straight away, instead of leaving it held until it ages out. ## Session metadata Pass `metadata` to record your own key/values on the session, anything your CI knows and the SDK does not. ```swift theme={null} gptDriver = GptDriver( apiKey: "", nativeApp: app, metadata: [ "branch": ProcessInfo.processInfo.environment["BUILDKITE_BRANCH"] ?? "unknown", "job": ProcessInfo.processInfo.environment["BUILDKITE_JOB_ID"] ?? "unknown" ] ) ``` Nothing in the SDK or the platform interprets these. They are stored on the session so a run can be found again by them, and so reporting can be sliced by them, failure rates on `master` alone for instance. Keys are free-form and values are strings. `language` and `version` are reserved for the SDK's own identity: it reports `Swift` and the package version it was built from, and a key of your own with either name is ignored rather than recorded. > [!NOTE] > > `metadata` requires SDK 1.12.0 or later. ## Keeping a run out of the baseline With caching on, every session that ends `succeeded` or `auto-completed` becomes the baseline that later runs of the same `testId` replay. Pass `excludeFromBaseline: true` for a run that should use the cache but never replace that baseline: a test re-run on every pull request, an experimental variant, or a branch that is not ready yet. ```swift theme={null} gptDriver = GptDriver( apiKey: "", nativeApp: app, cachingMode: CachingMode.interactionRegion, testId: self.name, excludeFromBaseline: ProcessInfo.processInfo.environment["IS_PULL_REQUEST"] == "true" ) ``` A typical setup is a nightly run on your main branch that records the baseline, and pull request runs that replay it. However many excluded runs come after it, the last run that was not excluded stays the baseline. An excluded run is still recorded and shown on the dashboard like any other session. The setting is fixed when the session starts. A session's final status cannot make an excluded run a baseline afterwards. > [!NOTE] > > `excludeFromBaseline` requires SDK 1.14.0 or later. ## Session URL The SDK creates a session URL for each test run, accessible via: * **`gptDriver.sessionURL`**: public property * **`onSessionCreated` callback**: for real-time access * **XCTest Attachments**: saved as a `.webloc` file in test results * **OS Logs**: logged at `.notice` level ### Retrieving in CI The session URL is saved as an XCTest attachment named `GPTDriver Session URL Link` with lifetime `.keepAlways`. Extract it from the `.xcresult` bundle in your CI pipeline. # Setup Source: https://docs.mobileboost.io/engineer-tooling/xcui-setup Set up the MobileBoost SDK with your XCUI test suite The MobileBoost SDK for XCUI integrates directly with `XCUIApplication`, adding self-healing and AI commands to your native iOS tests. ## Installation Add the [`gptd_swift`](https://github.com/MobileBoostHQ/gptd-swift) package to your Xcode project's UI testing target via **Swift Package Manager**: 1. In Xcode, go to your project settings 2. Select your **UI test target** 3. Click **Add Dependencies** 4. Enter the package URL: `https://github.com/MobileBoostHQ/gptd-swift` ## Configuration ### Native XCUITest Import the package and initialize `GptDriver` in your test's `setUp` method: ```swift theme={null} import XCTest import gptd_swift @MainActor final class DemoUITests: XCTestCase { var app: XCUIApplication! var gptDriver: GptDriver! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() gptDriver = GptDriver( apiKey: "", nativeApp: app ) } override func tearDownWithError() throws { app = nil } } ``` ## Caching To optimize execution time, enable caching so AI is only used when the test definition or app changes. | Mode | Description | | - | - | | `NONE` | No caching | | `FULL_SCREEN` | Cache hit only if the full screen matches a previous successful execution | | `INTERACTION_REGION` | Cache hit if the interaction region matches (recommended) | Set caching in the constructor: ```swift theme={null} gptDriver = GptDriver( apiKey: "", nativeApp: app, cachingMode: CachingMode.interactionRegion, testId: self.name ) ``` You can also override the caching mode per step for fine-grained control. Each successful run becomes the baseline that later runs of the same `testId` replay. To run a test that uses the cache without replacing its baseline, for example on every pull request, pass `excludeFromBaseline: true`. See [Keeping a run out of the baseline](/engineer-tooling/xcui-reference#keeping-a-run-out-of-the-baseline). # Artifacts Source: https://docs.mobileboost.io/qa-studio/artifacts Test execution artifacts generated from cloud device runs Each test run on a cloud device produces a detailed interaction log with the following artifacts. ## Video recording A full video of the test execution is captured, showing exactly what happened on the device screen during the run. ## Interaction log Each step in the test produces a log entry that includes: * **Screenshots**: captured at each step of the execution * **Commands executed**: the specific command or instruction that was run * **AI reasoning**: for steps handled by the AI, the reasoning behind the action taken is included ## Device logs System-level device logs are captured during the test run, providing additional context for debugging failures or unexpected behavior. ## Network logs When enabled, network logs capture HTTP traffic during the test execution, including: * The request URL and method * The request payload * The response payload This is useful for verifying API calls made by the app during the test flow. # Available commands Source: https://docs.mobileboost.io/qa-studio/available-commands Local commands available in the no-code test editor Use local commands when you want test steps that only use AI as fallback. Commands are matched by prefix, case-insensitively, after trimming whitespace. Values are usually wrapped in quotes: ```text theme={null} tapOn: "Continue" ``` ## Command reference | Command | Accepted values | What it does | | - | - | - | | `tapOn: ""` | Visible element text. `tapOn: center` is also supported. | Taps the matching text element or the center of the screen. | | `tapOn.id: ""` | Resource ID on Android or accessibility identifier on iOS. | Taps the matching element ID. | | `longPressOn: "" duration: ` | Visible element text. Duration is optional. | Long-presses the matching text element. Defaults to about 2 seconds when no duration is supplied. | | `longPressOn.id: "" duration: ` | Element ID. Duration is optional. | Long-presses the matching element ID. Defaults to about 2 seconds when no duration is supplied. | | `multiTap.text: "" count: ` | Visible element text. Count is optional. | Taps the matching text element multiple times. Defaults to `1` if count is omitted. | | `multiTap.id: "" count: ` | Element ID. Count is optional. | Taps the matching element ID multiple times. Defaults to `1` if count is omitted. | | `zoom: in` / `zoom: out` | `in` or `out`. | Performs a zoom gesture. | | `type: ""` | Any text. Quotes preserve whitespace and special characters. | Types text into the focused input. Zero-width characters are stripped before typing. | | `press: enter` | `enter`. | Presses Enter. Other keys are currently not handled by this command. | | `launchApp: ""` | App bundle or package ID. | Launches the specified app. | | `openLink: ""` | URL or deeplink. `{{extractedLink}}` is supported. | Opens a URL or deeplink in the session. | | `openAppClipLink: ""` | App Clip URL. | Opens an App Clip link. | | `restartApp` | No value. | Restarts the current app. | | `scroll: [ times]` | Direction: `up`, `down`, `left`, or `right`. Repeat count is optional. | Scrolls in the requested direction. For implementation purposes, scroll movement is inverted into the underlying swipe direction. | | `swipe: [ times]` | Direction: `up`, `down`, `left`, or `right`. Repeat count is optional. | Swipes in the requested direction one or more times. | | `slide: %;;;` | Direction: `up`, `down`, `left`, or `right`; percentage; and screen coordinates. | Performs a swipe or slide starting at the provided coordinates for the requested percentage of the screen. | | `scrollUpUntilVisible.text: ""` | Visible text. | Scrolls upward until the text is found, or fails if it cannot be found. | | `scrollDownUntilVisible.text: ""` | Visible text. | Scrolls downward until the text is found, or fails if it cannot be found. | | `scrollUpUntilVisible.id: ""` | Element ID. | Scrolls upward until the ID is found. Supported only for Android. | | `scrollDownUntilVisible.id: ""` | Element ID. | Scrolls downward until the ID is found. Supported only for Android. | | `scrollToBottom` | No value. | Repeatedly scrolls down until screenshots stop changing. | | `assertVisible.text: ""` | Visible text. | Passes if the text is visible. Otherwise, the step fails. | | `assertVisible.id: ""` | Element ID. | Passes if the element ID is visible. Otherwise, the step fails. | | `assertNotVisible.text: ""` | Text expected to be absent. | Passes if the text is not visible. Otherwise, the step fails. | | `assertNotVisible.id: ""` | Element ID expected to be absent. | Passes if the element ID is not visible. Otherwise, the step fails. | | `pressBackButton` | No value. | Presses Android Back. | | `wait: ` | Number of seconds. | Waits for the requested number of seconds. | | `removeText: ""` | Text whose length determines how many backspaces to send. | Sends one Backspace per character in the provided text. | | `go to: home` | `home`. | Sends the device to the home screen. | | `offlineMode: true` / `offlineMode: false` | Boolean string. | `true` disables network connectivity. `false` enables it again. Requires the offline-mode setting to be enabled. | | `curlRequest: ` | A cURL command. | Executes the request through the backend helper and remembers the JSON response for later steps. | | `takeScreenshot: "