Helpers for testing LiveView pages where you need to observe state during a server round-trip — not just after it completes.
The default Wallabidi interaction primitives (click/2, fill_in/3,
etc.) auto-await the LiveView patch resulting from each action.
That's the right behavior for ordinary tests: you don't have to
think about timing, and your assertions see the post-reconciliation
DOM.
But some LiveView features — optimistic UI, client-side hooks that patch the DOM before the server reply lands, multi-step animations — have observable intermediate states. To test those, you need to:
- Slow down the server reply so the optimistic phase is reliably
observable (this module's
set_latency/2/with_latency/3). - Skip the auto-await on the triggering interaction so the test
can assert on the optimistic-phase DOM (
await: :deferopt on interaction primitives). - Explicitly await the patch later, before the post-reconcile
assertions (
Wallabidi.LiveView.await_patch/2).
Example
session
|> visit("/counter")
|> Wallabidi.LiveView.with_latency(500, fn s ->
s
|> click(Query.button("Increment"), await: :defer)
|> assert_has(Query.css("#count", text: "1")) # optimistic
|> Wallabidi.LiveView.await_patch()
|> assert_has(Query.css("#count", text: "1")) # reconciled
end)Driver compatibility
All three remote drivers (Chrome BiDi, Chrome CDP, Lightpanda CDP) support latency simulation and deferred awaits.
The in-process LiveView driver renders synchronously — there's no
network round-trip to delay, and no patch lifecycle to defer. On
that driver, latency helpers are no-ops and await: :defer is
treated as await: :auto (the interaction completes synchronously
and the patch has already landed by the time the call returns).
Tests that depend on observing the optimistic phase should run on
a remote driver.
Summary
Functions
Arms a patch promise via prepare_patch and stashes the session
as :armed. Used by deferred fill_in/clear/send_keys/set_value.
Awaits the patch deferred by the most recent await: :defer
interaction.
Disables the LiveView latency simulator. No-op on the in-process driver, or if the simulator wasn't enabled.
Snapshots the current bootstrap pageId and stashes it on the
session as a deferred page-ready wait.
Enables LiveView's built-in latency simulator. Every push and every
receive callback is wrapped in setTimeout(cb, latency), stretching
the round-trip to latency_ms.
Runs fun with the latency simulator enabled at latency_ms, then
clears it. The block receives the session, and its return value is
used as the session that gets latency-cleared on exit.
Functions
@spec arm_next_patch(Wallabidi.Session.t()) :: Wallabidi.Session.t()
Arms a patch promise via prepare_patch and stashes the session
as :armed. Used by deferred fill_in/clear/send_keys/set_value.
Public so tests that need to observe a phx-change between fire and reconcile can wire the same pattern manually.
@spec await_patch( Wallabidi.Session.t(), keyword() ) :: Wallabidi.Session.t()
Awaits the patch deferred by the most recent await: :defer
interaction.
If session.pending_await holds a {:page_ready_after, id} stash
(a deferred click), waits for the next page_ready push from the
bootstrap with that pre-click id. If it holds :armed (a deferred
fill_in/clear/set_value/send_keys), waits for the patch promise
installed by prepare_patch. If neither — no prior :defer —
falls back to arm-and-await for the next patch, matching
Browser.await_patch/2.
Options
:timeout— max wait in ms (default:5_000).
@spec clear_latency(Wallabidi.Session.t()) :: Wallabidi.Session.t()
Disables the LiveView latency simulator. No-op on the in-process driver, or if the simulator wasn't enabled.
@spec defer_next_patch(Wallabidi.Session.t()) :: Wallabidi.Session.t()
Snapshots the current bootstrap pageId and stashes it on the
session as a deferred page-ready wait.
Used internally by click(query, await: :defer). Exposed publicly
so tests can defer awaits around non-click triggers (e.g. a custom
JS-driven action) and then drain them with await_patch/2.
No-op on the in-process driver and on non-LiveView pages.
@spec set_latency(Wallabidi.Session.t(), non_neg_integer()) :: Wallabidi.Session.t()
Enables LiveView's built-in latency simulator. Every push and every
receive callback is wrapped in setTimeout(cb, latency), stretching
the round-trip to latency_ms.
Useful for making the optimistic-UI phase reliably observable. A
latency of 300–500 ms is usually plenty: long enough that an
assert_has between the click and the await_patch/2 is virtually
certain to land during the in-flight phase.
No-op on the in-process LiveView driver.
Pair with clear_latency/1 or use with_latency/3 to scope the
simulation to a block.
@spec with_latency(Wallabidi.Session.t(), non_neg_integer(), (Wallabidi.Session.t() -> Wallabidi.Session.t())) :: Wallabidi.Session.t()
Runs fun with the latency simulator enabled at latency_ms, then
clears it. The block receives the session, and its return value is
used as the session that gets latency-cleared on exit.
Raises propagate after the simulator is cleared, so a failing assertion inside the block doesn't leave latency enabled for the rest of the test process.