Wallabidi.LiveView (wallabidi v0.4.0-rc.5)

Copy Markdown View Source

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:

  1. Slow down the server reply so the optimistic phase is reliably observable (this module's set_latency/2 / with_latency/3).
  2. Skip the auto-await on the triggering interaction so the test can assert on the optimistic-phase DOM (await: :defer opt on interaction primitives).
  3. 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

arm_next_patch(session)

@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.

await_patch(session, opts \\ [])

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

clear_latency(session)

@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.

defer_next_patch(session)

@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.

set_latency(session, latency_ms)

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.

with_latency(session, latency_ms, fun)

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.