LiveView testing expected exception in handle_event

I’ve created a LiveView using phx.gen.live and have modified the generated code to scope actions to the logged in user. I’m currently working on that for the delete action, and think I have it working, but I’m having trouble writing a test for it.

I’ve updated handle_event for delete to use a user-scoped get function instead of the default:

  def handle_event("delete", %{"id" => id}, socket) do
    config_file = Configs.get_user_config_file!(socket.assigns.current_user, id)
    {:ok, _} = Configs.delete_config_file(config_file)

    {:noreply, stream_delete(socket, :config_files, config_file)}
  end

The generated LiveView uses JS.push to send a delete event:

    <.link
      phx-click={JS.push("delete", value: %{id: config_file.id}) |> hide("##{id}")}
      data-confirm="Are you sure?"
    >
      Delete
    </.link>

It looks like I can use render_hook from a test to simulate this delete event:

    test "doesn't delete config_files owned by other users", %{conn: conn} do
      other_user = user_fixture()
      other_config_file = config_file_fixture(other_user, %{name: "other user file"})

      {:ok, index_live, _html} = live(conn, ~p"/app/config/files")

      assert_raise Ecto.NoResultsError, fn ->
        render_hook(index_live, "delete", %{id: other_config_file.id})
      end
    end

However, I think because the exception is raised in another process, the assert_raise doesn’t work and the test fails. Does anyone have any advice on how to write a test for this?

You can call Process.flag(:trap_exit, true) and then use assert_receive to check the process EXIT with a given reason.

1 Like

Thanks José. Sorry for the basic question, but I’m still pretty new to Elixir: where do I call Process.flag? I’ve tried the following, but I’m still seeing the test just erroring out due to the LiveView process crashing:

test "doesn't delete config_files owned by other users", %{conn: conn} do
      other_user = user_fixture()
      other_config_file = config_file_fixture(other_user, %{name: "other user file"})
      Process.flag(:trap_exit, true)

      {:ok, index_live, _html} = live(conn, ~p"/app/config/files")

      render_hook(index_live, "delete", %{id: other_config_file.id})

      receive do
        msg -> IO.inspect(msg)
      end
    end

You did it correctly. Who does the test fail currently?

There is actually some difference with trap_exit. This is the result when running the test without trap_exit:

22:18:38.400 [error] GenServer #PID<0.1224.0> terminating
** (Ecto.NoResultsError) expected at least one result but got none in query:

