Why you shouldn't use ExpectedConditions with your Page Object in Selenium Java

ExpectedConditions is somewhat everyone uses with their classic approach to look up elements with Wait-like classes. However when you implement your tests in Page Object design pattern, there is another mechanism that goes to the foreground. In this article we’ll see what ExpectedConditions is, why its design is arguable, look at the example case that implements Page Object design pattern and finally implement conditional waiting for the page object elements using the approach Selenium supports for Page Object out of the box.

Example description: Page Object test which expects the elements to meet certain conditions

Assume that you have a regular Page Object class that most of the people would use to describe, say, a menu of https://webelement.click/en/welcome. Let’s take a look at that:

package click.webelement.pagefactory.conditions;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class MenuPage {

    @FindBy(xpath = ".//li[1]/a")
    WebElement main;

    @FindBy(xpath = ".//li[2]/a")
    WebElement aboutMe;

    public MenuPage(WebDriver driver){
        PageFactory.initElements(driver, this);
    }

    public MenuPage switchToMain(){
        main.click();
        return this;
    }

    public MenuPage switchToAboutMe(){
        aboutMe.click();
        return this;
    }

}

What can we see here? Quite a standard stuff, right? Fields which are described as WebElement, and methods which access the fields in some way. The problem is that such the design does not allow to wait for the particular state of the element. For example if the element is under control of JavaScript it might change the state after a certain time and we might want to consider the element ready for interaction only after it has taken the mentioned state.

In a regular approach (not Page Object one) we would use Wait and ExpectedConditions classes. However for Page Object design which has a native support in Selenium there is another mechanism intended to address such things.

What is ExpectedConditions and when do we use it

Basically, ExpectedConditions is an utility class that provides preset objects which implement Function interface. When you define such an object you have to specify a type that would be taken as a parameter of a function and a type that would be returned by a function. In the implementation that is delivered with Selenium Java bindings, each condition in ExpectedConditions class is a Function that always takes WebDriver as the parameter and returns a type that is specific for a particular condition. The example of usage could look like the following:

private void doClick(){
    Wait<WebDriver> waiter = new FluentWait<>(driver);
    WebElement element = waiter
                .until(
                    ExpectedConditions
                        .visibilityOfElementLocated(By.xpath(".//some-element"))
                );
    element.click();
}

Thus the main arguable point of ExpectedConditions class is that while waiters like FluentWait support any sort of the parameter that would be accepted by a conditional function (you are even not limited with any Selenium-related things - you can just wait for any sort of event), the conditions which are pre-implemented within ExpectedConditions class are limited with WebDriver implementation. The controversy of the design used can even be seen in such "conditions" of ExpectedConditions class like:

public static ExpectedCondition<Boolean> attributeToBeNotEmpty(final WebElement element,
                                                               final String attribute) {
  return driver -> getAttributeOrCssValue(element, attribute).isPresent();
}

Above you can see that the driver reference is not even used in lambda expression. It is just ignored. And there are quite a lot of methods like the shown one. This is what I call freaky design.

Using waiters in page object that is created with PageFactory.init()

As it has been mentioned in introduction, Selenium has a special mechanism of waiting for element to take a certain condition in page object. To understand that lets take a quick look at the overall process of creating a page with PageFactory:

  1. There are several init() methods in PageFactory class. Each is used to inject fields to your page object. One can either use more easy init() so that it will cascadingly call other init methods with some default values, or use more complex init() with some custom non-default settings.

  2. One of the core concepts of that approach is so called ElementLocator. Objects which implement this interface are used by Selenium to bind the logic of lookup to a particular field of your page class.

  3. There are two default implementations of mentioned interface: DefaultElementLocator and AjaxElementLocator.

    1. DefaultElementLocator introduces some simple lookup logic. Basically it just delegates all the logic to a SearchContext that is currently used (either a WebDriver or WebElement). However it supports element caching so having that option enabled would force Selenium to use already located element rather than re-lookup it on the page (this is not always a good idea since you may go into stale element issue).

    2. AjaxElementLocator extends DefaultElementLocator. It is designed in the way to re-try lookup if the element was not located. This is what we’re going to take a closer look at.

  4. There is also a LocatorFactory playing important role. This interface defines a factory that is to return ElementLocator for a particular field of your page class. We’re going to implement something very trivial for our factory. It will be creating the same locators for all the fields. However in your case you will probably want to return different locators depending on, say, a value of your custom annotation that you are planning to annotate your fields with.

Having all these points considered let’s move to locator implementation.

Customizing AjaxElementLocator for our purpose

The implementation of AjaxElementLocator wraps the lookup logic of parent class into retry loop but it considers the element is ready for use as soon as it appears in DOM. This is the behavior that we would like change. Luckily Selenium developers have foreseen the need of such extension. There is the method that is intended to address checking the condition. Its default implementation is

protected boolean isElementUsable(WebElement element) {
  return true;
}

As you have noticed, the default implementation always returns true which means that once the element is located there is no any additional check performed. This is what we’re going to fix in particular. Since the change is really tiny, we’ll not be creating a separate class for that. Instead we’ll create our custom implementation of ElementLocatorFactory and extend the AjaxElementLocator right in its create method.

Since we’re testing our conditions within the method that overrides isElementUsable it will look organic to build our class around WebElement rather than a WebDriver like it happens in original ExpectedCondition. This is why in the beginning I noticed that the existing condition implementation does not really fit Page Object construction in Selenium in the best way.

package click.webelement.pagefactory.conditions;

import java.util.function.Function;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.pagefactory.AjaxElementLocator;
import org.openqa.selenium.support.pagefactory.ElementLocator;
import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;
import java.lang.reflect.Field;

public class AjaxConditionalLocatorFactory implements ElementLocatorFactory {

