# `Mob.Test`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/test.ex#L1)

Remote inspection and interaction helpers for connected Mob apps.

All functions accept a `node` atom and operate on the running screen via
Erlang distribution. Connect first with `mix mob.connect`, then use these
from IEx or from an agent via `:rpc.call/4`.

## Quick reference

    node = :"my_app_ios@127.0.0.1"

    # Inspection
    Mob.Test.screen(node)               #=> MyApp.HomeScreen
    Mob.Test.assigns(node)              #=> %{count: 3, ...}
    Mob.Test.tree(node)                 #=> %{type: :column, ...}
    Mob.Test.find(node, "Save")         #=> [{[0, 2], %{...}}]
    Mob.Test.inspect(node)              #=> %{screen: ..., assigns: ..., tree: ...}

    # Interaction
    Mob.Test.tap(node, :increment)      # tap a button by tag
    Mob.Test.back(node)                 # system back gesture
    Mob.Test.pop(node)                  # pop to previous screen (synchronous)
    Mob.Test.navigate(node, MyApp.DetailScreen, %{id: 42})
    Mob.Test.pop_to(node, MyApp.HomeScreen)
    Mob.Test.pop_to_root(node)

    # Lists
    Mob.Test.select(node, :my_list, 0)  # select first row

    # Device API simulation
    Mob.Test.send_message(node, {:permission, :camera, :granted})
    Mob.Test.send_message(node, {:camera, :photo, %{path: "/tmp/photo.jpg", width: 1920, height: 1080}})
    Mob.Test.send_message(node, {:location, %{lat: 43.65, lon: -79.38, accuracy: 10.0, altitude: 80.0}})
    Mob.Test.send_message(node, {:notification, %{id: "n1", title: "Hi", body: "Hey", data: %{}, source: :push}})

## Tap vs send_message

`tap/2` is for UI interactions that go through `handle_event/3` via the native
tap registry. `send_message/2` delivers any term directly to `handle_info/2`.
Use `send_message/2` to simulate async results from device APIs (camera, location,
notifications, etc.) without having to trigger the actual hardware.

## Synchronous vs fire-and-forget

Navigation functions (`pop`, `navigate`, `pop_to`, `pop_to_root`) are synchronous —
they block until the navigation and re-render complete. This makes them safe to
follow immediately with `screen/1` or `assigns/1` to verify the result.

`back/1` and `send_message/2` are fire-and-forget (they send a message to the
screen process and return immediately). Use `:sys.get_state/1` as a sync point
if you need to wait before reading state:

    Mob.Test.send_message(node, {:permission, :camera, :granted})
    :rpc.call(node, :sys, :get_state, [:mob_screen])  # flush mailbox
    Mob.Test.assigns(node)

## Two layers of inspection: render tree vs native UI

`Mob.Test` exposes two complementary views of what the app is showing:

| API                           | Source                              | When to use |
|-------------------------------|-------------------------------------|-------------|
| `tree/1`, `find/2`            | Mob render tree (logical components) | Mob apps you control. Fast, exact, has `on_tap` tags, no AX activation needed. |
| `view_tree/1`, `find_view/2`  | Native view hierarchy via NIF       | Native pixel frames; works for any app on iOS UIKit; shallow on SwiftUI/Compose. |
| `ui_tree/1`                   | OS accessibility tree               | What sighted users read; works on any app *if* AX is active (iOS: VoiceOver). Strict superset of `view_tree` for UIKit; the only path to semantics inside SwiftUI/Compose. |

Choose render tree first if your app is Mob-rendered. Reach for `view_tree`
when you want native frames or geometry. Reach for `ui_tree` when you need
to inspect non-Mob content (alerts, system overlays, third-party SDK UI),
or to verify the *rendered* state matches the logical render.

## Driving controls beyond plain taps

- **Buttons / nav items** — `tap/2` (by tag, fastest), or
  `mob_nif:tap/1` (by accessibility label), or `tap_xy/3` (by coordinate).
- **Sliders, steppers, pickers** — `adjust_slider/4` and the underlying
  `ax_action/3` / `ax_action_at_xy/4` use `accessibilityIncrement` /
  `accessibilityDecrement`. Synthetic drag gestures don't fire SwiftUI's
  `DragGesture` reliably; AX actions do.
- **Switches / toggles** — `toggle/2` finds the switch by nearby label and
  activates it via the AX path (sends `accessibilityActivate`).
- **Modals / alerts / sheets** — `dismiss_alert/2` uses
  `accessibilityActivate` on the named button; `ax_action/3` with
  `:escape` sends `accessibilityPerformEscape`.
- **Scroll views** — `ax_action/3` with `:scroll_up`/`:scroll_down`/
  `:scroll_left`/`:scroll_right` sends `accessibilityScroll:`.
- **System back** — `back/1` (Mob screens, framework-level) or — for
  sidecar mode against arbitrary apps — synthetic edge-pan via `swipe/5`
  from `x=0`, but iOS owns that gesture above the app process and the
  synthetic pan won't fire. Use `back/1` for Mob, document the limitation
  for sidecar.

## Platform support matrix

| Helper                       | iOS sim       | iOS device    | Android         |
|------------------------------|---------------|---------------|-----------------|
| `screen/1`, `assigns/1`      | ✅            | ✅            | ✅              |
| `tap/2` (by tag)             | ✅            | ✅            | ✅              |
| `back/1`, `pop/1`, `navigate`| ✅            | ✅            | ✅              |
| `send_message/2`             | ✅            | ✅            | ✅              |
| `screen_info/1`              | ✅            | ✅            | ✅              |
| `view_tree/1`                | ✅ (shallow†) | ✅ (shallow†) | ✅ (root only‡) |
| `find_view/2`                | ✅            | ✅            | ✅              |
| `ui_tree/1` (legacy AX)      | ⚠️ AX active§ | ⚠️ AX active§ | ❌ not_loaded   |
| `ax_action/3`                | ⚠️ AX active§ | ⚠️ AX active§ | ❌ not_supported |
| `ax_action_at_xy/4`          | ⚠️ AX active§ | ⚠️ AX active§ | ❌ not_supported |
| `toggle/2`                   | ⚠️ AX active§ | ⚠️ AX active§ | ❌ ui_tree_unavailable |
| `dismiss_alert/2`            | ⚠️ AX active§ | ⚠️ AX active§ | ❌ ui_tree_unavailable |
| `adjust_slider/4`            | ⚠️ AX active§ | ⚠️ AX active§ | ❌ ui_tree_unavailable |
| `tap_xy/3`                   | ✅ (AX path)  | ✅ (HID inj.) | n/a             |
| `swipe/5`                    | ⚠️ scroll only| ✅ (HID inj.) | n/a             |

- **†** SwiftUI doesn't expose its content as separate UIView instances —
  `view_tree` reaches the SwiftUI hosting view's container and stops.
  For semantic content on Mob screens use `tree/1` (render tree); for any
  other SwiftUI-based content use `ui_tree/1`.
- **‡** Android's Mob renderer is Compose. The View walk stops at the
  `AndroidComposeView` host. The eventual fix is `Modifier.onGloballyPositioned`
  in Mob's components writing to a registry the NIF reads (planned).
  See `issues.md` #11.
- **§** "AX active" means an iOS accessibility client is asking for the
  AX tree so SwiftUI materializes it. Today: VoiceOver toggle. Production:
  `XCAXClient_iOS` activation, debug-only — see WireTap stretch goals in
  `future_developments.md`.

Helpers that depend on AX return clear error tuples on Android instead of
raising. Callers should match on `{:error, :not_supported_on_android}` and
`{:error, :ui_tree_unavailable}` and either skip or fall back to
`send_message/2` for state mutations.

## Known limitations affecting AX automation

Even on iOS with AX active, three Mob component defects keep the natural
paths from working today. Workarounds in each helper's docstring:

- **Slider** — `accessibilityIncrement`/`Decrement` are no-ops because
  Mob's iOS Slider doesn't attach `.accessibilityAdjustableAction`.
  See `issues.md` #7.
- **Toggle** — the `label:` prop doesn't reach the AX tree; `toggle/2`
  can't find the switch by label name. Use `ax_action_at_xy/4` with
  coordinates for now. See `issues.md` #8.
- **Alert OK button** — `accessibilityActivate` on the AX-tree button
  doesn't fire the underlying `UIAlertAction`. Use Mob `Alert` with
  `action:` atoms and `send_message/2` to dismiss programmatically.
  See `issues.md` #9.

System-level gestures iOS owns *above* the app process (edge-pan back,
swipe-up app switcher, pull-down notification center) are out of reach
for in-process synthetic touches on physical devices. Use `back/1` for
Mob screens; for sidecar mode against arbitrary apps, document the
limitation rather than promising the gesture.

# `adjust_slider`

