How to properly test `push_navigate`

Hi all :slight_smile:

Apologies if this question has already been answered, or if there’s a page dedicated to it in the documentation, but I could not find one.

I’m having a problem testing a live view following a call to push_navigate/2. I am trying to navigate from a LiveView to another on the same live_session, and the transition works as expected, but I’m having a hard time unit-testing the flow.

This is the code in which I’m pushing the navigation to another LiveView:

  def handle_event("create", %{"privee_form" => params}, socket) do
    changeset = get_privee_form_changeset(params, socket)

    if changeset.valid? do
      selected_session = Ecto.Changeset.get_field(changeset, :session_name)

      {:noreply,
       socket
       |> push_navigate(to: ~p"/chat/#{selected_session}")}
    else
      {:noreply,
       socket
       |> assign_form(params)}
    end
  end

This function is invoked when the page form is submitted; as previously said, this code behaves exactly as it’s supposed to. Now, I wrote this unit test to test the flow:

    test " redirect to the chat when the right session name has been selected", %{conn: conn} do
      session = session_fixture()
      %{session_name: session_name} = session_fixture(%{
        session_name: "session_name",
        recovery_phrase: "recovery_phrase"
      })

      {:ok, privee_selector_live, _html} =
        conn
        |> log_in_session(session)
        |> live(~p"/privee")

      result =
        privee_selector_live
        |> form("#privee_form", %{privee_form: %{session_name: session_name}})
        |> render_submit()

      assert_redirect(result, ~p"/chat/#{session_name}")

      html = render(result)

      assert html =~ "#{session_name}"
    end

The test fails at the assert_redirect, as the result variable has the following value:

{:error, {:live_redirect, %{kind: :push, to: "/chat/session_name"}}}

Hence a match error is raised.

I have the following questions:

  • Why the render_submit function is returning an {:error, _} tuple?
  • How can I properly test the push_navigate transition?
1 Like

You need to follow_redirect after submitting. Details are at that link!

Check out PhoenixTest if you want a bit smoother experience there.

3 Likes

Thanks for the link!! I was actually looking for solution in the LiveView guide itself, I wasn’t aware that the testing tools were contained in another package (which, in hindsight, makes perfect sense).

I solved the problem: in the end it was both the usage of follow_redirect that was missing, and the connection was missing the session that carried the mocked logged user information, as the routes were protected behind authentication.

I solved by re-assigning the connection to an authenticated one. I will publish my solution for completeness, though I still need to polish it, but this version would best highlight the differences with the one I posted first:

    test " redirect to the chat when the right session name has been selected", %{conn: conn} do
      session = session_fixture()
      %{session_name: session_name} = session_fixture(%{
        session_name: "another-twenty-four-session-name",
        recovery_phrase: "Yet another recovery phrase of more than twenty four characters"
      })

      # Here, the connection variable is re-bound to one with authentication.
      conn = log_in_session(conn, session)

      {:ok, privee_selector_live, _html} =
        conn
        |> live(~p"/privee")

      {:ok, _chat_live_view, html} =
        privee_selector_live
        |> form("#privee_form", %{privee_form: %{session_name: session_name}})
        |> render_submit()
        # This allows rendering the HTML directly.
        |> follow_redirect(conn, ~p"/chat/#{session_name}")

      assert html =~ "#{session_name}"
    end
1 Like

Yep, that’s how you gotta do it! Since you’re navigating it starts a new LiveView process which is why you need to reassign. If you to patch then this wouldn’t be necessary,

1 Like

Hi! I’m hijacking this thread since my problem is very similar.
At the end of my multistep form, pretty much followed step by step from Phoenix LiveView: Multi-step forms · Bernheisel, I redirect.

|> push_navigate(to: ~p"/users/log_in?_action=registered")

However, when I submit the form I end up with this error message in the console

