Search code examples
javalistfor-loopselenide

Java Selenide - For-Looping ElementsCollection with Modifications During Loop


Background

I've been programming in Java for over 3 years but this is my first time I feel that I still do not understand how a basic for loop works, oh my gosh!

So I have a Selenide project. Selenide is a web automated testing library based on SeleniumHQ.

I have a simple for loop that loops through a list of image elements.


Project Setup

Dependency:

compile group: 'com.codeborne', name: 'selenide', version: '5.2.2'

Imports:

import static com.codeborne.selenide.Selenide.$$;
import static com.codeborne.selenide.Selectors.byXpath;

Evil code:

final ElementsCollection list = $$(byXpath("//img[@src='original src']"));
for (int i=0; i<list.size(); i++) {
    doSth(list.get(i));
}

The doSth method:

void doSth(SelenideElement element) {
    Selenide.executeJavaScript("arguments[0].src = 'new src';", element);
}

Of course the above code is a simple version of the code I'm working on but the core structure remains.


Problem

I find that the loop count/iteration is less than expected, in my case it's expected 4 vs actual 2.


Solution

  • Root Cause Analysis

    The doSth method tries to change the src attribute of the img element that comes from the parameter. After the change, the img element will no longer fulfil the XPath. Whether or not the img element fulfils the XPath after modification matters because of how Selenide retrieves the elements.

    As the element collection selected by an XPath by Selenide is lazily evaluated just like individual element objects, each of the elements in the collection stores only the element index and the collection XPath (this is mentioned in Selenide's JavaDoc). When the src attribute of one of the collection elements is updated, it affects subsequent loop iteration by causing the dynamic collection size to shrink.

    After reading the source code of the ElementsCollection class, we will know that ElementsCollection is an AbstractList with overridden methods. As the termination condition in a Java for loop gets evaluated in every iteration, the overridden size() method is actually returning a smaller number with a decrement of 1 after every loop iteration.


    Solution

    Since the first (index of 0) item of the list has its src modified, the list will shrink its size by 1 after each iteration, the n+1th element will occupy the position of nth element. I can always get the first (index of 0) item which in fact gives me the next item as the previous item will no longer remain on the dynamic list.

    • 1st iteration:
      • [0] = element A
      • [1] = element B
      • [2] = element C
      • [3] = element D
    • 2nd iteration:
      • [0] = element B
      • [1] = element C
      • [2] = element D

    Modify the loop as follows:

    final ElementsCollection list = $$(byXpath("//img[@src='original src']"));
    final int count = list.size(); // extract to evaluate once only
    for (int i=0; i<count; i++) {
        doSth(list.get(0)); // change i to 0
    }