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

The blessed way to unit-test a `Mob.Screen` in the BEAM, no device or
emulator required. The screen-level analog of `Phoenix.LiveViewTest`.

A `Mob.Screen` is a GenServer-shaped module: `mount/3` builds state,
`handle_event/3` and `handle_info/2` mutate it, and `render/1` turns the
assigns into a **view tree** (plain data: `%{type:, props:, children:}`). That
last part is the key difference from LiveView: the screen produces a typed
data structure, not an HTML string, so assertions are tree queries against
real data instead of brittle string matching.

This module drives those callbacks directly (the same thing the on-device
runtime does) and gives you query helpers whose vocabulary matches `Mob.Test`
(the device-side driver): `assigns/1`, `tree/1`, `find/3`, `flatten/1`. So a
test reads the same whether it runs here in milliseconds or, later, against a
real device.

    defmodule MyApp.CounterScreenTest do
      use Mob.ScreenCase

      test "increment bumps the count and the rendered text" do
        view = mount_screen(MyApp.CounterScreen)
        assert assigns(view).count == 0

        view = render_event(view, "increment")
        assert assigns(view).count == 1
        assert text(view) =~ "Count: 1"
        assert find(view, :button, tag: "increment")

        # cheap native-contract check: every node the screen emits is a
        # type the Compose / SwiftUI layer actually renders.
        assert_renderable(view)
      end
    end

## What this does and does not catch

This is tier 1 of the testing pyramid: it exercises **logic, state, and the
shape of the view tree**, fast and deterministically. `assert_renderable/2`
adds a tier-2 **contract** check (does the tree only use renderable node
types). Neither runs the native layer, so they cannot catch a node that
renders wrong or behaves wrong on a real iOS/Android build. That needs a
device test driven through `Mob.Test`. Weight your suite heavily toward this
module, with a thin band of device tests for the things only hardware proves.

# `assert_renderable`

```elixir
@spec assert_renderable(
  Mob.ScreenCase.View.t() | map(),
  keyword()
) :: map()
```

Assert every node in the tree is a type the native layer can render. Returns
the tree on success so it composes; flunks (with the offending types) if a
node uses a type that has no Compose / SwiftUI renderer.

This catches the "you emitted a node the native side can't draw" class of bug
at `mix test` time, no device needed. Pass extra types a plugin or your own
app registers via `:extra`:

    assert_renderable(view, extra: [:gauge])

# `assigns`

```elixir
@spec assigns(Mob.ScreenCase.View.t()) :: map()
```

The screen's current assigns. Mirrors `Mob.Test.assigns/1` (and uses it on device).

# `device_view`

```elixir
@spec device_view(node()) :: Mob.ScreenCase.View.t()
```

Wrap a running device `node` as a `View` so the query/assertion helpers read
it over `Mob.Test`'s Erlang-distribution RPC. The device-backed counterpart to
`mount_screen/3`. Use behind `@tag :on_device`; get the `node` from
`mix mob.connect` / `Mob.Test`. Driving (navigate, tap) stays on `Mob.Test`;
this is for asserting against the live screen with the same helpers.

    @tag :on_device
    test "the live home screen is renderable" do
      node = :"my_app_android@127.0.0.1"
      Mob.Test.navigate(node, MyApp.HomeScreen)
      view = device_view(node)
      assert_renderable(view)
      assert navigated_to(view) == MyApp.HomeScreen
    end

# `find`

```elixir
@spec find(Mob.ScreenCase.View.t() | map(), atom(), keyword()) :: map() | nil
```

The first node matching `find_all/3`, or `nil`.

# `find_all`

```elixir
@spec find_all(Mob.ScreenCase.View.t() | map(), atom(), keyword()) :: [map()]
```

All nodes of `type` whose props are a superset of `props`. Mirrors
`Mob.Test.find/2`, but matches on the typed tree rather than a substring.

    find_all(view, :button, tag: "increment")

# `flatten`

```elixir
@spec flatten(Mob.ScreenCase.View.t() | map()) :: [map()]
```

Every node in the tree, depth-first. Mirrors `Mob.Test.flatten_tree/1`.

# `mount_screen`

```elixir
@spec mount_screen(module(), map(), map()) :: Mob.ScreenCase.View.t()
```

Mount a screen and return a `View` handle. Calls `Mob.Socket.new/1` then the
screen's `mount/3`, asserting it returns `{:ok, socket}`.

# `navigated_to`

```elixir
@spec navigated_to(Mob.ScreenCase.View.t()) :: term() | nil
```

The screen the last event navigated to, or `nil` if none.

Returns the destination **module** on both backends, so the same assertion
reads identically whether the test ran in-BEAM or against a device:

    assert navigated_to(view) == MyApp.CounterScreen

  * in-BEAM: the destination of the nav action recorded on the socket by
    `Mob.Socket.push_screen/3` and friends. Destination-bearing actions
    (`{:push, Dest, _}`, `{:reset, Dest, _}`, `{:pop_to, Dest}`) return
    `Dest`; destinationless ones (`{:pop}`, `{:pop_to_root}`,
    `{:switch_tab, tab}`) return the raw action unchanged.
  * on device: the screen currently showing (`Mob.Test.screen/1`).

# `render_event`

```elixir
@spec render_event(Mob.ScreenCase.View.t(), String.t(), map()) ::
  Mob.ScreenCase.View.t()
```

Dispatch a `handle_event/3` (the explicit-event style) and return the updated
`View`. In-BEAM only; on a device, drive with `Mob.Test.tap/2`.

# `render_info`

```elixir
@spec render_info(Mob.ScreenCase.View.t(), term()) :: Mob.ScreenCase.View.t()
```

Deliver a message to the screen's `handle_info/2` and return the updated
`View`. This is how taps reach a screen on device (a `Button`'s `on_tap`
sends a message), so it is the in-BEAM equivalent of a tap. In-BEAM only.

# `renderable_types`

```elixir
@spec renderable_types() :: MapSet.t(atom())
```

The set of node types the native layer can render: the core component tags
plus `:native_view`. The contract surface `assert_renderable/2` checks against.

# `text`

```elixir
@spec text(Mob.ScreenCase.View.t() | map()) :: String.t()
```

Concatenated text of every `:text` node in the tree, joined by spaces.

# `tree`

```elixir
@spec tree(Mob.ScreenCase.View.t() | map()) :: map()
```

The current view tree, from in-BEAM render or the device. Mirrors `Mob.Test.tree/1`.

---

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