20:18:00.484 [error] GenServer #PID<0.481.0> terminating
** (stop) exited in: GenServer.call(#PID<0.483.0>, {:phoenix, :ping}, :infinity)
    ** (EXIT) shutdown: {:live_redirect, %{kind: :push, to: "/users/log_in?_action=registered", flash: "SFMyNTY.g2gDdAAAAAFtAAAABGluZm9tAAAAMUFjY291bnQgc3VjY2Vzc2Z1bGx5IGNyZWF0ZWQhIENoZWNrIHlvdXIgbWFpbGJveC5uBgCktaG0jgFiAAFRgA.GM1Er3LioeyvKedjJ9XQHr58L5f-omPzHjACCz2zrBE"}}
Last message: {:EXIT, #PID<0.462.0>, {{:shutdown, {:live_redirect, %{kind: :push, to: "/users/log_in?_action=registered", flash: "SFMyNTY.g2gDdAAAAAFtAAAABGluZm9tAAAAMUFjY291bnQgc3VjY2Vzc2Z1bGx5IGNyZWF0ZWQhIENoZWNrIHlvdXIgbWFpbGJveC5uBgCktaG0jgFiAAFRgA.GM1Er3LioeyvKedjJ9XQHr58L5f-omPzHjACCz2zrBE"}}}, {GenServer, :call, [#PID<0.483.0>, {:phoenix, :ping}, :infinity]}}}

When I do a

|> follow_redirect(conn)

I receive “LiveView did not redirect” (since the response from the previous function isn’t an error tuple).

This is the test code

      conn
      |> visit(~p"/users/register")
      |> fill_form("#brf-selection", user: %{"brf" => "some value"})
      |> click_button("Next")
      |> assert_has("#apartment-selection")
      |> fill_form("#apartment-selection", user: %{"apartment_nr" => "4072"})
      |> click_button("Next")
      |> assert_has("#email-password-selection")
      |> fill_form("#email-password-selection",
        user: %{"email" => email, "password" => password}
      )
      |> click_button("Create account")
      |> follow_redirect(conn)
      |> assert_has("#flash-info")

So the problem seems to be that the live view genserver crashes (which is expected). But the error tuple doesn’t bubble up to my pipeline.

Thankful for any pointers!

It looks like you’re using PhoenixTest which follows redirects automatically. follow_redirect is from Phoenix.LiveViewTest which is neither compatible nor necessary with PhoenixTest.

Hi!

Unfortunately without follow_redirect I end up with this error

20:18:00.484 [error] GenServer #PID<0.481.0> terminating
** (stop) exited in: GenServer.call(#PID<0.483.0>, {:phoenix, :ping}, :infinity)
    ** (EXIT) shutdown: {:live_redirect, %{kind: :push, to: "/users/log_in?_action=registered", flash: "SFMyNTY.g2gDdAAAAAFtAAAABGluZm9tAAAAMUFjY291bnQgc3VjY2Vzc2Z1bGx5IGNyZWF0ZWQhIENoZWNrIHlvdXIgbWFpbGJveC5uBgCktaG0jgFiAAFRgA.GM1Er3LioeyvKedjJ9XQHr58L5f-omPzHjACCz2zrBE"}}
Last message: {:EXIT, #PID<0.462.0>, {{:shutdown, {:live_redirect, %{kind: :push, to: "/users/log_in?_action=registered", flash: "SFMyNTY.g2gDdAAAAAFtAAAABGluZm9tAAAAMUFjY291bnQgc3VjY2Vzc2Z1bGx5IGNyZW

I tried with and without PhoenixTest, but it seems to produce the same results.

Thanks!

hmmmm, that’s strange. Can you share more, perhaps in a repo that reproduces it? I can’t reproduce it myself. This was my quick attempt:

defmodule ExampleWeb.FollowLive do
  use ExampleWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <div id="click-me" phx-click="nav">click</div>
    """
  end

  @impl true
  def handle_event("nav", _params, socket) do
    {:noreply, push_navigate(socket, to: "/")}
  end
end
defmodule ExampleWeb.FollowLiveTest do
  use ExampleWeb.ConnCase

  import Phoenix.LiveViewTest

  test "does it", %{conn: conn} do
    {:ok, lv, _} = live(conn, "/follow")

    lv
    |> element("#click-me")
    |> render_click()
    |> follow_redirect(conn, "/")
  end
end

Is this essentially what you have?

Essentially, yes! However, I think the problems lie in the use of LiveComponent. So since I have a multistep registration flow I use LiveComponents to render the different parts.
I followed this: Phoenix LiveView: Multi-step forms · Bernheisel

@impl Phoenix.LiveComponent
def handle_event("save", %{"user" => %{"email" => email, "password" => password}}, socket) do

send(self(), {:next, %{email: email, password: password}})

{:noreply, socket}
end

receiving that message in the parent module

def handle_info({:next, data_from_form}, socket) do
  #  logic for registering the user

  {:noreply, push_navigate(conn, to: ~p"/users/log_in?_action=registered")
)}
end

If I understand the problem more correctly now, I seem to be able to do the push_navigate in the LiveComponent, i.e:

@impl Phoenix.LiveComponent
def handle_event("save", %{"user" => %{"email" => email, "password" => password}}, socket) do

  send(self(), {:next, %{email: email, password: password}})

  {:noreply, push_navigate(conn, to: ~p"/users/log_in?_action=registered")
end

This resolves the redirect issue discussed above. But now the user won’t get created because the parent never gets time to perform it. What would be optimal would be sending a message to the parent synchronously and then when that returns, return the redirect. But since it’s the same process that would be a deadlock.

I’m not sure if there is a good and clean solution to this.