# `Musubi.Testing`
[🔗](https://github.com/fahchen/musubi/blob/v0.3.0/lib/musubi/testing.ex#L1)

Test entry point for Musubi root stores, analogous to
`Phoenix.LiveViewTest`. Wraps `Musubi.Page.Server.start_link/1` with
test-friendly defaults and exposes the primary assertion surface
(`render/2`).

## Primary surface

Assert against the rendered wire-shape map — the same contract a
client observes — not internal `socket.assigns`. `assigns/2` is an
escape hatch for state not surfaced through `render/1`; prefer
`render/2` for contract assertions.

## Example

    page = Musubi.Testing.mount(RoomStore, %{"room_code" => "AB12"})
    Musubi.Testing.dispatch_command(page, :ko, %{"target" => "p2"})

    assert Musubi.Testing.render(page) == %{
      winner: :p1,
      hp: %{p1: 100, p2: 0}
    }

# `t`

```elixir
@type t() :: %Musubi.Testing{pid: pid(), root: module(), transport: pid()}
```

Handle returned by `mount/3`; passed back into the other helpers.

# `allow_upload`

```elixir
@spec allow_upload(t(), atom(), [map()], keyword(), Musubi.Socket.store_id()) ::
  {:ok, Musubi.Page.Server.preflight_reply()} | {:error, atom()}
```

Runs the `allow_upload` preflight against the mounted page and
returns the reply.

Bypasses the transport channel — useful for tests that exercise the
preflight + entry add-op path without a Phoenix channel in the way.

## Options

  * `:endpoint` — Phoenix endpoint module used to sign tokens.
    Defaults to `Musubi.Testing.TestEndpoint`, a stub endpoint
    automatically registered in test mode.

# `assigns`

```elixir
@spec assigns(t(), Musubi.Socket.store_id()) :: map()
```

Returns the raw `socket.assigns` for the addressed store.

Escape hatch — prefer `render/2` for contract assertions. Use only
when the value you need is not exposed through `render/1` (e.g. a
private field captured for later use).

# `dispatch_command`

```elixir
@spec dispatch_command(t(), atom(), map(), Musubi.Socket.store_id()) ::
  {:ok, map()} | {:error, term()}
```

Dispatches a command to a mounted store. Defaults to the root
(`store_id: []`); pass a child path to address a nested store.

Mirrors the client-side `proxy.dispatchCommand(name, payload)`
contract.

# `mount`

```elixir
@spec mount(module(), map(), keyword()) :: t()
```

Mounts `module` as a root page. Push patches are delivered to the
calling process; consume them with `ExUnit.Assertions.assert_receive/2`.
Tears down on test exit via `start_supervised!`.

## Options

  * `:transport_pid` — pid that receives push patches. Defaults to `self()`.

# `render`

```elixir
@spec render(t(), Musubi.Socket.store_id()) :: map()
```

Runs the addressed store's `render/1` against its current socket and
returns the wire-shape map. Primary assertion surface — what the
client would observe after the next reconcile.

Values are returned as native Elixir terms (atom literals stay atoms);
the JSON-string transformation happens on the way out to the client,
not inside `render/1`.

# `simulate_external_progress`

```elixir
@spec simulate_external_progress(
  t(),
  atom(),
  String.t(),
  non_neg_integer(),
  Musubi.Socket.store_id()
) ::
  :ok
```

Sends an external-mode progress event for an entry on the mounted page.

# `simulate_upload`

```elixir
@spec simulate_upload(
  t(),
  atom(),
  String.t(),
  non_neg_integer(),
  Musubi.Socket.store_id()
) :: :ok
```

Simulates a full upload for one entry: sends a single complete chunk
via the page server's `upload_channel_chunk` API, then waits for the
resulting patch envelope to land.

---

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