from c0 in Myapp.Configs.ConfigFile,
  where: c0.user_id == ^"0e18a0a4-ff7f-4e09-acf1-311063a5409d",
  where: c0.id == ^"0dc74e3f-afc1-4f59-81ad-d4f5921d4099"

    (ecto 3.9.5) lib/ecto/repo/queryable.ex:161: Ecto.Repo.Queryable.one!/3
    (myapp_web 0.1.0) lib/myapp_web/live/config_file_live/index.ex:43: MyappWeb.ConfigFileLive.Index.handle_event/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:401: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.2.1) /Users/james/Developer/myapp/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:221: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 4.3) gen_server.erl:1123: :gen_server.try_dispatch/4
    (stdlib 4.3) gen_server.erl:1200: :gen_server.handle_msg/6
    (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: %Phoenix.Socket.Message{topic: "lv:phx-F1Rg6djBAQKBKwZo", event: "event", payload: %{"cid" => nil, "event" => "delete", "type" => "hook", "value" => %{"id" => "0dc74e3f-afc1-4f59-81ad-d4f5921d4099"}}, ref: "1", join_ref: 0}
22:18:38.415 [error] GenServer #PID<0.1222.0> terminating
** (Ecto.NoResultsError) expected at least one result but got none in query:

from c0 in Myapp.Configs.ConfigFile,
  where: c0.user_id == ^"0e18a0a4-ff7f-4e09-acf1-311063a5409d",
  where: c0.id == ^"0dc74e3f-afc1-4f59-81ad-d4f5921d4099"

    (ecto 3.9.5) lib/ecto/repo/queryable.ex:161: Ecto.Repo.Queryable.one!/3
    (myapp_web 0.1.0) lib/myapp_web/live/config_file_live/index.ex:43: MyappWeb.ConfigFileLive.Index.handle_event/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:401: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.2.1) /Users/james/Developer/myapp/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:221: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 4.3) gen_server.erl:1123: :gen_server.try_dispatch/4
    (stdlib 4.3) gen_server.erl:1200: :gen_server.handle_msg/6
    (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:EXIT, #PID<0.1219.0>, {%Ecto.NoResultsError{message: "expected at least one result but got none in query:\n\nfrom c0 in Myapp.Configs.ConfigFile,\n  where: c0.user_id == ^\"0e18a0a4-ff7f-4e09-acf1-311063a5409d\",\n  where: c0.id == ^\"0dc74e3f-afc1-4f59-81ad-d4f5921d4099\"\n"}, [{Ecto.Repo.Queryable, :one!, 3, [file: 'lib/ecto/repo/queryable.ex', line: 161]}, {MyappWeb.ConfigFileLive.Index, :handle_event, 3, [file: 'lib/myapp_web/live/config_file_live/index.ex', line: 43]}, {Phoenix.LiveView.Channel, :"-view_handle_event/3-fun-0-", 3, [file: 'lib/phoenix_live_view/channel.ex', line: 401]}, {:telemetry, :span, 3, [file: '/Users/james/Developer/myapp/deps/telemetry/src/telemetry.erl', line: 321]}, {Phoenix.LiveView.Channel, :handle_info, 2, [file: 'lib/phoenix_live_view/channel.ex', line: 221]}, {:gen_server, :try_dispatch, 4, [file: 'gen_server.erl', line: 1123]}, {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 1200]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 240]}]}}


  1) test Index doesn't delete config_files owned by other users (MyappWeb.ConfigFileLiveTest)
     apps/myapp_web/test/myapp_web/live/config_file_live_test.exs:112
     ** (EXIT from #PID<0.1219.0>) an exception was raised:
         ** (Ecto.NoResultsError) expected at least one result but got none in query:
     
     from c0 in Myapp.Configs.ConfigFile,
       where: c0.user_id == ^"0e18a0a4-ff7f-4e09-acf1-311063a5409d",
       where: c0.id == ^"0dc74e3f-afc1-4f59-81ad-d4f5921d4099"
     
             (ecto 3.9.5) lib/ecto/repo/queryable.ex:161: Ecto.Repo.Queryable.one!/3
             (myapp_web 0.1.0) lib/myapp_web/live/config_file_live/index.ex:43: MyappWeb.ConfigFileLive.Index.handle_event/3
             (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:401: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
             (telemetry 1.2.1) /Users/james/Developer/myapp/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
             (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:221: Phoenix.LiveView.Channel.handle_info/2
             (stdlib 4.3) gen_server.erl:1123: :gen_server.try_dispatch/4
             (stdlib 4.3) gen_server.erl:1200: :gen_server.handle_msg/6
             (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

This is the result with trap_exit:

22:16:05.903 [error] GenServer #PID<0.1224.0> terminating
** (Ecto.NoResultsError) expected at least one result but got none in query:

from c0 in Myapp.Configs.ConfigFile,
  where: c0.user_id == ^"5cef925d-65c6-420c-9c7e-f0a08f374958",
  where: c0.id == ^"815ef352-e223-4b98-886b-e0a73af9f6e3"

    (ecto 3.9.5) lib/ecto/repo/queryable.ex:161: Ecto.Repo.Queryable.one!/3
    (myapp_web 0.1.0) lib/myapp_web/live/config_file_live/index.ex:43: MyappWeb.ConfigFileLive.Index.handle_event/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:401: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.2.1) /Users/james/Developer/myapp/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:221: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 4.3) gen_server.erl:1123: :gen_server.try_dispatch/4
    (stdlib 4.3) gen_server.erl:1200: :gen_server.handle_msg/6
    (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: %Phoenix.Socket.Message{topic: "lv:phx-F1RgxlbcCS-BKwQF", event: "event", payload: %{"cid" => nil, "event" => "delete", "type" => "hook", "value" => %{"id" => "815ef352-e223-4b98-886b-e0a73af9f6e3"}}, ref: "1", join_ref: 0}


  1) test Index doesn't delete config_files owned by other users (MyappWeb.ConfigFileLiveTest)
     apps/myapp_web/test/myapp_web/live/config_file_live_test.exs:112
     ** (exit) exited in: Phoenix.LiveViewTest.call(#Phoenix.LiveViewTest.View<id: "phx-F1RgxlbcCS-BKwQF", module: MyappWeb.ConfigFileLive.Index, pid: #PID<0.1224.0>, endpoint: MyappWeb.Endpoint, ...>)
         ** (EXIT) an exception was raised:
             ** (Ecto.NoResultsError) expected at least one result but got none in query:
     
     from c0 in Myapp.Configs.ConfigFile,
       where: c0.user_id == ^"5cef925d-65c6-420c-9c7e-f0a08f374958",
       where: c0.id == ^"815ef352-e223-4b98-886b-e0a73af9f6e3"
     
                 (ecto 3.9.5) lib/ecto/repo/queryable.ex:161: Ecto.Repo.Queryable.one!/3
                 (myapp_web 0.1.0) lib/myapp_web/live/config_file_live/index.ex:43: MyappWeb.ConfigFileLive.Index.handle_event/3
                 (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:401: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
                 (telemetry 1.2.1) /Users/james/Developer/myapp/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
                 (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:221: Phoenix.LiveView.Channel.handle_info/2
                 (stdlib 4.3) gen_server.erl:1123: :gen_server.try_dispatch/4
                 (stdlib 4.3) gen_server.erl:1200: :gen_server.handle_msg/6
                 (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Yes, that makes sense. The render_hook call is exiting, so you can do both trap_exit (to avoid the linked process from crashing your own) and catching the render_hook exit:

assert catch_exit(render_hook(...))

Awesome, that works perfectly! I also added a capture_log tag to avoid the error being printed to the console. This is the final version:

    @tag capture_log: true
    test "doesn't delete config_files owned by other users", %{conn: conn} do
      other_user = user_fixture()
      other_config_file = config_file_fixture(other_user, %{name: "other user file"})
      Process.flag(:trap_exit, true)

      {:ok, index_live, _html} = live(conn, ~p"/app/config/files")

      assert {{%Ecto.NoResultsError{}, _}, _} =
               catch_exit(render_hook(index_live, "delete", %{id: other_config_file.id}))
    end
2 Likes