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

Compare two `Mob.Test.view_tree/1` snapshots and report the first divergence.

Mob's product claim is one design, both platforms. A comparator over the
semantic trees each platform returns is what makes that claim testable — the
first thing that differs is the framework failing its own promise.

This module is deliberately pure: it takes two normalised view-trees and
answers `:ok` or `{:divergence, %{...}}`. Rendering the same fixture on both
platforms and driving the round trip belongs to `mob_dev`, not here — the
comparator is easy to test without a device, and hard to test *with* one.

## The rules

Each rule guards a class of platform bug and skips the classes that would
produce false alarms until the framework can report them faithfully.

* **Structure.** Different `type` at a matched position, or a different
  number of children at any level, is always a divergence. This is the
  class MOB-147 B-1 falls into — one platform renders a node type the other
  does not.

* **Label / value.** A `text` prop that only reaches the tree on one side
  (a rendering path that drops it), or `value` on an input that differs, is
  a divergence.

* **Frame.** Compared only when *both* sides carry a frame. Android's
  `MobBridge.uiViewTree()` reports a frame only for nodes with `props["id"]`
  (only those are tracked by `Modifier.onGloballyPositioned`), so any node
  without an id has `frame: nil` on Android and a real frame on iOS.
  Reporting that as a divergence would flag every non-id'd node in every
  fixture — the fixture author chooses which nodes need geometry compared by
  giving them ids. Numbers are compared with a tolerance, default 1.0 dp;
  small differences are ordinary rounding across the platform layout
  engines.

* **Root frame is not compared.** The synthetic root's frame is the screen,
  and comparing screen sizes across an iPad and a Motorola G says nothing
  about the framework.

* **Class, bg_color, text_color are not compared yet.** `bg_color` and
  `text_color` are `null` on Android today (paint resolves through the theme
  after the tree is built), and comparing them would treat every colour as
  a divergence. When Android surfaces those fields the rules can be added
  without a shape change here.

Report only the *first* divergence walked in child order. Depth-first, so a
parent's own divergence is reported before its children's; among children,
the leftmost wins. That keeps the reader looking at cause rather than
cascade.

# `divergence`

```elixir
@type divergence() :: %{path: path(), reason: atom(), ios: term(), android: term()}
```

# `path`

```elixir
@type path() :: [non_neg_integer()]
```

Path to a divergent node, as a list of child indices from the root.
`[]` is the root itself; `[0, 2]` is the third child of the first child.

# `result`

```elixir
@type result() :: :ok | {:divergence, divergence()}
```

# `compare`

```elixir
@spec compare(map() | term(), map() | term(), keyword()) ::
  result() | {:error, :not_ready}
```

Compare two normalised view trees.

Options:
  * `:frame_tolerance_dp` — max absolute difference per frame coordinate
    before geometry is reported as divergent. Default `1.0`.

Either side may be the atom `:no_window`, `nil` or `{:error, reason}` — the
shapes `Mob.Test.view_tree/1` can return when a device is not ready. The
comparator refuses to report divergence in that case (`{:error, :not_ready}`),
because "one side did not answer" is not a bug in the tree, it is a bug in
the harness.

# `describe`

```elixir
@spec describe(divergence()) :: String.t()
```

One-line human summary of a divergence, for a log line or a defect report.

---

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