Browse lessons

Testing Live Redirects

In this lesson we’re going to cover how to test live redirects, part of the live navigation equation.

We’ll cover how to test live patches in our next lesson.

So, in this lesson we’ll cover how to test navigation between two LiveViews in the same live_session, but we’ll also look at how to test navigation from a LiveView to a non-LiveView (or from LiveView to LiveView but in different live sessions).

In practice, that means we’ll look at how to test <.link> components that use the navigate strategy vs the href strategy.

That will help us see the differences and similarities of testing navigation between LiveViews in the same live_session and navigation to other pages.

Dashboard live

Here’s the code for our DashboardLive:

defmodule RangerWeb.DashboardLive do
  use RangerWeb, :live_view

  import RangerWeb.NavigationComponents

  def render(assigns) do
    ~H"""
    <div class="min-h-full">
      <.navbar active={:dashboard} />

      <div class="py-10">
        <header>
          <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold leading-tight tracking-tight text-gray-900">Dashboard</h1>
          </div>
        </header>
      </div>
    </div>
    """
  end
end

As you can see, the core of the work happens in the <.navbar> component found in NavigationComponents.

Let’s take a look at that:

defmodule RangerWeb.NavigationComponents do
  use Phoenix.Component

  use Phoenix.VerifiedRoutes,
    endpoint: RangerWeb.Endpoint,
    router: RangerWeb.Router,
    statics: RangerWeb.static_paths()

  attr :active, :atom, required: true, values: [:dashboard, :team]

  def navbar(assigns) do
    ~H"""
    <nav class="border-b border-gray-200 bg-white">
      <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <div class="flex h-16 justify-between">
          <div class="flex">
            <div class="sm:-my-px sm:flex sm:space-x-8">
              <.link
                navigate={~p"/dashboard"}
                class={[
                  "inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium",
                  color_classes(@active == :dashboard)
                ]}
                data-role="page-link"
              >
                Dashboard
              </.link>

              <.link
                navigate={~p"/team"}
                class={[
                  "inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium",
                  color_classes(@active == :team)
                ]}
                data-role="page-link"
              >
                Team
              </.link>
            </div>
          </div>
        </div>
      </div>
    </nav>
    """
  end

  defp color_classes(active) do
    if active do
      "border-orange-500 text-gray-900"
    else
      "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
    end
  end
end

Here you can see that our navigation is built with two tabs:

  • One takes us to the Dashboard (<.link navigate={~p"/dashboard"}>), and
  • The other takes us to the Team (<.link navigate={~p"/team"}>).

If you look at our router, you’ll see that those two LiveViews are part of the same live_session:

live_session :app do
  live "/dashboard", DashboardLive
  live "/team", TeamLive
end

But before we see how to test live navigation between two LiveViews, let’s first see how to test navigation to a non-LiveView. That’ll give us a good point of comparison.

Testing LiveView to Non-LiveView navigation

To test this, we’ll test that we can click on the top Testing LiveView logo and that should take us to the home page (which is a regular static page).

defmodule RangerWeb.DashboardLiveDoneTest do
  use RangerWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  describe "Navigates to home page (outside of live_session)" do
    test "when clicking Testing LiveView (error tuple)", %{conn: conn} do
      {:ok, view, _html} = live(conn, ~p"/dashboard")

      {:error, {:redirect, %{to: path}}} =
        view
        |> element("#logo")
        |> render_click()

      assert path == ~p"/"
    end

    test "when clicking Testing LiveView (assert_redirect)", %{conn: conn} do
      {:ok, view, _html} = live(conn, ~p"/dashboard")

      view
      |> element("#logo")
      |> render_click()

      {path, _flash} = assert_redirect(view)
      assert path == ~p"/"
    end

    test "when clicking Testing LiveView (follow_redirect)", %{conn: conn} do
      {:ok, view, _html} = live(conn, ~p"/dashboard")

      {:ok, conn} =
        view
        |> element("#logo")
        |> render_click()
        |> follow_redirect(conn, ~p"/")

      html = html_response(conn, 200)
      assert html =~ "Testing"
      assert html =~ "LiveView"
    end
  end
end

As you can see, there are three ways to write this test:

  • In the first, we pattern match the :error tuple and get the path we’re redirecting to. But that’s a little confusing because it seems like we’re erroring.
  • A better approach is to use the assert_redirect/2 helper. That more clearly shows that the redirection is an expected part of our test.
  • A third approach is to follow the redirect with follow_redirect/3. That is very helpful when we want to assert things about the next page.

Note: these are the same tests we would write if we were navigating between two LiveViews that did not have the same live_session.

Testing LiveView to LiveView navigation in same live session

Now let’s see how those tests compare to the tests we write when we navigate between two LiveViews that are mounted in the same live_session.

describe "Navigates to another LiveView (within live_session)" do
  test "when clicking Team (error tuple)", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/dashboard")

    {:error, {:live_redirect, %{to: path}}} =
      view
      |> element("[data-role=page-link]", "Team")
      |> render_click()

    assert path == ~p"/team"
  end

  test "when clicking Team (assert_redirect)", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/dashboard")

    view
    |> element("[data-role=page-link]", "Team")
    |> render_click()

    {path, _flash} = assert_redirect(view)
    assert path == ~p"/team"
  end

  test "when clicking Team (follow_redirect)", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/dashboard")

    {:ok, _team_view, team_html} =
      view
      |> element("[data-role=page-link]", "Team")
      |> render_click()
      |> follow_redirect(conn, ~p"/team")

    assert team_html =~ "Team"
  end
end

As you can see, the tests are very familiar with a few differences:

  • For the :error tuple, we see a different strategy being highlighted in the return value (:live_redirect vs :redirect).
  • assert_redirect/1 is identical, and therefore, can be helpful to use when we don’t want to make a distinction about which kind of navigation we expect.
  • We can also follow the redirect with follow_redirect/3 and, in this case, LiveView mounts the next LiveView and gives us a new view struct and HTML.

Testing push_navigate

Even though we’ve been dealing with live navigation through the <.link> component here, the same concepts can be applied if we’re using a push_navigate helper. For example, if you have a form that saves and then uses push_navigate to take us to a different LiveView, you can use follow_redirect/3 exactly like we used it here.