Use relative locators for elements which are being initialized through PageFactory.initElements() - Java

One of the problems which people encounter when they implement Page Object model using PageFactory class is the problem of making one field the relative children of another fields so that the locator of one element is based relatively assuming we have another element already located. In this article I’m going to show you one of the implementation approaches that can address the problem. I’m not going to explain in details why this works in the way it does. I will rather devote a separate article to the details review of how PageFactory works. Yet we’ll continue.

Classic approach and basic points of architecture

If we would use the classic approach to address "relative" issue of locating elements of User Interface, our code would look like this:

public void test(){
	// ... Some preceding code including instantiating of "WebDriver driver = new ..."
	WebElement baseElement = driver.findElement(By.xpath("//base_node"));
	WebElement childElement = baseElement.findElement(By.xpath(".//child_node"));
	// ... Some following code
}

Such approach is used by a lot of people and allows to find one elements in the context of other elements. Lets dig a bit deeper. If to look at the code above we can see that we can invoke the method findElement not only from driver object but from baseElement object as well despite they implement different interfaces: WebDriver and WebElement correspondingly. The fact is that both they extend interface SearchContext which defines two methods: findElement and findElements which are assumed to take By object as the only parameter.

Why do one needs By objects? Such objects are used by Selenium to carry the information about the look up method across the entities. That is it. They do not define the logic of looking up - they just unifies the format of how such or another element is to bee looked up.

Example description

In our example we’re going to use the are of left-hand menu of https://webelement.click/en/welcome as the base element. Taking that element as parent one we will initialize the list of menu items which are inside that element. Since the example is pretty much far-fetched, we will print out parent element name in our test and also print out the list of menu items, hence we will demonstrate that they are separate elements from "page object" point of view.

Page model and the classic usage of PageFactory

Before we start reviewing the example of relative lookup of elements with the help of PageFactory, lets look at the basic model of our page.

package click.webelement.pagefactory.relative;

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

public class WebElementClickBase {

    @FindBy(xpath = "//ul[@class='side-menu']")
    protected WebElement sideMenu;

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

    public void printBaseTagName(){
        System.out.println(sideMenu.getTagName());
    }

}

What would happen if we create an instance of our page? Code of PageFactory will see that the page we have passed is this object (itself). It will scan fields of our class, point out the fields of the following types: WebElement and List<WebElement>, check if they are annotated with one of the annotations which is known to Selenium framework. If no annotations were found for the pointed fields then Selenium will build a standard locator that will use the field name as the name attribute or id attribute.

After Selenium has formed such the locator it will create a proxy-object which will intercept all the invocations (for the methods defined in WebElement, WrapsElement and Locatable interfaces) of the page fields and dispatches it to the "real" object representing the UI element (if it does not exist it will find and serialize it - so called lazy initialization).

Lets look at more representative example. Assume we have WebElement myButton; field. We initialized the page with PageFactory class. What will happen when we invoke click() method against myButton field?

As the initialization has completed, myButton field is assigned with the instance of Proxy class (proxy object). That object has click() method since we configured our proxy-object to comply WebElement interface. Also our object has so called InvocationHandler assigned (all is a part of standard java.lang.reflect package). That handler defines the actions to be taken when invocation of your field method happens.

In our case a handler does the following: using a lookup method defined in By built for your field, it attempts to look up the required element in the current search context. If that element has been already found it does not search for it again (however if we use special annotation that disables cached lookup it searches for the element again). Having received the real element it routes method invocation to that "real" object.

The key point in out class is the field protected WebElement sideMenu;. We also can see the constructor in our class. Actually PageFactory can initialize your fields even if you do not have any constructor specified. However sometimes it is useful to store a reference to a WebDriver in your page class. Thins might be useful when you describe business logic of a page that requires direct access to a WebDriver instance (for example the logic that requires arbitrary JavaScript execution). The best way to pass such reference to your page is to pass it through a constructor. In our example we’re not going to store such reference however we’re still going to use the constructor in order to make a page capable to initialize itself on instantiation phase.

In addition we can see the method printBaseTagName(). I added it in order to demonstrate that we indeed interact with different elements on the page: separately with a parent item and separately with child item. There would be unlikely any value in such logic in the real life.

As I have already mentioned, the page model shown above does not represent "minimal" model that PageFactory can work with. This is how the minimal page class would look if we would remove all "excess" elements.

package click.webelement.pagefactory.relative;

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

public class WebElementClickBase {

    @FindBy(xpath = "//ul[@class='side-menu']")
    protected WebElement sideMenu;

}

