Last active
August 3, 2024 22:58
-
-
Save mcrumm/8e6b0a98196dd74a841d850c70805f50 to your computer and use it in GitHub Desktop.
Testing Phoenix.LiveComponent in Isolation
This file contains 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
# lib/party_web/components/example_component.ex | |
defmodule PartyWeb.ExampleComponent do | |
@moduledoc """ | |
An example LiveComponent under test. | |
""" | |
use Phoenix.LiveComponent | |
def render(assigns) do | |
~H""" | |
<div> | |
<p>Component extra: <%= inspect(@extra) %></p> | |
<button id="clickme" phx-click="click" phx-target={@myself}> | |
click me | |
</button> | |
</div> | |
""" | |
end | |
def handle_event("click", _, socket) do | |
socket = update(socket, :clicks, &(&1 + 1)) | |
send(self(), {:after_click, socket.assigns.clicks}) | |
{:noreply, socket} | |
end | |
def mount(socket) do | |
{:ok, assign(socket, clicks: 0, extra: %{})} | |
end | |
end |
This file contains 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
# test/party_web/components/example_component_test.exs | |
defmodule PartyWeb.ExampleComponentTest do | |
use PartyWeb.ConnCase, async: true | |
import Phoenix.LiveViewTest | |
import LiveComponentTests | |
test "live component accepts attributes", %{conn: conn} do | |
{:ok, lcd, html} = | |
live_component_isolated(conn, PartyWeb.ExampleComponent, %{extra: %{foo: :bar}}) | |
assert html =~ "Component extra: %{foo: :bar}" | |
assert render(lcd) =~ "Component extra: %{foo: :bar}" | |
end | |
test "component handle_event sends to parent", %{conn: conn} do | |
{:ok, lcd, _html} = live_component_isolated(conn, PartyWeb.ExampleComponent) | |
test_pid = self() | |
# Intercepts `handle_info` messages from the LiveComponent. | |
live_component_intercept(lcd, fn | |
{:after_click, new_clicks}, socket -> | |
send(test_pid, {:clicks, new_clicks}) | |
{:halt, socket} | |
_, socket -> | |
# catch-all for other :handle_info messages | |
{:cont, socket} | |
end) | |
btn = element(lcd, "button#clickme") | |
render_click(btn) | |
assert_received {:clicks, 1} | |
render_click(btn) | |
assert_received {:clicks, 2} | |
render_click(btn) | |
assert_received {:clicks, 3} | |
end | |
end |
This file contains 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
# test/support/live_component_tests.ex | |
defmodule LiveComponentTests do | |
@moduledoc """ | |
Conveniences for testing a LiveComponent in isolation. | |
""" | |
defmodule Driver do | |
@moduledoc """ | |
A LiveView for driving a LiveComponent under test. | |
""" | |
use Phoenix.LiveView | |
def render(assigns) do | |
~H""" | |
<.live_component :if={@lc_module} module={@lc_module} {@lc_attrs} /> | |
""" | |
end | |
def handle_call({:run, func}, _, socket) when is_function(func, 1) do | |
func.(socket) | |
end | |
def mount(_, _, socket) do | |
{:ok, assign(socket, lc_module: nil, lc_attrs: %{})} | |
end | |
## Test Helpers | |
def run(lv, func) do | |
GenServer.call(lv.pid, {:run, func}) | |
end | |
end | |
## Test helpers | |
require Phoenix.LiveViewTest | |
@doc """ | |
Spawns a Driver process to mount a LiveComponent in isolation as the sole rendered element. | |
## Examples | |
Starting a LiveComponent under test: | |
{:ok, lcd, html} = LiveComponentTest.live_component_isolated(conn, MyComponent) | |
Starting a LiveComponent under test with attributes: | |
{:ok, lcd, html} = LiveComponentTest.live_component_isolated(conn, MyComponent, foo: :bar) | |
""" | |
defmacro live_component_isolated(conn, module, attrs \\ []) do | |
quote bind_quoted: binding() do | |
# Starts the Driver LiveView. It will render empty until we give it a `@module`. | |
{:ok, lcd, _html} = Phoenix.LiveViewTest.live_isolated(conn, Driver) | |
# <.live_component> requires an :id, so we set one if it's not already included. | |
attrs = attrs |> Map.new() |> Map.put_new(:id, module) | |
# Runs the given function _in the LiveView process_. | |
Driver.run(lcd, fn socket -> | |
{:reply, :ok, Phoenix.Component.assign(socket, lc_module: module, lc_attrs: attrs)} | |
end) | |
{:ok, lcd, Phoenix.LiveViewTest.render(lcd)} | |
end | |
end | |
@doc """ | |
Intercepts messages on the LiveComponentTest LiveView. | |
Use this function to intercept messages sent by the LiveComponent to the LiveView. | |
## Examples | |
{:ok, lcd, _html} = LiveComponentTest.live_component_isolated(conn, MyLiveComponent) | |
test_pid = self() | |
live_component_test_intercept(lv, fn | |
:message_to_intercept, socket -> | |
send(test_pid, :intercepted) | |
{:halt, socket} | |
_other, socket -> | |
{:cont, socket} | |
end) | |
assert_received :intercepted | |
""" | |
def live_component_intercept(lv, func) when is_function(func) do | |
Driver.run(lv, fn socket -> | |
name = :"lcd_intercept_#{System.unique_integer([:positive, :monotonic])}" | |
ref = {:intercept, lv, name, :handle_info} | |
{:reply, ref, Phoenix.LiveView.attach_hook(socket, name, :handle_info, func)} | |
end) | |
end | |
@doc """ | |
Removes an intercept from the LiveComponentTest LiveView. | |
## Examples | |
ref = LiveComponentTest.intercept(lv, fn msg, socket -> {:halt, socket} end) | |
:ok = LiveComponentTest.remove_intercept(ref) | |
""" | |
def live_component_remove_intercept({:intercept, lv, name, stage}) do | |
Driver.run(lv, fn socket -> | |
{:reply, :ok, Phoenix.LiveView.detach_hook(socket, name, stage)} | |
end) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment