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.
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.
I find that the loop count/iteration is less than expected, in my case it's expected 4 vs actual 2.
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.
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+1
th element will occupy the position of n
th 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.
[0] = element A
[1] = element B
[2] = element C
[3] = element D
[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
}