coen.bakker

coen.bakker

Flakey Wallaby test: `execute_script()` followed by assertion

TLDR; In Wallaby, how do I correctly use execute_script() before an assertion? Does execute_script(_, _, _, assertion_callback) ensure synchronous code execution? But execute_script/2 does not?

I am trying to test an infinity scrolling implementation that uses LiveView’s streams with Wallaby.

My original test looked like this.

  @chat_window Query.data("role", "chat-window")
  @n_posts 40
  test "user scrolls up to see past posts", %{session: session} do
    posts = many_posts(@n_posts)
    topic = insert!(:topic, name: "General", posts: posts)
    groups = [insert!(:welcome_group, topics: [topic])]
    user = insert!(:user, groups: groups)

    session
    |> log_in_user(user)
    |> visit(~p"/groups")
    |> find(@chat_window)

    |> assert_in_viewport("[data-role=\"post\"]:first-child")

    |> scroll_chat_up_a_bit()
    |> refute_in_viewport("[data-role=\"post\"]:first-child")

    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> scroll_chat_up_a_bit()
    |> assert_in_viewport("[data-role=\"post\"]:nth-child(#{@n_posts})")
  end

  defp assert_in_viewport(parent, selector) do
    execute_script(parent,
      """
      const post = document.querySelector(arguments[0]);

      if (!post) return false;

      const isInViewport = (element) => {
        const rect = element.getBoundingClientRect();
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        )
      }

      return isInViewport(post);
      """,
      [selector],
      fn resp -> assert resp == true end
    )
  end

  defp refute_in_viewport(parent, selector) do
    execute_script(parent,
      """
        const post = document.querySelector(arguments[0]);

        const isInViewport = (element) => {
          const rect = element.getBoundingClientRect();
          return (
              rect.top >= 0 &&
              rect.left >= 0 &&
              rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
              rect.right <= (window.innerWidth || document.documentElement.clientWidth)
          )
        }

        return isInViewport(post);
      """,
      [selector],
      fn resp -> assert resp == false end
    )
  end

  defp scroll_chat_up_a_bit(parent) do
    execute_script(parent,
      """
      const chat = document.querySelector('[data-role="chat-window"');
      chat.scrollBy(0, -400);
      """
    )
  end

  defp many_posts(amount) do
    Enum.reduce(1..amount, [], fn n, acc ->
      [insert!(:post, content: "Post #{n}") | acc ]
    end)
  end

This results in unreliable test results. Most of the time the test fails. Sometimes it doesn’t. If I add some :timer.sleep/1 time, the test passes consistently.

I reckoned the problem with my original test was code execution order. I adjusted my test to make use of the optional callback argument of the execute_script\4 function. This is the new test.

  @chat_window Query.data("role", "chat-window")
  @n_posts
  test "user scrolls up to see past posts", %{session: session} do
    posts = many_posts(@n_posts)
    topic = insert!(:topic, name: "General", posts: posts)
    groups = [insert!(:welcome_group, topics: [topic])]
    user = insert!(:user, groups: groups)

    session
    |> log_in_user(user)
    |> visit(~p"/groups")
    |> find(@chat_window)
    |> assert_in_viewport("[data-role=\"post\"]:first-child")

    session
    |> scroll_chat_up_a_bit(1, fn s ->
     refute_in_viewport(s, "[data-role=\"post\"]:first-child")
    end)

    session
    |> scroll_chat_up_a_bit(12, fn s ->
      assert_in_viewport(s, "[data-role=\"post\"]:nth-child(#{@n_posts})")
    end)
  end

  defp assert_in_viewport(parent, selector) do
    execute_script(parent,
      """
      const post = document.querySelector(arguments[0]);

      if (!post) return false;

      const isInViewport = (element) => {
        const rect = element.getBoundingClientRect();
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        )
      }

      return isInViewport(post);
      """,
      [selector],
      fn resp -> assert resp == true end
    )
  end

  defp refute_in_viewport(parent, selector) do
    execute_script(parent,
      """
        const post = document.querySelector(arguments[0]);

        if (!post) throw new Error("Bottom post not found");

        const isInViewport = (element) => {
          const rect = element.getBoundingClientRect();
          return (
              rect.top >= 0 &&
              rect.left >= 0 &&
              rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
              rect.right <= (window.innerWidth || document.documentElement.clientWidth)
          )
        }

        return isInViewport(post);
      """,
      [selector],
      fn resp -> assert resp == false end
    )
  end

  defp scroll_chat_up_a_bit(parent, 0, callback), do: callback.(parent)

  defp scroll_chat_up_a_bit(parent, n, callback) when n > 0 do
    execute_script(parent,
      """
      const chat = document.querySelector('[data-role="chat-window"');
      chat.scrollBy(0, -400);
      """,
      [],
      fn _ -> scroll_chat_up_a_bit(parent, n - 1, callback) end
    )
  end

I was surprised to find that the second test also returns inconsistent test results.

My priority is to understand what is going wrong here. Any ideas?

Secondarily, it could very well be that I am over complicating this test. Better approaches are more than welcome. :slight_smile:

P.S. There is a small section about asynchronous JavaScript in hexdocs of Wallaby, but following its advice did not solve my problem.

Marked As Solved

coen.bakker

coen.bakker

Oh yes. That’s great to know and makes sense.

Thank you.

Also Liked

sodapopcan

sodapopcan

I don’t have a great answer but since you are getting any traction I thought I’d chime in.

Resorting to sleeps in e2e testing is just a fact of life, unfortunately. Even if you don’t call it yourself, most e2e testing frameworks, including Wallaby, are doing it for you. Anything that asserts an element is on the page will keep re-trying to find it for a fixed number of seconds. See the retry definition here which includes a :timer.sleep. That function is an underpinning of the has_text?, has_value?, execute_query, and find functions.

Where Next?

Popular in Questions Top

quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

We're in Beta

About us Mission Statement