Testing LiveView and Presence: How to simulate a user has left (e.g. closed tab)

I have a LiveView that tracks how many users are viewing another page. The other page is using Presence to track when users enter and leave. Everything works fine during manual testing.

For my tests, I need to simulate that a user has left the page (closed tab, clicked a link, etc.) so that they no longer appear in the list of users being tracked. I am unable to simulate such an event in my Elixir test code.

In my Elixir test code, how can I simulate that a LiveView user has exited the page?

EDIT: I can use render_click/2 to click a link that happens to be in the view and that does the trick for now, but hopefully there’s something a little less hacky that I can use.

You could always just kill the process:

{:ok, lv, _html} = live(conn, ~p"/the-page")

Process.exit(lv.pid, :kill)

If there is a cleaner way I am not sure.


I tried that, it causes the whole test to fail:

21:35:39.842 [error] GenServer #PID<0.465.0> terminating
** (stop) killed
Last message: {:EXIT, #PID<0.461.0>, :killed}

  1) test PageLive renders expected presence data (ProjectWeb.StatsLiveTest)
     ** (EXIT from #PID<0.461.0>) killed

Dang, I’m also interested, though. I’ve only ever tested this stuff with Wallaby.

Can you post your whole test?

It’s a really long test, so there’s a lot of cruft. I tried making a barebones version and it’s not crashing the whole test anymore. I’ll poke around for a bit and see if I can find out what is causing the issue.

Thanks for the hint. I have to make dinner so I’ll be back with more info in a bit. :smiley:

It looks like killing the LiveView process causes the test to crash pretty much immediately. This example will crash:

  test "test crash example", %{conn: conn, quiz: quiz} do
    {:ok, view, html} = conn |> live(~p"/contact-us")
    Process.exit(view.pid(), :kill)

I’m in over my head on this one. Until I can figure out how to gracefully end the LiveView processes, I’m just gonna stick to my simulated link-clicking hack.

Try Process.exit(view.pid, :normal). Docs: Process — Elixir v1.15.5

Using :normal does not appear to have any effect.

Here’s a brief version of the test I’ve been working with (it’s for a toy project where multiple users can take a quiz while a teacher supervises the results):

    test "renders expected presence data as the user progresses through the quiz", %{
      conn: conn,
      quiz: quiz
    } do
      {:ok, quiz_stats_view, html} = conn |> live(_get_quiz_stats_url(quiz.id))

      # quiz_stats: template contains expected content before user joins the quiz lobby
      assert html =~ "No users are preparing to take this quiz."

      ## quiz_take: unauthenticated user joins the quiz lobby
      {:ok, quiz_take_view, _html} = build_conn() |> live(_get_quiz_take_url(quiz.id))

      # quiz_stats: template no longer contains initial content after receiving Presence data broadcast
      user_joined_html = render(quiz_stats_view)

      refute user_joined_html =~ "No users are preparing to take this quiz."

      # quiz_stats: template contains placeholder text for unauthenticated user with no name
      assert html_element_has_content(
               "No name yet"

      ## quiz_take: user leaves the page (hacky, but it works)
      # quiz_take_view |> element(~s|a|, "Exit this quiz") |> render_click()

      ## quiz_take: user leaves the page (not hacky, but crashes the test)
      Process.exit(quiz_take_view.pid(), :normal)

      # quiz_stats: template no longer contains information about this user
      user_exited_html = render(quiz_stats_view)
      refute user_exited_html =~ "No name yet"

To recap: Process.exit(quiz_take_view.pid(), :kill) crashes the entire test, and Process.exit(quiz_take_view.pid(), :normal) appears to have no effect whatsoever (even after a lengthy Process.sleep(30_000)). Simulating a link being clicked seems to shut down the view gracefully (thus the test can complete successfully), but I have no idea what’s going on behind the scenes (I’ve looked, but there’s a lot going on that goes above my head).

Tested on Elixir 1.15.5-otp-26 on Linux.


The issue is that two things are started. A LV client and a channel for communication. Both seem to be linked to the test process. Most of that stuff is private though. stop_supervised(view.pid) seems to work, though with a warning. I’d argue that LV should provide an API to close a view.


Finally got it! GenServer.stop(view.pid()) did the trick.


To simulate a user being unauthenticated and disconnected in all liveviews and channels you can do:

MyAppWeb.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})

This covered in the documentation here Disconnecting all instances of a live user.