Mob.ScreenCase: blessed in-BEAM screen testing (+ contract check, device backend)#44
Merged
Conversation
Tier-1 screen unit testing, the Phoenix.LiveViewTest analog. `use Mob.ScreenCase`
gives mount_screen/3, render_event/3, render_info/2, assigns/1, plus tree queries
(tree/find/find_all/text/flatten) whose vocabulary mirrors Mob.Test (the device
driver), so the same assertions read identically in-BEAM or, later, on device.
Key difference from LiveView: render returns a typed view tree
(%{type, props, children}), so assertions query real data, not HTML strings.
assert_renderable/2 adds a tier-2 contract check: every node type must be
renderable, derived at compile time from priv/tags/{ios,android}.txt (the same
authoritative source the ~MOB sigil validates against) plus :native_view, with
an :extra opt for plugin/custom types. Catches 'emitted a node the native layer
cannot draw' at mix test time, no device.
15 tests; credo --strict clean. Prototype for review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffolding a real test from mob.new surfaced this: the generated home screen reads the theme from Mob.State in mount/3, and Mob.State is DETS-backed, so every screen that touches it crashed in mix test with a :dets argument error. A blessed screen Case should own that runtime, like ConnCase owns the Ecto sandbox. The setup starts Mob.State per test against a throwaway MOB_DATA_DIR so screen tests just work and never pollute the app's real dev state. Idempotent (reuses an already-running Mob.State). Existing 15 tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
navigated_to/1: assert what an event navigated to. In-BEAM it reads the
nav action Mob.Socket.push_screen/3 records on the socket (e.g.
{:push, Dest, params}); on device it returns the live screen via Mob.Test.
Device backend: the View now carries a :source (:beam | :device). device_view/1
wraps a running node, and tree/1, assigns/1, navigated_to/1 dispatch to Mob.Test
over Erlang-distribution RPC. The query + assertion layer (find/text/flatten/
assert_renderable) is tree-based, so the SAME assertions run in-BEAM or against
hardware; only driving differs (render_event/render_info in-BEAM, Mob.Test.tap/
navigate on device). Device example is @tag :on_device (mob's auto-excluded tag).
18 tests pass + 1 excluded device example; the mob.new scaffold still passes
against the updated helper. credo clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rmalize navigated_to
- tree/1 :device clause called Mob.Test.view_tree/1 (native accessibility
tree, %{type,label,value,frame}) when the query helpers expect the logical
render tree (%{type,props,children}). Route to Mob.Test.tree/1 instead.
- Add a node-less unit test pinning the :device dispatch: against a down node
Mob.Test.tree/1 raises BadMapError (it does rpc(...).tree) while view_tree/1
returns the tuple, so asserting the raise proves the routing without a device.
- navigated_to/1 returned the raw nav tuple ({:push, Dest, params}) on :beam
but a bare module on :device, breaking the same-assertion promise. Normalize
destination-bearing actions ({:push,_,_}/{:reset,_,_}/{:pop_to,_}) to the
destination module; leave destinationless actions unchanged. Update docs/tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GenericJam
added a commit
that referenced
this pull request
Jun 19, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Mob.ScreenCase— the blessed way to unit-test aMob.Screenin the BEAM, no device or emulator required. The screen-level analog ofPhoenix.LiveViewTest, but better positioned becauserender/1returns a typed view tree (%{type:, props:, children:}), so assertions are tree queries against real data instead of brittle HTML-string matching.The three tiers
mount_screen/3,render_event/3,render_info/2,assigns/1, plus tree queries (tree,find,find_all,text,flatten) whose vocabulary mirrorsMob.Test(the device driver).assert_renderable/2verifies every node type is renderable, derived at compile time frompriv/tags/{ios,android}.txt(the same authoritative source the~MOBsigil validates against) plus:native_view, with an:extraopt for plugin/custom types. Catches "emitted a node the native layer can't draw" atmix testtime.Viewcarries a:source(:beam | :device).device_view/1wraps a running node;tree/1,assigns/1,navigated_to/1dispatch toMob.Testover Erlang-distribution RPC. The query + assertion layer is tree-based, so the same assertions run in-BEAM or against hardware; only driving differs. Device examples are@tag :on_device(already auto-excluded).Plus
navigated_to/1: in-BEAM it reads the nav actionMob.Socket.push_screen/3records; on device it returns the live screen.It also starts
Mob.Stateper test against a throwaway data dir (likeConnCasestarts the Ecto sandbox), since screens commonly read it inmount/3.Tests
18 pass, 1 excluded (the device example).
credo --strictclean. Companionmob_newPR scaffolds one of these frommix mob.new.Caveat
The device backend reuses the existing, working
Mob.TestRPC surface and compiles, but the on-hardware run hasn't been exercised in CI (no connected node). The in-BEAM tiers are fully verified.🤖 Generated with Claude Code