    final SearchContext searchContext;
    final int timeOutInSeconds;
    final Function<WebElement, Boolean> condition;

    public AjaxConditionalLocatorFactory(SearchContext searchContext,
                                         int timeOutInSeconds,
                                         Function<WebElement, Boolean> condition) {
        this.searchContext = searchContext;
        this.timeOutInSeconds = timeOutInSeconds;
        this.condition = condition;
    }

    @Override
    public ElementLocator createLocator(Field field) {
        return new AjaxElementLocator(searchContext, field, timeOutInSeconds) {
            @Override
            protected boolean isElementUsable(WebElement element) {
                return condition.apply(element);
            }
        };
    }
}

Here are few remarkable points regarding this class:

  1. Since implementation of AjaxElementLocator requires SearchContext, Field and int as parameters we need to supply them to our factory. Field is required according to the interface contract and will be provided by Selenium framework. However other stuff is under our own responsibility so that we have to set up it in our constructor.

  2. Since we are going to implement condition check logic, I suggest to use the same approach as they use in ExpectedConditions. A condition (in both our and their approaches) is an object that implements Function<T, R> interface where T is a type of parameter the function accepts, and R is a type of result that the function returns. As it is seen from my implementation, our conditions would take WebElement as a parameter and would return Boolean value.

  3. Function interface defines the only apply() method. So that our overridden isElementUsable method would just delegate the check to the condition object.

Implement our custom conditions

Let’s implement couple of conditions. Much like in ExpectedConditions class we’ll prepare a class that would contain static methods returning Function<WebElement, Boolean>. Check the sample implementation:

package click.webelement.pagefactory.conditions;

import org.openqa.selenium.WebElement;
import java.util.function.Function;
import java.util.regex.Pattern;

public class MyConditions {

    public static Function<WebElement, Boolean> elementIsVisibleAndEnabled(){
        return webElement -> webElement.isDisplayed() && webElement.isEnabled();
    }

    public static Function<WebElement, Boolean> elementTextCorrespondsToAPattern(final Pattern p){
        return webElement -> {
            String elementText = webElement.getText();
            if(p.matcher(elementText).matches()){
                return true;
            }else{
                return false;
            }
        };
    }

}

As you can see, here we have two conditions implemented (just as an example). First would check if the element is not just in DOM but also visible and enabled, and the second one performs more complex check: it tests if the element’s text corresponds to a specified pattern. Having this example you can easily add your own checks.

So we now need to take two more steps to have the puzzle solved: amend our page class constructor to make it constructed with our custom factory and finally write some sample test class.

Make your page class be constructed with custom ElementLocatorFactory

Now we have to change our page class a bit. Instead of using PageFactory.init(WebDriver, Object) method we’re going to use PageFactory.init(ElementLocatorFactory, Object) which is really more flexible and will fit our design. This is how our new constructor would look like:

public MenuPage(SearchContext searchContext, Function<WebElement, Boolean> condition){
    PageFactory.initElements(
            new AjaxConditionalLocatorFactory(
                    searchContext,
                    10,
                    condition
            ),
            this
    );
}

Here we changed several things in our original constructor. First of all we changed the parameters set. Instead of WebDriver we now take SearchContext so that we can instantiate our page not only against the whole driver but also against any implementation of SearchContext. For example it could be a WebElement. Second is that we also take a condition object that would define the logic of how we consider the element is ready.

In this example we use quite a simple architecture that implies that our conditions accept only WebElement parameter (after all we’re implementing the conditions which are to be used within our overridden isElementUsable method) and that all the elements of the page instance are handled by the same readiness condition. If you need to have your architecture more flexible, I would suggest to introduce your custom annotation that would define which conditions are to be used for a particular page field and then improve AjaxConditionalLocatorFactory that would be using that field’s annotation instead of taking the condition as a parameter.

We also provide some timeout (this parameter is defined in AjaxElementLocator which we extend by our custom element locator class) that would be used to stop looking for the element at some point and consider it unavailable eventually.

Final step: implement a test

Now we have everything prepared for a test. We have our custom ElementLocatorFactory that produces custom ElementLocator that extends AjaxElementLocator having isElementUsable(WebElement element) method overridden with the logic that is defined in our custom conditions. Let’s now look at how the test class that would use all we have prepared could look like:

package click.webelement.pagefactory.conditions;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import java.util.regex.Pattern;

public class PageFactoryConditionsTest {

    WebDriver driver;

    @BeforeAll
    public static void globalSetup() {
        System.setProperty("webdriver.gecko.driver", "/home/alexey/Desktop/Dev/webdrivers/geckodriver");
    }

    @BeforeEach
    public void setUp() {
        driver = new FirefoxDriver();
    }

    @Test
    public void testCapitalizedTest() {
        driver.get("https://webelement.click/en/welcome");
        MenuPage menuPage = new MenuPage(
            driver,
            MyConditions.elementTextCorrespondsToAPattern(Pattern.compile("^[A-Z]{1}.*"))
        );
        menuPage.switchToAboutMe();
        menuPage.switchToMain();
    }

    @AfterEach
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

}

What is this test doing? Basically it tests if a menu item starts from capitalized latin letter. We can assume that we have a web page that is somehow "prettified" by JavaScript (like it happens on my site with code snippet stylization) after all elements have been loaded. Thus we’re considering the menu item is ready as soon as it is properly styled. This test demonstrates such the case.

Well, now you know the end-to-end flow of how to implement the waiting mechanism for Page Object design pattern with PageFactory harness like it was originally designed by Selenium developers. This does not mean there are no other ways to address such problem. However I believe the described approach is most flexible and self-descriptive.

If you still have the questions please send them to me using this form. I will amend the article according to your feedback.