Now that we’ve seen how to test function components, let’s take a look at how to test LiveComponents.
Unlike function components, LiveComponents can have their own state and events.
So, let’s take a look at how we can test them.
SettingsLive
If you go to our router, you’ll notice we have a SettingsLive mounted on /users/:user_id/settings.
If you launch your browser and visit /users/1/settings, you’ll see that the settings page has two inputs: one for a name and one for an email.
Each input is editable inline. We can click on it and edit it. Hitting enter will save the new value. Clicking away will stop editing.
Let’s see how we implemented that. Open up SettingsLive.
defmodule RangerWeb.SettingsLive do
use RangerWeb, :live_view
alias Ranger.{Repo, User}
alias RangerWeb.SettingsLive.EditableInputComponent
def render(assigns) do
~H"""
<div class="space-y-4">
<.live_component module={EditableInputComponent} id="name" field={:name} user={@user} />
<.live_component module={EditableInputComponent} id="email" field={:email} user={@user} />
</div>
"""
end
def mount(params, _, socket) do
user = get_user(params["user_id"])
{:ok, assign(socket, user: user)}
end
def handle_info({:updated_user, user}, socket) do
socket
|> assign(:user, user)
|> noreply()
end
defp noreply(socket), do: {:noreply, socket}
defp get_user(user_id) do
Repo.get(User, user_id)
end
end
As you can see, we do the following:
- Our LiveView renders two
EditableInputComponentLiveComponents. - The components pass a
fieldand theuserstruct. mount/3takes"user_id", fetches the corresponding user, and sets it as an assign.- We also have
handle_info/2to communicate with the LiveComponents. - The other private functions are helpers to keep code clean.
EditableInputComponent
Let’s dive into the EditableInputComponent found in lib/ranger_web/live/settings_live/editable_input_component.ex.
defmodule RangerWeb.SettingsLive.EditableInputComponent do
use RangerWeb, :live_component
alias Ranger.{Repo, User}
def render(assigns) do
~H"""
<div class="max-w-md mx-auto">
<%= if @edit do %>
<.simple_form
:let={f}
id={"#{@field}-form"}
for={@form}
phx-click-away="cancel"
phx-submit="save"
phx-target={@myself}
>
<.input field={{f, @field}} value={Map.get(@user, @field)} />
</.simple_form>
<% else %>
<div class="space-y-8 bg-white mt-10">
<div
id={@field}
phx-click="edit"
phx-target={@myself}
class="text-sm bg-gray-100 border-1 p-2 rounded-lg mt-2 flex items-center justify-between gap-6"
>
<%= Map.get(@user, @field) %>
<Heroicons.pencil solid class="text-blue-500 w-4 h-4 stroke-current inline" />
</div>
</div>
<% end %>
</div>
"""
end
def mount(socket) do
{:ok, assign(socket, form: to_form(%{}), edit: false)}
end
def handle_event("edit", _, socket) do
socket
|> assign(:edit, true)
|> noreply()
end
def handle_event("cancel", _, socket) do
socket
|> assign(:edit, false)
|> noreply()
end
def handle_event("save", params, socket) do
user = socket.assigns.user
updated_user = update_user(user, params)
send(self(), {:updated_user, updated_user})
socket
|> assign(:edit, false)
|> noreply()
end
defp noreply(socket), do: {:noreply, socket}
defp update_user(user, params) do
user
|> User.changeset(params)
|> Repo.update!()
end
end
This is where most of the behavior lives:
- We have an
if/elsethat renders either a form or a display div based on@edit. - In the
elsecase, we render the current field value and wirephx-click="edit"targeted to@myself. - Clicking it triggers
handle_event("edit", ...), which flips:edittotrue. - In edit mode, we render a form with one input and handle
phx-submit="save"targeted to@myself. - On save, we update the user, send
{:updated_user, updated_user}to the parent LiveView, and set edit mode back to false.
Testing LiveComponents
There are two things we can test about LiveComponents:
- static content
- interactivity
Each needs a different approach.
Testing static content
Open test/ranger_web/live/settings_live/editable_input_component_test.exs.
defmodule RangerWeb.SettingsLive.EditableInputComponentTest do
use RangerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias RangerWeb.SettingsLive.EditableInputComponent
end
Let’s write our first test:
test "renders field text" do
user = %{name: "Frodo"}
html = render_component(EditableInputComponent, id: "name", field: :name, user: user)
assert html =~ "Frodo"
end
Downsides of render_component/3 here:
- It only tests static content, not interactivity.
- It has the usual HTML-blob assertion trade-offs (too broad vs too brittle).
Testing interactivity
To test interactivity, we need to go through SettingsLive, so this becomes a normal LiveView test.
That means we’re testing both SettingsLive and EditableInputComponent together.
Let’s write tests in settings_live_test.exs and focus on behavior (not implementation):
defmodule RangerWeb.SettingsLiveDoneTest do
use RangerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Ranger.{Repo, User}
test "renders user's information", %{conn: conn} do
user = create_user()
{:ok, _view, html} = live(conn, ~p"/users/#{user}/settings")
assert html =~ user.name
assert html =~ user.email
end
test "users can edit their name", %{conn: conn} do
user = create_user(%{name: "frodo"})
{:ok, view, _html} = live(conn, ~p"/users/#{user}/settings")
view
|> element("#name")
|> render_click()
view
|> form("#name-form", %{name: "bilbo"})
|> render_submit()
assert has_element?(view, "#name", "bilbo")
end
test "users can edit their email", %{conn: conn} do
user = create_user(%{email: "frodo@hobitton.com"})
{:ok, view, _html} = live(conn, ~p"/users/#{user}/settings")
view
|> element("#email")
|> render_click()
view
|> form("#email-form", %{email: "frodo@fellowship.com"})
|> render_submit()
assert has_element?(view, "#email", "frodo@fellowship.com")
end
defp create_user(params \\ %{}) do
%{email: "random@example.com", name: "randomname"}
|> Map.merge(params)
|> User.changeset()
|> Repo.insert!()
end
end
Notice we test each field even though both use the same component.
That can feel like duplication, but it’s the trade-off when interactivity must be exercised through the parent LiveView.
Since users only care about behavior on the page, that’s what we optimize these tests for.