```elixir
@spec adjust_slider(node(), String.t(), float(), keyword()) ::
  {:ok, float()} | {:error, term()}
```

Step a slider toward a target percentage (0.0..1.0) using accessibility
increment/decrement actions. Reliable when synthetic-drag won't fire
(SwiftUI Slider's DragGesture ignores in-process touches on iOS).

`match` is a substring of the slider's label or value (e.g. `"Volume"`).
`target` is a fraction 0.0..1.0. `max_steps` caps the increment loop
(default 30) so a wrong match can't spin forever.

Returns `{:ok, final_pct}` or `{:error, reason}`.

    Mob.Test.adjust_slider(node, "Volume", 0.30)
    #=> {:ok, 0.30}

Implementation note: each AX increment/decrement on a SwiftUI slider moves
by the slider's `.step` value (default 0.10 of the range). The function
re-reads the slider value after each step to converge.

## Known limitation (issues.md #7)

Mob's iOS Slider component does not currently attach
`.accessibilityAdjustableAction { … }`, so `accessibilityIncrement` and
`accessibilityDecrement` are silently dropped by SwiftUI even though the
NIF returns `:ok`. This helper currently returns `{:error,
:max_steps_exhausted}` against an unfixed slider. Until issue #7 lands,
drive sliders via `Mob.Test.send_message(node, {:change, :slider_tag, value})`.

# `assigns`

```elixir
@spec assigns(node()) :: map()
```

Return the current screen's assigns map.

# `ax_action`

```elixir
@spec ax_action(node(), String.t(), atom()) :: :ok | {:error, atom()}
```

Invoke an accessibility action on the first AX element matching `match`.

## Platform support

- **iOS**: works once AX is active (today: VoiceOver on; future:
  `XCAXClient_iOS` activation, see `future_developments.md`).
- **Android**: returns `{:error, :not_supported_on_android}`. The Compose
  semantics walker is queued under WireTap (issues.md #11).

Used for controls where synthetic touches don't reach the gesture recognizer
(sliders, scrolls, modal dismissal).

`match` is a string searched in both label and value. `action` is one of:
`:increment`, `:decrement`, `:activate`, `:escape`, `:scroll_up`,
`:scroll_down`, `:scroll_left`, `:scroll_right`.

    Mob.Test.ax_action(node, "Volume", :decrement)
    Mob.Test.ax_action(node, "Cancel", :activate)

# `ax_action_at_xy`

```elixir
@spec ax_action_at_xy(node(), number(), number(), atom()) :: :ok | {:error, atom()}
```

Invoke an AX action on whatever element occupies the given screen coordinates.

Useful when label/value substring matching is ambiguous (e.g. multiple
sliders that all read "50%", a toggle whose accessibility label is empty).
Caller picks coordinates from `ui_tree/1` and points at the exact element.

    Mob.Test.ax_action_at_xy(node, 187.0, 296.0, :increment)

## Platform support

- **iOS**: works once AX is active (VoiceOver on, today).
- **Android**: returns `{:error, :not_supported_on_android}` — see
  `ax_action/3`.

# `back`

```elixir
@spec back(node()) :: :ok
```

Simulate the system back gesture (Android hardware back / iOS edge-pan).

Fire-and-forget. The framework pops the navigation stack; if already at the
root, it exits the app. Prefer `pop/1` when you need to know that navigation
has finished before reading state.

# `clear_text`

```elixir
@spec clear_text(node()) :: :ok | {:error, atom()}
```

Clear all text in the focused input (select-all + delete).

# `delete_backward`

```elixir
@spec delete_backward(node()) :: :ok | {:error, atom()}
```

Delete one character behind the cursor (backspace).

# `dismiss_alert`

```elixir
@spec dismiss_alert(node(), String.t()) :: :ok | {:error, atom()}
```

Dismiss a modal/alert overlay by tapping its first button labelled with
`button_label` (e.g. "OK", "Cancel"). Mirrors what a user does when an
alert pops up.

    Mob.Test.dismiss_alert(node, "OK")

## Known limitation (issues.md #9)

UIAlertController exposes its buttons twice in the AX tree (visual view +
action target). Activating the visual view doesn't fire the action. This
helper currently reports `:ok` while the alert stays on screen. Workaround:
define alerts with `action: :tag_atom` and dismiss via
`Mob.Test.send_message(node, {:alert, :tag_atom})`.

# `find`

```elixir
@spec find(node(), String.t()) :: [{list(), map()}]
```

Find all nodes in the current tree whose text contains `substring`.
Returns a list of `{path, node}` tuples where `path` is a list of
indices from the root.

    Mob.Test.find(node, "Device APIs")
    #=> [{[0, 1, 8], %{"type" => "button", "props" => %{"text" => "Device APIs →", ...}}}]

# `find_native`

```elixir
@spec find_native(node(), String.t()) :: list()
```

Find elements in the native accessibility tree whose label or value contains `text`.

    Mob.Test.find_native(node, "Increment")
    #=> [{:button, "Increment", "", {164.0, 400.0, 54.0, 54.0}}]

# `find_view`

```elixir
@spec find_view(node(), String.t()) :: [{[non_neg_integer()], map()}]
```

Find nodes in the view tree whose label or value contains `text`.

Returns `[{path, node}]` for each match. Faster and more accurate than
`find_native/2` (no AX dependency, sees all views).

    Mob.Test.find_view(node, "Roll Dice")
    #=> [{[0, 0, 0, 4], %{type: :button, label: "Roll Dice", ...}}]

# `flatten_tree`

```elixir
@spec flatten_tree(map()) :: [{[non_neg_integer()], map()}]
```

Flatten an already-fetched view tree. Pure function — useful for tests
and for inspecting a captured tree without re-fetching.

    tree = Mob.Test.view_tree(node)
    flat = Mob.Test.flatten_tree(tree)

# `inspect`

```elixir
@spec inspect(node()) :: map()
```

Return a map with `:screen`, `:assigns`, `:nav_history`, and `:tree`
(the raw render tree from calling `render/1` on the current screen).

# `key_press`

```elixir
@spec key_press(node(), atom()) :: :ok | {:error, atom()}
```

Press a special key on the focused text input.

Keys: `:return` | `:tab` | `:escape` | `:space`

    Mob.Test.key_press(node, :return)
    Mob.Test.key_press(node, :escape)

# `locate`

```elixir
@spec locate(atom() | String.t()) :: {:ok, map()} | {:error, :not_found}
```

Locate an element by visible label text or accessibility ID (tag atom name).
Returns the element's screen frame.

Requires `idb` (iOS) to be installed.

    Mob.Test.locate(:save)
    #=> {:ok, %{x: 0.0, y: 412.0, width: 402.0, height: 44.0}}

    Mob.Test.locate("Save")
    #=> {:ok, %{x: 0.0, y: 412.0, width: 402.0, height: 44.0}}

# `long_press_xy`

```elixir
@spec long_press_xy(node(), number(), number(), non_neg_integer()) ::
  :ok | {:error, atom()}
```

Long-press at screen coordinates for `duration_ms` milliseconds (default 800ms).

    Mob.Test.long_press_xy(node, 195.0, 400.0)
    Mob.Test.long_press_xy(node, 195.0, 400.0, 1200)

# `navigate`

```elixir
@spec navigate(node(), module() | atom(), map()) :: :ok
```

Push a new screen onto the navigation stack. Synchronous.

`dest` is a screen module or a registered name atom (from `navigation/1`).
`params` are passed to the new screen's `mount/3`.

    Mob.Test.navigate(node, MyApp.DetailScreen, %{id: 42})
    Mob.Test.navigate(node, :detail, %{id: 42})
    Mob.Test.navigate(node, MyApp.SettingsScreen)

# `pop`

```elixir
@spec pop(node()) :: :ok
```

Pop the current screen and return to the previous one. Synchronous.

Returns `:ok` once the navigation and re-render are complete, so it is safe
to call `screen/1` or `assigns/1` immediately after.

No-op (returns `:ok`) if already at the root of the stack.

# `pop_to`

```elixir
@spec pop_to(node(), module() | atom()) :: :ok
```

Pop the stack until `dest` is at the top. Synchronous.

`dest` is a screen module or registered name atom. No-op if not in history.

# `pop_to_root`

```elixir
@spec pop_to_root(node()) :: :ok
```

Pop all screens back to the root of the current stack. Synchronous.

# `reset_to`

```elixir
@spec reset_to(node(), module() | atom(), map()) :: :ok
```

Replace the entire navigation stack with a new root screen. Synchronous.

Use this to simulate auth transitions (e.g. login → home with no back button).

# `screen`

```elixir
@spec screen(node()) :: module()
```

Return the current screen module.

# `screen_info`

```elixir
@spec screen_info(node()) :: map()
```

Return screen geometry in logical units (points on iOS, dp on Android).

    Mob.Test.screen_info(node)
    #=> %{
    #     width: 393.0, height: 852.0, scale: 3.0,
    #     safe_area: %{top: 59.0, bottom: 34.0, left: 0.0, right: 0.0}
    #   }

`:scale` is the device-pixel ratio (UIScreen.scale on iOS, displayMetrics.density
on Android). All other values are already in logical units; no further conversion
needed in the agent.

# `select`

```elixir
@spec select(node(), atom(), non_neg_integer()) :: :ok
```

Select a row in a `:list` component by index.

`list_id` must match the `:id` prop on the `type: :list` node. `index` is
zero-based. Delivers `{:select, list_id, index}` to `handle_info/2`.

Fire-and-forget.

    Mob.Test.select(node, :my_list, 0)   # first row

# `send_message`

```elixir
@spec send_message(node(), term()) :: :ok
```

Send an arbitrary message to the screen's `handle_info/2`. Fire-and-forget.

Use this to simulate results from device APIs without triggering real hardware:

    # Permissions
    Mob.Test.send_message(node, {:permission, :camera, :granted})
    Mob.Test.send_message(node, {:permission, :notifications, :denied})

    # Camera
    Mob.Test.send_message(node, {:camera, :photo, %{path: "/tmp/photo.jpg", width: 1920, height: 1080}})
    Mob.Test.send_message(node, {:camera, :cancelled})

    # Location
    Mob.Test.send_message(node, {:location, %{lat: 43.6532, lon: -79.3832, accuracy: 10.0, altitude: 80.0}})
    Mob.Test.send_message(node, {:location, :error, :denied})

    # Photos / Files
    Mob.Test.send_message(node, {:photos, :picked, [%{path: "/tmp/photo.jpg", width: 800, height: 600}]})
    Mob.Test.send_message(node, {:files, :picked, [%{path: "/tmp/doc.pdf", name: "doc.pdf", size: 4096}]})

    # Audio / Motion / Scanner
    Mob.Test.send_message(node, {:audio, :recorded, %{path: "/tmp/audio.aac", duration: 12}})
    Mob.Test.send_message(node, {:motion, %{ax: 0.1, ay: 9.8, az: 0.0, gx: 0.0, gy: 0.0, gz: 0.0}})
    Mob.Test.send_message(node, {:scan, :result, %{type: :qr, value: "https://example.com"}})

    # Notifications
    Mob.Test.send_message(node, {:notification, %{id: "n1", title: "Hi", body: "Hello", data: %{}, source: :push}})
    Mob.Test.send_message(node, {:push_token, :ios, "abc123def456"})

    # Biometric
    Mob.Test.send_message(node, {:biometric, :success})
    Mob.Test.send_message(node, {:biometric, :failure, :user_cancel})

    # Custom
    Mob.Test.send_message(node, {:my_event, %{key: "value"}})

# `swipe`

```elixir
@spec swipe(node(), number(), number(), number(), number()) :: :ok | {:error, atom()}
```

Swipe from (x1, y1) to (x2, y2). Drives UIScrollView contentOffset on
simulator; synthesises a drag gesture on real device.

    Mob.Test.swipe(node, 195.0, 500.0, 195.0, 100.0)   # scroll down

# `tap`

```elixir
@spec tap(node(), atom()) :: :ok
```

Send a tap event to the current screen by tag atom.

The tag comes from `on_tap: {self(), :tag_atom}` in the screen's `render/1`.
Check the screen's render function to find available tags.

Fire-and-forget — does not wait for the screen to finish processing.

    Mob.Test.tap(node, :save)
    Mob.Test.tap(node, :open_detail)

# `tap_native`

```elixir
@spec tap_native(atom() | String.t()) :: :ok | {:error, term()}
```

Locate an element and tap it via the simulator's native UI mechanism.

Requires `idb` (iOS) to be installed. Exercises the full native gesture path
rather than sending a BEAM message — useful for testing gesture recognizers
or verifying that the native layer wired up the tap handler correctly.

Prefer `tap/2` for testing Elixir logic; use `tap_native/1` when you need
the native path.

    Mob.Test.tap_native("Save")      # by visible text
    Mob.Test.tap_native(:save)       # by accessibility_id (= tag atom name)

# `tap_xy`

```elixir
@spec tap_xy(node(), number(), number()) :: :ok | {:error, atom()}
```

Tap at screen coordinates on the native app. On simulator uses accessibility
activation; on real device synthesises a UITouch via IOHIDEvent.

    Mob.Test.tap_xy(node, 289.7, 518.8)

# `toggle`

```elixir
@spec toggle(node(), String.t()) :: :ok | {:error, atom()}
```

Toggle a switch by a label substring. SwiftUI exposes `Toggle` as a button
with an empty accessibility label and value `"0"` or `"1"` — so we find the
Text element matching `label_match`, then activate the next button below it.

    Mob.Test.toggle(node, "Notifications")

## Known limitation (issues.md #8)

Mob's iOS Toggle component does not currently surface its `label:` prop as
a separate `:text` AX element, so `find_label_y/2` returns
`{:error, :label_not_found}`. Workaround: use `ax_action_at_xy/4` directly
with the toggle's frame from `ui_tree/1` (filter for `:button` with value
`"0"` or `"1"`). Once issue #8 lands, this helper works as documented.

# `tree`

```elixir
@spec tree(node()) :: map()
```

Return the current rendered tree (calls render/1 on the live assigns).

# `type_text`

```elixir
@spec type_text(node(), String.t()) :: :ok | {:error, atom()}
```

Type text into the currently focused text field.

Tap the field first to give it focus, then call this function.

    Mob.Test.tap_xy(node, 195.0, 300.0)
    Process.sleep(100)
    Mob.Test.type_text(node, "hello@example.com")

# `ui_tree`

```elixir
@spec ui_tree(node()) :: list()
```

Return the live accessibility tree from the running native app.

Each element is a tuple: `{type, label, value, {x, y, w, h}}`

    Mob.Test.ui_tree(node)
    #=> [{:button, "Increment", "", {164.0, 400.0, 54.0, 54.0}}, ...]

# `view_tree`

```elixir
@spec view_tree(node()) :: map() | {:error, term()}
```

Return the live UI tree as a nested map, walking native views directly.

Unlike `ui_tree/1` (which uses the accessibility subsystem and requires
VoiceOver activation on iOS), this walks UIView/View hierarchies directly:
no AX activation needed.

## Coverage caveat

- **UIKit apps (sidecar mode)**: full UIView hierarchy with labels and frames.
- **SwiftUI apps (current Mob)**: shallow — SwiftUI doesn't expose its content
  as separate UIView instances under the hosting view. You'll see containers
  and scroll views but not individual buttons/text. For Mob apps, prefer
  `Mob.Test.tree/1` (the logical render tree, which has all the semantic info)
  or `Mob.Test.ui_tree/1` (AX walk, requires VoiceOver activation).
- **Android (planned)**: a registry populated via `onGloballyPositioned` in
  Mob's Compose components — see `future_developments.md` "WireTap" section.

Returns a nested map:

    %{
      type: :root, label: nil, value: nil,
      frame: {0.0, 0.0, 393.0, 852.0},
      children: [
        %{type: :window, ..., children: [
          %{type: :scroll, ..., children: [
            %{type: :button, label: "Roll Dice",
              frame: {24.0, 416.0, 327.0, 53.5}, children: []}
          ]}
        ]}
      ]
    }

On Android, the JSON returned by `mob_nif:ui_view_tree/0` is decoded here.

# `view_tree_flat`

```elixir
@spec view_tree_flat(node()) :: [{[non_neg_integer()], map()}]
```

Return the view tree flattened to a list of `{path, node}` tuples.

`path` is the list of child indices from the root — e.g. `[0, 2, 1]` is
"the second child of the third child of the first child of the root."

Useful for filter/find — see `find_view/2`.

    Mob.Test.view_tree_flat(node)
    #=> [
    #     {[], %{type: :root, ...}},
    #     {[0], %{type: :window, ...}},
    #     {[0, 0], %{type: :scroll, ...}},
    #     ...
    #   ]

# `wait_for`

```elixir
@spec wait_for(node(), (list() -&gt; boolean()), keyword()) :: :ok | {:error, :timeout}
```

Wait until `predicate` returns true when called with the current `ui_tree`,
polling every `interval_ms` until `timeout_ms` elapses.

    Mob.Test.wait_for(node, fn tree ->
      Enum.any?(tree, fn {_, label, _, _} -> label == "Success" end)
    end)

# `wait_for_text`

```elixir
@spec wait_for_text(node(), String.t(), keyword()) :: :ok | {:error, :timeout}
```

Wait until an element whose label or value contains `text` appears in the
accessibility tree.

    Mob.Test.wait_for_text(node, "Welcome")
    Mob.Test.wait_for_text(node, "Error", timeout_ms: 2000)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
