Testing LiveView which relies on attach_hook

Hi,

I’m testing a LiveView page with components that rely on an attach_hook to run.

The hook looks like this:

attach_hook(socket, :set_current_url, :handle_params, fn
  _params, url, socket ->
    uri = URI.parse(url)

    if uri.query do
      {:cont, assign(socket, current_url: uri.path <> "?" <> uri.query)}
    else
      {:cont, assign(socket, current_url: uri.path)}
    end
end)

The component responses to a handle_event with: {:noreply, push_redirect(socket, to: socket.assigns.current_url)} and fails if current_url is not set. Since handle_params should be executed multiple times in the lifecycle, we do not experience any failure in dev/prod.

The test looks like this:

{:ok, view, html} = live(conn, "/people")
view 
  |> element("li.context-menu-item a[phx-value-user=2]", "Change group")
  |> render_click()
  |> form("#update-group-assignations", %{groups: ["1", "2"]})
  |> render_submit()

This executes the {:noreply, push_redirect(socket, to: socket.assigns.current_url)} mentioned above and fails because key :current_url not found in .... I tried debugging the issue and can see the hook being executed multiple times (simple IO.inspect() inside the hook.

Maybe somebody has any idea what’s wrong?

You should show your test code, then maybe someone could help

1 Like

Thanks for your reply! I have changed the initial post.

This can’t work, because render_click returns a string and form expects a view.

Try this:

view 
  |> element("li.context-menu-item a[phx-value-user=2]", "Change group")
  |> render_click()

view
  |> form("#update-group-assignations", %{groups: ["1", "2"]})
  |> render_submit()

You are correct, my code would not work! The actual code is split into a setup and test code. It works, the form is submitted but the code fails in the redirect because the current_url is missing.

Then show us the real test code that triggers the actual error please :slight_smile:

Here it is (I rewrote it to make it more clear and ran it before posting, so this is exactly the code that gets executed):

test "changes employee after change of group and submit", %{conn: conn} do
  {:ok, view, html} = live(conn, "/people")

  view
  |> element("li.context-menu-item a[phx-value-user=2]", "Change group")
  |> render_click()

  view
  |> form("#update-group-assignations", %{groups: ["1", "2"]})
  |> render_submit()

  refute has_element?(view, "#group-change-table")
end

Hope this helps and sorry for the confusion.

can you show how the hook is attached so someone can try to reproduce the error? For example, are you attaching it in your mount/3 call back, or an on_mount/4 hook declared in the module body or in the router as a live session? And what version of live view? Looks like a strange error, if you provide more details on how to reproduce I would gladly give it a try.

Also just b/c I have to ask, there’s no little typo like uri vs url?

Let’s see. In our AppWeb module, we have

def live_view do
  quote do
    use Phoenix.LiveView,
      layout: {AppWeb.LayoutView, "live.html"}

    import AppWeb.LiveHelpers

    unquote(view_helpers())
  end
end

and inside the AppWeb.LiveHelpers, we have

def assign_defaults(session, socket) do
  # some code
  socket =
      attach_hook(socket, :set_current_url, :handle_params, fn
        _params, url, socket ->
          uri = URI.parse(url)

          if uri.query do
            {:cont, assign(socket, current_url: uri.path <> "?" <> uri.query)}
          else
            {:cont, assign(socket, current_url: uri.path)}
          end
      end)
  socket
end

Our LiveView version is 0.17.10.

Since it’s working when using it in dev, I don’t think there are any typos or weird stuff. And, as said, if I put a IO.inspect into the attach_hook callback, it gets called in the tests.

thanks, so you are attaching the hook in your mount/3 callback with something like {:ok, assign_defaults(session, socket)}?

I have used hooks very similar to this one with no trouble in testing which is why I am asking for so much context, thanks in advance

Feel free to ask as many questions as you need. I am the one to be thankful for the help!

In the LiveView itself, we use socket = assign_defaults(session, socket) inside the mount, yeah. The component does not have a mount and the update does not have the assign_defaults, but I think it doesn’t need it since it’s never mounted outside the LiveView itself?

1 Like

There’s nothing that looks suspicious to me here, I think you’re gonna have to share more code. Even better, you could create a little github repo to showcase the error.

2 Likes

I agree, need to see more code. I tried to reproduce the error with the context that’s been given, but no success. I created a gist

Note that while I don’t actually push_redirect, my live view depends on the current_url assign being present or else it will crash. But my tests are green.

1 Like

I have a suspicion that the answer lies in your update/2 callback of your live component. All of the assigns you need in your component must be explicitly handled in update/2 if you are defining the callback. If handle_event/3 is triggered on the component rather than the live_view, and your form has phx-target={@myself}, and you are not explicitly assigning :current_url in update/2, then you will get the key error on submit. This is probably the code we need to see that we haven’t yet seen :slightly_smiling_face: . Your update/2 callback should have something like this:

def update(assigns, socket) do
  {:ok, assign(socket, :current_url, assigns.current_url}
end

I think that your test is revealing a bug that you did not know that you have, because it is hidden from you in the browser. Since you are doing a self redirect to the same page, presumably reloading fresh data on mount, you are not noticing that you are actually crashing the live view and reloading the page from scratch. The result in the browser would look the same, you’d have to look at your logs to see the crash

2 Likes

You are correct. Wow, I wasted a ton of time there because I didn’t notice this crash and assumed the code is correct and something is wrong with the test. As you pointed out, I missed assigning the current_url in the update function and it crashed, but since it restarted right away and the redirect is intended to go to the same page, this crash is not noticeable from the frontend.

Thanks for your help! And also thanks to @trisolaran!

2 Likes

wow, that was some good thinking! Well done :clap:

1 Like