# `Mob.Test`
[🔗](https://github.com/genericjam/mob/blob/main/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)

# `assigns`

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

Return the current screen's assigns map.

# `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.

# `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 →", ...}}}]

# `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).

# `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}}

# `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.

# `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"}})

# `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)

# `tree`

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

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

---

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