In this blog post, you'll see how useful open_browser/2
is when debugging LiveView tests. In addition, we'll give a brief introduction to testing LiveView.
Let's get started!
Introducing LiveView Testing
Testing during the software development process is one way to build confidence and ensure your application will work as expected.
The ability to easily write meaningful tests is an important factor for any framework, regardless of programming language.
Developers can easily test the LiveView framework's component, life-cycle, and behavior by writing LiveView tests with pure Elixir, since it uses ExUnit (a built-in testing framework) for all its testing. We can be confident in writing LiveView tests that are fast, concurrent, and stable.
You can test the functionality of your live views' behavior through the help of the Phoenix.LiveViewTest
module, which offers convenient functions without the need to introduce JS testing frameworks. The test helpers assist us in writing meaningful tests for our LiveView modules with ease and speed.
Sample Feature in LiveView
Let's write a sample feature to use as our testing example.
We shall add a form to our application that allows users to enter their email address and password when attempting to register. This feature will only focus on the rendered HTML and not include the ability to add users into the system (the backend part of taking user params and adding them into the data store).
Let's begin!
First, start by creating a new LiveView application:
mix phx.new sample_live --live
NB: You can use an existing LiveView application if you have one.
The mix phx.new
command with the --live
flag will create a new application with LiveView installed and configured.
Add a live path in the router.
# lib/sample_live_web/router.ex
scope "/", SampleLiveWeb do
live "/user_registration", RegistrationLive
end
Create registration_live.ex
inside the sample_live_web
folder.
# lib/sample_live_web/registration_live.ex
defmodule SampleLiveWeb.RegistrationLive do
use SampleLiveWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<.form let={f} for={:changeset} id={"registration-form"} >
<%= label f, :email %>
<%= text_input f, :email, id: "email-input" %>
<%= error_tag f, :email %>
<%= label f, :password, id: "password-input" %>
<%= password_input f, :password %>
<%= error_tag f, :password %>
<%= submit "Save" %>
</.form>
"""
end
end
In our LiveView application module, two callbacks have been defined: mount/3
and render/1
.
The mount/3
callback expects three arguments — params
, session
, and liveview socket
— and returns {:ok, socket}
. When the LiveView page is rendered, the mount/3
callback will be invoked twice: once to perform the initial page load and again to establish the live connection.
The render/1
callback is responsible for displaying the HTML
template/content — in this case, the added registration form. It receives the socket assigns
and must return a template passed inside the ~H sigil
. Whenever there is a change in our LiveView, the render/1
callback will be invoked.
In our template, we have defined a sample form where a user can enter their email and password.
Let's start the server and open the registration form in our browsers. You should see something similar to this:
Display the Feature on the Browser while Testing
When working on an HTTP-based web application, we usually write integration tests to validate passing expected attributes and properties to parts of our application.
We'll write an integration test validating user interactions with our application. The test should verify that when a user visits the /user_registration
page, they can see the registration form properties/attributes rendered. In addition, we'll explore how we can use open_browser/2 to debug our LiveView test.
First, let's write a test for our form implemented above that will verify the HTML rendered. In here, we shall use open_browser/2
to verify the displayed form. The implemented registration form has an HTML id
("registration-form"
) that should help us select the element in our test.
# test/sample_live_web/registration_live_test.exs
defmodule SampleLiveWeb.RegistrationLiveTest do
use SamplLiveWeb.ConnCase
import Phoenix.LiveViewTest
test "user can see registration form", %{conn: conn} do
{:ok, view, html} = live(conn, "/user_registration")
html =
view
|> element("#registration-form")
|> open_browser()
|> render()
assert html =~ "Email</label>"
assert html =~ "Password</label>"
end
end
import Phoenix.LiveViewTest
module gets access to the test helper functions.
In the sample feature, when a user is on the application and wants to visit the registration page, they will navigate to /user_registration
. A similar thing happens here — we shall navigate the user to /user_registration
using live/2
, which performs a regular get(conn, path)
and then upgrades the page to LiveView. It takes in the conn
and the path
then returns a three-element tuple with :ok
, liveview process
, and the rendered HTML
.
open_browser/2
expects a view
or element
as the first argument. In the test, I have opted to pass an element, and this can be done by invoking element/3
, passing the view
and the query selector
to it. element/3
returns an element
which is then passed to open_browser/2
. If open_browser/2
finds the form element with the query selector
(#registration-form) within the LiveView page, we expect the following:
- The default browser to be opened displaying the HTML of the form element.
- The form element to be returned.
The
form element
returned can be passed torender/1
, which takes in aview_or_element
and returns an HTML string of the view that we can finally assert in the test.
Run the test:
mix test test/sampl_live_web/registration_live_test.exs
We should expect the default browser to open and our test to pass.
Now, let's make the user enter their email and password by using form/3
, which takes in the view
, query selector
, and form data
, and then returns a form element. But this time we'll use a query selector that doesn't exist to demonstrate how open_browser/2
can be useful in debugging LiveView tests.
# test/sample_live_web/registration_live_test.exs
defmodule SampleLiveWeb.RegistrationLiveTest do
use SamplLiveWeb.ConnCase
import Phoenix.LiveViewTest
test "user can see registration form", %{conn: conn} do
{:ok, view, html} = live(conn, "/user_registration")
view
|> element("#registration-form")
|> open_browser()
view
|> form("#wrong_registration-form", user: %{email: "hello@email.com", password: "hello123"})
|> render()
end
end
Let's run the test:
** (ArgumentError) expected selector "#wrong_registration-form" to return a single element, but got none within:
<main class="container"><p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info"></p><p class="alert alert-danger" role="alert" phx-click="lv:clear-flash" phx-value-key="error"></p><form action="#" method="post" id="registration-form" phx-submit="save"><input name="_csrf_token" type="hidden" value="CHwQRnBHAnF7JDQEXQk3AXx8QQ0hZxceYLY-91nA2PLS3bB13Q0HC8CQ"/><label for="registration-form_email">Email</label><input id="email-input" name="user[email]" type="text"/><label for="registration-form_password">Password</label><input id="password-input" name="user[password]" type="password"/><button type="submit">Save</button></form></main>
We notice that our test is failing with an ArgumentError
. In this test, the form/3
function returns a form element
with the query selector #wrong_registration-form
. This form element
is then passed to render/1
, which expects the form element
with the query selector #wrong_registration-form
passed to it as the first argument to return a form element (but it gets none within the LiveView page). The test has made it clear that we are using the wrong query selector.
Our form template is very short, so it's easy to identify our mistake. Even from the HTML string displayed on the terminal after running the test, it's easy to point out the query selector we should have used.
Imagine working with larger HTML templates, though. It will be cumbersome to go through the HTML string to try and identify the mistake. Let's examine how open_browser/2
can come in handy in that case.
First, let's add open_browser/2
after form/3
:
# test/sample_live_web/registration_live_test.exs
defmodule SampleLiveWeb.RegistrationLiveTest do
use SamplLiveWeb.ConnCase
import Phoenix.LiveViewTest
test "user can see registration form", %{conn: conn} do
{:ok, view, html} = live(conn, "/user_registration")
view
|> element("#registration-form")
|> open_browser()
view
|> form("#wrong_registration-form", user: %{email: "hello@email.com", password: "hello123"})
|> open_browser()
|> render()
end
end
This way, we expect the browser to open with the displayed registration form — but apparently, that won't be the case when we run our test. Just like the render/1
function, open_browser/2
expects the form element
with the query selector #wrong_registration-form
passed to it, but it doesn't exist.
The other option is to debug the LiveView page where the registration form is rendered. In the test, we shall call open_browser/2
and pass the view
returned after invoking live/2
to it. Earlier on, I mentioned that open_browser/2
could either take in a view
or an element
. The option to use an element
is failing due to the wrong query selector
. So using view
can be a better option in our case to help us understand why the test is failing.
# test/sample_live_web/registration_live_test.exs
defmodule SampleLiveWeb.RegistrationLiveTest do
use SamplLiveWeb.ConnCase
import Phoenix.LiveViewTest
test "user can see registration form", %{conn: conn} do
{:ok, view, html} = live(conn, "/user_registration")
open_browser(view)
end
end
The debug process will be as follows:
- Run the test. This time, we should expect the default browser to be opened and our registration form displayed.
- Use the element inspector in the browser to find the correct form selector.
From the image above, we can see that the form element under the inspect elements section highlighted in blue has a different query selector
(the id
attribute value) specified to the one passed in form/3
in our test. Our test expected the registration_form
query selector. If we now refactor our test and pass the required query selector, it will definitely pass.
We have seen that we can use open_browser/2
as a debugging tool for our tests. With open_browser/2
, we can easily verify that the correct HTML elements are rendered to users. If the correct view_or_element
passes, the browser opens with the displayed HTML.
Monitor Your Phoenix LiveView App in Production with AppSignal
If you also want to keep track of your live view performance in production, you can set up LiveView instrumentation in AppSignal. Once you've integrated AppSignal with Phoenix LiveView, you'll be able to monitor incoming HTTP requests.
Check out our docs for Phoenix LiveView for more information.
We recommend using our automatic LiveView instrumentation in the majority of cases.
Wrapping Up
The introduction of open_browser/2
has made debugging LiveView tests easy and efficient. In this post, we introduced how to use open_browser/2
when testing to verify that the correct HTML is rendered within your live view.
Happy debugging!
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!
Top comments (0)