Testing JavaScript hooks (infinite scroll)
LiveView can do a lot for us. But there are times when we want to interact with JavaScript. For those cases, LiveView exposes hooks.
Phoenix.LiveViewTest comes with helpers that can help us test part of that. Let’s see what we can test.
TimelineLive
If you open your router, you’ll see that we have TimelineLive mounted on /timeline.
If you launch your server and navigate there, you’ll see that our timeline has a list of posts.
By default we’re loading 10 posts at a time. When you go past that, you’ll notice that we automatically load more posts. Our page has infinite scroll-like behavior.
Let’s take a look at the TimelineLive code:
defmodule RangerWeb.TimelineLive do
use RangerWeb, :live_view
alias Ranger.Timeline
def render(assigns) do
~H"""
<ul
id="posts"
role="list"
class="px-6 py-3 rounded-md bg-gradient-to-b from-cyan-100 to-red-100 mx-auto max-w-lg divide-y divide-gray-300"
phx-update="stream"
>
<li :for={{id, post} <- @streams.posts} id={id} class="py-6 space-y-2" data-role="post">
<div class="font-semibold" data-role="author"><%= post.author %></div>
<div class="text-lg"><%= post.text %></div>
</li>
</ul>
<div class="py-4 text-center text-lg animate-pulse" id="load-more" phx-hook="InfiniteScroll">
Loading ...
</div>
"""
end
def mount(_, _, socket) do
offset = 0
limit = 10
posts = Timeline.posts(limit: limit)
{:ok,
socket
|> stream(:posts, posts)
|> assign(:offset, offset)
|> assign(:limit, limit)}
end
def handle_event("load-more", _, socket) do
socket =
socket
|> update(:offset, fn offset -> offset + socket.assigns.limit end)
|> stream_new_posts()
{:noreply, socket}
end
defp stream_new_posts(socket) do
offset = socket.assigns.offset
limit = socket.assigns.limit
posts = Timeline.posts(offset: offset, limit: limit)
Enum.reduce(posts, socket, fn post, socket ->
stream_insert(socket, :posts, post)
end)
end
end
As you can see:
- In
render/1we render each post in@streams.posts. - In
mount/3we seed posts withoffset: 0andlimit: 10. - We have a loading indicator tied to
phx-hook="InfiniteScroll". - When the hook sends a
"load-more"event,handle_event/3loads another 10 posts.
Let’s take a look at the JavaScript side of things. Open app.js:
let Hooks = {}
Hooks.InfiniteScroll = {
mounted() {
const observer = new IntersectionObserver(entries => {
const entry = entries[0];
if (entry.isIntersecting) {
this.pushEvent("load-more");
}
});
observer.observe(this.el);
}
}
As you can see, we’re using IntersectionObserver to push an event when the loading indicator intersects the viewport.
Testing TimelineLive
defmodule RangerWeb.TimelineLiveDoneTest do
use RangerWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Ranger.Timeline
test "user can see older posts with infinite scroll", %{conn: conn} do
[post] = Timeline.posts(offset: 10, limit: 1)
{:ok, view, _html} = live(conn, ~p"/timeline")
view
|> element("#load-more", "Loading ...")
|> render_hook("load-more")
assert has_element?(view, "[data-role=post]", post.text)
assert has_element?(view, "[data-role=author]", post.author)
end
end
What this does:
- In setup, we grab the 11th post (
offset: 10, limit: 1). - We mount with
live/2. - We target
#load-moreand performrender_hook("load-more"). - We assert the new post and author are present.
As you might notice, render_hook/2 is slightly different from other helpers because we pass the event name explicitly.
That’s because the event is coming from JavaScript code, not from a phx-* event in static HTML.
And that is the downside of testing JavaScript hooks in LiveView tests: we can only test the Elixir portion of the hook. If you remove the JavaScript hook code completely, this test will continue to pass.
Is element/3 helpful in this case?
Yes. element/3 still provides safety because it ensures our #load-more element with that selector and text is present in the rendered page.
What if we want to test JS?
Whether or not you want to test the JavaScript portion is up to you.
If the JS hook is playing a vital role (for example, checkout flow), it can be worth introducing end-to-end tooling.
If you’re interested in that, take a look at Wallaby or Hound.
Links
- JS hooks: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks
render_hook/3helper: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#render_hook/3- Wallaby: https://hexdocs.pm/wallaby/readme.html
- Hound: https://hexdocs.pm/hound/readme.html
IntersectionObserver: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver