Mutare.Phoenix.LiveView.SendUpdate (mutare_phoenix_live_view v0.1.0)

Copy Markdown View Source

:lv_send_update — mutates a Phoenix.LiveView component-update call, the message that tells a child Phoenix.LiveComponent to re-render with new assigns. Two kinds, each a variant label:

remove drops the update ("does the update matter?"):

send_update(Cart, id: "cart", count: 3)            ->  :ok
send_update(pid, Cart, id: "cart", count: 3)       ->  :ok
send_update_after(Cart, [id: "cart"], 1_000)       ->  :erlang.make_ref()
send_update_after(pid, Cart, [id: "cart"], 1_000)  ->  :erlang.make_ref()

immediate keeps the update but drops the delay ("does the timing matter?"):

send_update_after(Cart, [id: "cart"], 1_000)       ->  send_update(Cart, id: "cart")
                                                       :erlang.make_ref()

A remove survivor means no test asserts the child component was told to update — that it re-renders with the new assigns (Phoenix.LiveViewTest.render_component/2, or asserting the live render reflects the change). It is the LiveComponent sibling of :lv_event's dropped client push: same "forgot to fire a side effect" removal, aimed at the server→component channel rather than the server→client one. An immediate survivor means no test depends on the update being deferred — the delay could be deleted (or garbled) without a failure. Suppress one kind with # mutare:ignore[lv_send_update:remove] / [lv_send_update:immediate], or the whole family with # mutare:ignore[lv_send_update].

Unlike push_event (socket-in, socket-out), send_update/send_update_after are fire-and-forget side effects that never return the socket — so both kinds collapse the whole call to a happy-path return (not to an argument), and the two calls differ. send_update's return is undocumented (under the hood, the raw send/2 echo), so the conventional :ok stands in; send_update_after documents its return — the scheduled timer's reference() — so its no-ops end in a fresh :erlang.make_ref(). The immediate mutant is a two-expression block for the same reason: send_update/2,3 alone would change the return type, while (send_update(...); :erlang.make_ref()) fires the update now and keeps the documented ref type. Matching that type keeps both mutants clean no-ops even at the rare site that captures the ref to Process.cancel_timer/1 it later — cancelling an unknown ref returns false, where a :ok there would raise (an uninformative crash-kill this package exists to avoid).

The delay argument itself is marked with the shared :timeout label (Mutare.Mutator.argument_marks/1), so core's IntegerLiteral/AtomLiteral leave the duration literal alone — the immediate mutant owns the timing question, and a 1_000 → 1_001 off-by-one there is the near-unkillable noise those families' own timeout table exists to avoid.

Because no socket flows through, these calls are never idiomatically piped (a direct pipe would feed the module/pid slot), so a piped occurrence is left alone rather than given a non-faithful Function.identity() pass-through. Only the real arities fire — send_update/2,3 and send_update_after/3,4 — so a name-matched call of any other arity (reachable only by an explicit qualifier) is left alone, keeping every metamutant compiling.

Matches direct (Phoenix.LiveView.send_update(...)), aliased, and bare-imported (use-injected) calls. Like :lv_event, this family has a high equivalent-mutant rate in suites that never assert a component re-rendered — drop it from your preset if that is noisy.