Mob.Differential (mob v0.8.3)

Copy Markdown View Source

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.

Summary

Types

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.

Functions

Compare two normalised view trees.

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

Types

divergence()

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

path()

@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()

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

Functions

compare(ios, android, opts \\ [])

@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(map)

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

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