Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing for errors raised in a liveview #3329

Open
LostKobrakai opened this issue Jul 2, 2024 · 5 comments
Open

Testing for errors raised in a liveview #3329

LostKobrakai opened this issue Jul 2, 2024 · 5 comments

Comments

@LostKobrakai
Copy link
Contributor

Environment

  • Elixir version (elixir -v): 1.17.1
  • Phoenix version (mix deps): 1.7.12
  • Phoenix LiveView version (mix deps): 1.0.0-rc.0
  • Operating system: Mac OS
  • Browsers you attempted to reproduce this bug on (the more the merrier):
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no:

Actual behavior

An exception in the LV crashes the test.

Expected behavior

Some way to catch the exception and at best assert on it.

@SteffenDE
Copy link
Collaborator

Well, there is "some way", but it's indeed not very nice:

Application.put_env(:phoenix, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7"},
  # please test your issue using the latest version of LV from GitHub!
  {:phoenix_live_view, github: "phoenixframework/phoenix_live_view", branch: "main", override: true},
  {:floki, ">= 0.30.0"}
])

ExUnit.start()

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def mount(params, _session, socket) do
    if params["raise"] == "mount", do: raise "oops"

    socket
    |> then(&{:ok, &1})
  end

  def handle_event("boom", _params, _socket), do: raise "boom"

  def render("live.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js"></script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <style>
      * { font-size: 1.1em; }
    </style>
    <%= @inner_content %>
    """
  end

  def render(assigns) do
    ~H"""
    <p>The LiveView content goes here</p>
    <button phx-click="boom"></button>
    """
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :phoenix
  socket("/live", Phoenix.LiveView.Socket)
  plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
  plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
  plug(Example.Router)
end

defmodule Example.HomeLiveTest do
  use ExUnit.Case

  import Phoenix.ConnTest
  import Phoenix.LiveViewTest

  @endpoint Example.Endpoint

  test "works properly" do
    conn = Phoenix.ConnTest.build_conn()

    catch_exit(live(conn, "/?raise=mount"))
  end

  test "raise on event" do
    conn = Phoenix.ConnTest.build_conn()
    {:ok, view, _html} = live(conn, "/")

    Process.flag(:trap_exit, true)

    catch_exit(view |> element("button") |> render_click())

    assert_receive {:EXIT, _, _}
  end
end

{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
ExUnit.run()

For errors during mount you can use catch_exit and for errors during events you need to use both catch_exit and trap exits for the test process itself. Maybe there's a better way? @josevalim

@LostKobrakai
Copy link
Contributor Author

I wasn't aware of catch_exit. That might actually be enough for what I was expecting. I've always manually done monitoring, but that doesn't work well with LV as there's multiple linked processes involved.

@LostKobrakai
Copy link
Contributor Author

Hmm, this is close, but the pid of the LV expectedly is a different one the view.pid, so one cannot really match to a specific exit message.

@LostKobrakai
Copy link
Contributor Author

Process.flag(:trap_exit, true)

assert {{exception, _}, _} =
         catch_exit(
           view
           |> FloorplanPage.switch_rocker_configuration(switch_id)
           |> FloorplanPage.add_trigger_function(target: "123", at: -1, mode: "toggle")
         )

assert "Unknown channel" = Exception.message(exception)

This seems to works, but also leaves a big red error being logged.

@josevalim
Copy link
Member

You will have to @tag :capture_log, as you do when testing processes. But generally speaking I think this is the direction to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants