> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobster.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Network and device conditions

> Set the location, language, permissions and network a check runs under, and assert what the app sent.

A check names the state the device is in while it runs, the network the app sees, and what the app must send and store. Mobster sets that state before the launch and puts it back when the run ends.

## Set the device's state

Add `conditions` to an existing [check file](https://docs.mobster.dev/checks.md#the-check-file). This example runs it in dark appearance with larger text:

```yaml theme={"theme":"css-variables"}
conditions:
  appearance: dark
  content_size: accessibility-extra-large
```

| Key | Values | Set with |
| - | - | - |
| `appearance` | `light`, `dark` | `simctl ui appearance` |
| `content_size` | Dynamic Type from `extra-small` to `accessibility-extra-extra-extra-large` | `simctl ui content_size` |
| `locale` | a language and region, such as `de_DE` or `fr-CA` | the launch arguments `-AppleLanguages` and `-AppleLocale`: the app changes language, not iOS |
| `increase_contrast` | `enabled`, `disabled` | `simctl ui increase_contrast` |
| `location` | `latitude` and `longitude`, or `"52.52,13.405"` | `simctl location set` |
| `route` | `waypoints`, 2 to 50 places written as `location` is, and `speed` in meters per second (default 20, up to 1000). The device moves along them. Use `route` or `location`, not both | `simctl location start` |
| `permissions` | a service (`all`, `calendar`, `contacts`, `contacts-limited`, `location`, `location-always`, `photos`, `photos-add`, `media-library`, `microphone`, `motion`, `reminders`, `siri`) set to `grant`, `revoke` or `reset` | `simctl privacy` |
| `media` | up to 20 photos, videos or contacts (`.jpg`, `.jpeg`, `.png`, `.heic`, `.gif`, `.mov`, `.mp4`, `.m4v`, `.vcf`) | `simctl addmedia` |
| `status_bar` | `time`, `battery_level` (0 to 100), `battery_state` (`charging`, `charged`, `discharging`), `wifi_mode` (`searching`, `failed`, `active`), `wifi_bars` (0 to 3), `cellular_mode` (`notSupported`, `searching`, `failed`, `active`), `cellular_bars` (0 to 4), `data_network` (`hide`, `wifi`, `3g`, `4g`, `lte`, `lte-a`, `lte+`, `5g`, `5g+`, `5g-uwb`, `5g-uc`), `operator_name` | `simctl status_bar override` |
| `push` | a JSON payload file with an `aps` object, up to 4,096 bytes | `simctl push`, once the app is in front |
| `biometric` | `enrolled`, `not_enrolled`: whether Face ID is enrolled on the simulator | the simulator's `notifyutil` enrollment event, the one `mobster sim biometric` sends |
| `network` | `offline`, `slow-3g`, `lte`, `flaky` | a local proxy that the app's `URLSession` is pointed at ([below](#slow-the-network-and-stub-requests)) |

`appearance`, `content_size` and `locale` take a list as well as one value. A check with `appearance: [light, dark]` and `content_size: [large, accessibility-large]` runs four times under `mobster test`, once for each combination, named "Home \[light, large]" and so on (at most 24 combinations). `mobster verify` runs one device state, so it refuses a check with lists and says to use `mobster test`.

Mobster checks the block when the file loads. A typo, a missing file or a value out of range stops the check before anything runs, and the message says what is wrong and what to write. A condition that can't be set when the check runs makes it `couldnt_run` (class `environment`): the check never runs in the wrong state.

When the run ends, Mobster puts back the appearance, content size, contrast, location or route, status bar, permissions and Face ID enrollment it changed. Photos and videos added with `media` stay in the simulator's library. Erase the simulator to clear them. `status_bar` `data_network` and `wifi_mode` change only what the status bar shows, not the connection.

On a real iPhone, Mobster sets only `appearance` and `locale`. `network` and `stubs` are refused there, and the check ends as couldn't run. Everything else changes a phone's state (its location, its permissions, its photos) or sends to a real token, so Mobster skips it. `reset` is not a condition: it is the check's own key, with the levels `none`, `data` and `reinstall` ([Reset levels](https://docs.mobster.dev/simulators.md#reset-levels)). Each result in `results.json` has `conditions.applied` and `conditions.skipped` (each skipped key and why), and the report notes them.

## Slow the network and stub requests

`conditions.network` limits the app's connection while the check runs, and `stubs` answers named requests with fixed responses. Both work on a simulator only.

```yaml theme={"theme":"css-variables"}
conditions:
  network: slow-3g
stubs:
  - match: { host: api.example.com, path_prefix: /v1/orders, method: GET }
    respond: { status: 200, json: { orders: [] }, delay_ms: 800 }
  - match: { host: "*.example.com" }
    respond: { status: 503 }
```

| Profile | Round trip | Down | Up | Stalls | Resets |
| - | - | - | - | - | - |
| offline | none: every connection is reset | none | none | none | all |
| slow-3g | 400 ms | 400 kbit/s | 400 kbit/s | 2% of chunks, 1 s each | none |
| lte | 50 ms | 12 Mbit/s | 5 Mbit/s | none | none |
| flaky | 150 ms | 1.5 Mbit/s | 750 kbit/s | 5% of chunks, 1 s each | 10% of connections |

A stall stands for a lost packet and its retransmission. `flaky` draws its stalls and resets from a fixed seed, so a check meets the same failures on every run unless its requests race each other. These profiles aren't Apple's Network Link Conditioner profiles.

A stub matches on `host` (a name, or `*.example.com`), `path_prefix` (default `/`) and `method` (default any), and the first match wins. `respond` takes `status` (default 200), a body from `json` or from `file` (a path relative to the project, up to 8 MiB) and `delay_ms` (up to 30,000). A request no stub matches goes to the real server, shaped by the profile. `offline` and `stubs` together are an error, because nothing can answer on a link that refuses everything.

Before the app launches, Mobster starts a proxy on `127.0.0.1` and launches the app with a small library (built with Xcode's compiler the first time and cached in `~/Library/Caches/Mobster/netshim`) that points the app's `URLSession` at it. Mobster changes no rule, setting or file on your Mac, asks for no password and doesn't touch other apps' traffic. The proxy lives as long as the run.

* The app keeps the library each time the run opens it, after a relaunch or a quit, and a library you name in the check's `DYLD_INSERT_LIBRARIES` loads beside it.
* A link that opens in the app while it has no process launches the app that way first: the check's `open_url`, the `open_url` tool, and, on a simulator, a link the model opens in one of the URL schemes the app declares (`CFBundleURLTypes`), or one Mobster maps to an app it knows, such as Settings.
* When iOS starts the app some other way, such as a tap on its Home Screen icon or a universal link, the app runs without the library, and the run ends `needs_review`, class `unlaunched_process`.
* Each run writes what the proxy saw to `network.json` in its run folder (method, host, path without the query, status, bytes, time, and whether the request was passed, stubbed, tunneled or refused). `conditions.network` in `results.json` repeats the first 200 rows.

The proxy reaches every `URLSession` (`shared`, `default` and `ephemeral`, over `http` and `https`), so it covers Swift and Objective-C apps, Alamofire and React Native's `fetch`. It doesn't reach:

* The app's view of the link. `NWPathMonitor`, Reachability and the status bar read your Mac's connection, so an offline screen that appears only when the system reports no network stays hidden. Under `offline`, an `http` request fails with `NSURLErrorNetworkConnectionLost` (-1005) and an `https` request with error 310 in `kCFErrorDomainCFNetwork`, never `NSURLErrorNotConnectedToInternet` (-1009).
* Traffic that doesn't use `URLSession`: Flutter's `dart:io`, gRPC, Cronet, Firebase's own transport, raw sockets and QUIC. When no request reaches the proxy, `network.json` says so.
* Web views, other apps and system services.
* An app that sets its own proxy in its `URLSessionConfiguration`.

An `https` stub needs the app to trust a certificate Mobster makes for the run. Mobster adds it to the simulator's trust store with `simctl keychain add-root-cert`. The run's private key is deleted when the run ends, and the public certificate stays in the simulator. Apps that pin certificates refuse a stubbed host, and the check fails with the app's own error. An `https` host without a stub passes through as a tunnel Mobster never opens, so pinned apps keep working under `network:` alone.

## Assert what the app sent and stored

`expect:` can also judge what the app sent over the network and what it wrote to disk, so a check can prove that "Add to cart" reached the server without reading a second screen. This works inside canvases and web views where the accessibility tree is empty, as long as the traffic uses `URLSession`. Set `MOBSTER_EXPERIMENTAL_NETWORK=1`, or Mobster refuses a check that uses it. It runs on a simulator only. On a real iPhone the check ends as couldn't run.

```yaml theme={"theme":"css-variables"}
steps:
  - Add the first item to the cart
expect:
  - text: Cart (1)
  - request: POST api.example.com/v1/cart
  - request: { method: POST, host: api.example.com, path: /v1/cart, status: 2xx, body: /"sku":"a1"/ }
  - no_request: { host: "*.tracker.example" }
  - storage: { file: Library/Preferences/*.plist }
  - storage: { sqlite: Library/app.db, query: "SELECT 1 FROM cart WHERE sku = 'a1'", at_least: 1 }
```

| Assertion | Holds when |
| - | - |
| `request` | The proxy saw at least one request that matches every part you wrote. `method`, `host` (a name or `*.example.com`), `path`, `status` (a number or a class such as `2xx`), `body` (in what the app sent) and `response` (in what came back) are all optional, but one of `method`, `host` and `path` is required. A string such as `POST api.example.com/v1/cart` or `/v1/cart` is the short form. |
| `no_request` | The app was seen on the network and no request matched. It takes `method`, `host`, `path` and `body`, and a bare host (`*.tracker.example`) is enough. |
| `storage` with `file` | A file that matches the glob, relative to the app's data container, was added, changed or deleted since the launch. `changed: false` holds when none was. `contains` also requires matching text in a matching file. |
| `storage` with `sqlite` | The `SELECT` or `WITH` query returns `equals`, `at_least` or `at_most` rows. Mobster runs it on a copy of the database, so the app's file is never opened for writing. |

`path` is a glob (`*` matches anything, including `/`) on the path, or a `/regex/`. A glob without `?` ignores the query string. A body or `response` is a case-insensitive substring, or a `/regex/`. Mobster reads gzip, deflate and chunked response bodies, and the first 64 KiB of each body.

A request that matches is the evidence: `POST api.example.com/v1/cart -> 200 at 4.2 s` in the report, and `wire.json` in the run folder with the request's headers (authorization, cookie, token and key headers are masked, other values go through the secret filter) and the first 200 characters of its body and response. A storage change shows the diff of a text or plist file, or the first rows of a query, masked the same way. `results.json` repeats this in the assertion's `matches`.

* Mobster opens an `https` connection only to a host a `request` or `no_request` names together with a `method`, `path`, `status`, `body` or `response`, because opening it needs the run's certificate. Name the host. Any other `https` connection is a tunnel: the assertions see its host, not its paths.
* A `no_request` with a `method`, `path` or `body` and no host can't be decided while the app has an unopened `https` connection, and fails with the host to name. With a host, it is decided on that host alone.
* If no request from the app reached the proxy at all, `request` and `no_request` both fail: the app may not use `URLSession`. Pair a `no_request` with a `request` or a screen assertion that proves the app was routed.
* The proxy records every request from launch to the end of the run, not only those after the steps.
* Pinned apps refuse the certificate for an opened host, as with stubs.
* `storage` compares the app's data container at the launch screen with the container at the end. Mobster reads it in place with `simctl get_app_container`. Preferences the app hasn't flushed to disk aren't there yet.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.