Looks very simple, doesn’t it? We removed excess methods, including constructor. The latter will require initialize page fields from outside. Note that we’re going to talk about it in more details in my further article.

Overridden PageFactory.initElements(..) methods and couple of words about ElementLocators

Before we come closer to our example lets briefly look at how PageFactory class offers us to initialize the page field. There are four ways:

public class PageFactory {

  // ...

  public static <T> T initElements(WebDriver driver, Class<T> pageClassToProxy) {
    // ...
  }

  public static void initElements(WebDriver driver, Object page) {
    // ...
  }

  public static void initElements(ElementLocatorFactory factory, Object page) {
    // ...
  }

  public static void initElements(FieldDecorator decorator, Object page) {
    // ...
  }

  // ...

}

Two of them we drop off straight away since they suggest us to use WebDriver as a search context. We are going to take a closer look at the method initElements(ElementLocatorFactory factory, Object page)

Every next method is used within each previous getting deeper and deeper in the framework logic that we’re going to review in the next article.

If to explain briefly what ElementLocator is, then it can be said that it is an object that for each particular field of your page class defines how the element that will be assigned to that field will be looked up. In turn ElementLocatorFactory is a component that knows how to create ElementLocator for the specified field.

There is default implementation (actually several ones) for the mentioned interfaces. They are: DefaultElementLocatorFactory and DefaultElementLocator that is produced by a factory. You can create an instance of DefaultElementLocatorFactory by passing to its constructor whatever implements SearchContext interface. Hence it could be a WebElement for example. What we do need.. Don’t we?

Create relatively looked up fields using PageFactory.initElements()

So, we are about to apply PageFactory.initElements(ElementLocatorFactory factory, Object page) in order to initialize dependent (relative) web elements. We need to recall the following:

  1. Method initializes the fields of already existing object. I we apply the method to the object where the field have already been initialized, the fields will be re-created using new rules (for example using new search context). This is why the pages holding parent and child elements must not be in relationship.

  2. If we use xpath lookup method we should start our xpath query with . symbol. Otherwise the "relativeness" will be ignored.

Considering said above, we prepare one new page model:

package click.webelement.pagefactory.relative;

import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;

import java.util.List;

public class MenuPage {

    @FindBy(xpath = "./li")
    List<WebElement> items;

    public MenuPage(SearchContext searchContext){
        PageFactory.initElements(new DefaultElementLocatorFactory(searchContext), this);
    }

    public void printDependent(){
        items.stream().map(w -> w.getText()).forEach(System.out::println);
    }

}

As we can see, it is independent class, that contains the field of type List<WebElement>. In the constructor we expect to receive an object that implements SearchContext interface (for example WebDriver or WebElement). We wrap what we receive within DefaultElementLocatorFactory that is part of Selenium package and specify the object that we would like to handle by our new initialization mechanism (this).

We also add printDependent() method that demonstrates how our approach works. There are Java streams used to simplify output logic. Method map() converts a list of WebElement to a list of String each of which represent particular element’s text. The for each of resulting texts we apply System.out::println function so that the text is printed to a console.

We also add a method to our first WebElementClickBase class that will return an instance of a page of our new class.

public MenuPage getMenuPage(){
	return new MenuPage(sideMenu);
}

Calling everything from a test

To simplify test code execution I use JUnit test framework (at least in this example), hence it is worth making familiar with the basic conceptions of either JUnit or TestNG. In a nutshell such frameworks allow to mark up your code in convenient manner so that the executor is aware of which part of code is a test, which part is precondition set up and which one is responsible to clean up the garbage after test run. Below you can find the code that demonstrates how all our model works.

package click.webelement.pagefactory.relative;

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;

public class TestRelative {

    WebDriver driver;

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

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

    @Test
    public void testPageFactory() {
        driver.get("https://webelement.click/en/welcome");
        WebElementClickBase menu = new WebElementClickBase(driver);
        menu.printBaseTagName();
        menu.getMenuPage().printDependent();
    }

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

    }

}

As the result, we will see the line ul and the list of menu items we have under that tag.

P.S. - DefaultElementLocatorFactory produces the locators for the elements which we expect to be available right after the page has been loaded. For dynamic elements (for example the content that is controlled by AJAX requests), Selenium developers prepared another special instrument. To build locators for such elements in PageFactory one should use AjaxElementLocatorFactory instead of DefaultElementLocatorFactory. Our constructor in such case would look like:

public MenuPage(SearchContext searchContext){
	PageFactory.initElements(new AjaxElementLocatorFactory(searchContext, 10), this);
}

where in constructor AjaxElementLocatorFactory after searchContext we specify number of seconds to be a lookup timeout for the element.

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