Testing LiveView forms

Image by Annie Ruygt

This is a post about a few functions to test that our app does what we think it does when a form is submitted. If you want to deploy your Phoenix LiveView app right now, then check out how to get started. You could be up and running in minutes.

In previous posts we’ve used forms for different purposes, but we’ve never talked about how to test that our app does the right thing when a form is submitted. In this post we’ll take a walk around some functions of the LiveViewTest module that come in handy for testing forms.

Testing the phx-submit result

We can test the behavior of our LiveView when the event specified with the phx-submit option is handled. The render_submit/2 function sends a form submit event and returns the rendered result. It is useful if we want to test the contents of our LiveView right after handling the submit event.

For example, this form checks what happens if we submit a password-update form with a too-short password and a non-matching password confirmation:

Verified routes can be used in tests too, Phoenix 1.7 will bring really cool stuff!

  test "Update password: renders errors with invalid data", %{conn: conn} do
    {:ok, lv, _html} = live(conn, ~p"/users/settings")

    result =
      lv
      |> form("#password_update_form", %{
        "current_password" => "invalid",
        "user" => %{
          "password" => "too short",
          "password_confirmation" => "does not match"
        }
      })
      |> render_submit()

    assert result =~ "<h1>Settings</h1>"
    assert result =~ "should be at least 12 character(s)"
    assert result =~ "does not match password"
    assert result =~ "is not valid"
  end 

We use the render_submit function to try to update the user’s password. After handling the submit event, we can verify that the LiveView’s content contains the expected error messages by asserting the resulting content.

Testing a form submission with a redirect

The render_submit function has two return types. We already saw the first one —the rendered content of the LiveView— but we haven’t mentioned the second one; it returns a tuple of type {:error, reason} when the LiveView redirects after handling the phx-submit event.

We can use the follow_redirect/3 function to verify that the LiveView redirects as expected. When there is a redirect it returns a {:ok, conn} or {:ok, live_view, html} tuple depending on the redirect type we use: a regular redirect or a live redirect respectively. If there is no redirect or if it redirects to the wrong place, it raises an error.

We can also use the resulting conn to verify the new rendered page content. For example, we have a LiveView to reset the user’s password after receiving a reset link. If the password is successfully reset, an :info message is added and the user is redirected to the login page as follows:

{:noreply,
   socket
   |> put_flash(:info, "Password reset successfully.")
   |> redirect(to: ~p"/users/log_in")}

We can verify that the LiveView redirects to the correct place and it’s setting the expected :info message:

test "resets password once", %{conn: conn, token: token, user: user} do
  {:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")

  {:ok, conn} =
    lv
    |> form("#reset_password_form",
      user: %{
        "password" => "new valid password",
        "password_confirmation" => "new valid password"
      }
    )
    |> render_submit() # {:error, {:redirect, %{to: "/users/log_in", flash: "SFMyNTY..."}}}
    |> follow_redirect(conn, ~p"/users/log_in")

  refute get_session(conn, :user_token)
  assert get_flash(conn, :info) =~ "Password reset successfully"
  assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
end

The render_submit function is called and it returns an {:error, redirect} tuple. We pass the redirect tuple and the conn to follow_redirect/3 to perform the underlying request, and the route ~p"/users/log_in" to verify that it redirects to the correct place.

Finally we use the resulting conn to verify that:

1) The user wasn’t logged in.

2) The expected flash message was added.

3) The user’s password was reset.

Testing an HTTP form submission

In a previous post, we used a form’s :action attribute to execute a controller action from a LiveView.

For example, we have a form to send a username and password to the ~p/users/log_in route and save the user’s data in session if the authentication is successful:

 <.form
  id="login_form"
  :let={f}
  for={:user}
  action={~p"/users/log_in"}
  as={:user}
  >
  <%= label f, :email %>
  <%= email_input f, :email, required: true, value: @email %>

  <%= label f, :password %>
  <%= password_input f, :password, required: true %>

  <div>
    <%= submit "Log in" %>
  </div>
</.form>

In this case, we don’t have the phx-submit option since we don’t do any validation/process inside the LiveView, but we delegate the whole login process to the ~p"/users/log_in" controller action.

In LiveView 0.18, the function submit_form/2 was added to test the results of sending an HTTP request through the plug pipeline. Let’s use it to test a form submission with valid login params:

test "Login user with valid credentials", %{conn: conn} do
  password = "valid_password"
  user = user_fixture(%{password: password})

  {:ok, lv, _html} = live(conn, ~p"/users/log_in")

  form =
    form(lv, "#login_form", user: %{
      email: user.email, 
      password: password, 
      remember_me: true})

  conn = submit_form(form, conn)

  assert redirected_to(conn) == ~p"/"
  assert get_session(conn, :user_token)
  assert get_flash(conn, :info) =~ "Welcome back!"
end

The submit_form/2 function receives a form and executes the HTTP request specified in the form’s :action attribute. Then we use the resulting conn to check that:

1) This results in the right redirect.

2) The logged user’s token was added to the session.

3) The expected flash message was returned.

Triggering the submit form action and testing the results

In the same post, we used the phx-trigger-action option to trigger a form :action only once a condition had been met. Now we’ll cover how to test that the phx-trigger-action attribute has been set to true, and we’ll follow the underlying HTTP request to test the action call results.

In this test we submit the same password-update form we used at the beginning of the post, but this time we send valid params. We expect the form :action — in charge of updating the user’s token in session — is triggered after validating that the user’s password was successfully updated and setting the phx-trigger-action to the form:

test "updates the user password", %{conn: conn, user: user} do
  new_password = valid_user_password()

  {:ok, lv, _html} = live(conn, ~p"/users/settings")

  form =
    form(lv, "#password_update_form", %{
      "current_password" => user.password,
      "user" => %{
        "email" => user.email,
        "password" => new_password,
        "password_confirmation" => new_password
      }
    })

  assert render_submit(form) =~ ~r/phx-trigger-action/

  new_password_conn = follow_trigger_action(form, conn)

  assert get_session(new_password_conn, :user_token) != 
           get_session(conn, :user_token)
  assert get_flash(new_password_conn, :info) =~ 
           "Password updated successfully"
  assert Accounts.get_user_by_email_and_password(user.email, new_password)
end

We build a form and pass it to the render_submit/1 function, which returns the new content of our LiveView after handling the phx-submit event so that we can assert that the phx-trigger-action option is added to the form.

Then, we execute the controller action specified in the :action attribute using the follow_trigger_action/2 function, and we use new_password_conn to check that:

1) The user’s token was renewed with the new password.

2) The expected flash message was added to the connection.

3) The user’s password was updated.

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!

Wrap-up

In this post we used a few functions of the LiveViewTest module to test form submission results when we are using LiveView.

We can use render_submit to test that our LiveView executes the correct logic and renders the expected results after handling the submit event that we specify with the phx_submit attribute. Additionally, we can use follow_redirect together with it for those times when we redirect to another LiveView after handling the submit event.

We can also test the results of executing an HTTP form submission using the :action attribute. We use submit_form to test the results of sending an HTTP request through the plug pipeline, and follow_trigger_action to test the resulting content of the action that is executed once the phx-trigger-action attribute is set to true.