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

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
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
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
RisingFromAshes
I've read in another post that it may be possible with a router helper - but I couldn't find an appropriate one, and tbh, I'm still just ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
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
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
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New

Other popular topics Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30840 112
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
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
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126226 1237
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement