Simple way of how to add human-readable name to WebElement using annotations in Selenium Page Object for logging purpose in Java

Often it is really convenient to have a human-readable name assigned to your page object field. If you do not use any Selenium extension there are several ways how you can achieve that. They can be easy but not really functional or heavy functioned but required a lot of things to code.

Today we’re going to look at the former case. We’re going to implement annotation that will be used to assign an arbitrary string as a name to WebElement field of a page object that is instantiated with PageFactory utility.

The downside of such approach is that the name won’t be a part of the element so that you won’t be able to obtain the name with getName() method or something like this. However it can be found valuable for those who wish to log the elements names when accessing them.

You’ll still be able to access the "name" value of the field using reflection. However that won’t be looking really nice because you’ll be working with a field (a reference to an object), not with the object itself. At the same time this might work more or less well when you have some utility methods which work with WebElement but which are not the part of WebElement like logging methods.

I’ll tell about other implementations allowing to make the name or any other property a part of WebElement with annotations in my future articles. Those implementations will be more tricky but will allow to access those new element properties from anywhere of your code in good architectural style.

Let’s now look at everything in more details.

Example description: logging the element name when locating it and when the location fails

We’re going to introduce a test that would just click a button on the page below (so if you want to reproduce test locally, copy the content shown below to the file button.html):

<html>
  <head/>
  <body>
    <input type="button" style="margin: 50px;">
  </body>
</html>

The test will be operating with page object that will be defining the field associated with the button on the page.

package click.webelement.pagefactory.customelement.easy;

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

public class PageObjectWithName {

    @Name("My test name")
    @FindBy(xpath = "//input[@type='button']")
    WebElement button;

    public PageObjectWithName(SearchContext context) {
        // TODO: implement page initialization
    }

    public void clickButton(){
        button.click();
    }

}

where @Name is the annotation we’re going to develop also.

Adding annotation

Since we’re going to annotate our element we need to implement the annotation that we’ll be using for that purpose. It is very simple and it should be looking like this:

package click.webelement.pagefactory.customelement.easy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Name {
    String value();
}

Now, having the field annotated with this annotation we’ll be able to extract the name value in runtime.

Integrating the annotation into Selenium Page Factory harness

We’re going to implement logging of element’s name for the cases of element lookup attempts and those attempts failures, hence we’ll need to prepare ElementLocator entity which will be responsible for element lookup logic. We won’t be developing element locator from scratch, but extend DefaultElementLocator. It is used for providing basic lookup logic for static page elements.

Couple of words about how everything works when you initialize your page with PageFactory. When you do so, Selenium iterates over the fields annotated with @FindBy and of the types of WebElement or List<WebElement>. It uses ElementLocatorFactory that produces ElementLocator object for each given field. Then Selenium assigns to each of those field its associated proxy object that overtakes the calls to element’s findElement(..) and findElements(..) methods and dispatches them to corresponding implementations of ElementLocator object.

You can extend ElementLocatorFactory and ElementLocator classes to meet your needs and then use them in PageFactory.init(..). This is what we’ll do soon.

Extending DefaultElementLocator

Let’s look at our DefaultElementLocator (which in turn is the implementation of ElementLocator interface) extension.

package click.webelement.pagefactory.customelement.easy;

import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.pagefactory.DefaultElementLocator;
import java.lang.reflect.Field;
import java.util.UUID;

public class WECElementLocator extends DefaultElementLocator {

    final String UNNAMED = UUID.randomUUID().toString();
    final String elementName;

    public WECElementLocator(SearchContext searchContext, Field field) {
        super(searchContext, field);
        Name elementNameAnnotated = field.getAnnotation(Name.class);
        if (elementNameAnnotated != null){
            elementName = elementNameAnnotated.value();
        }else{
            elementName = UNNAMED;
        }
    }

    private void log(String message){
        if(!UNNAMED.equals(elementName)){
            System.out.println(message + " (for [" + elementName + "])");
        }
    }

    @Override
    public WebElement findElement() {
        try{
            log("Attempt to lookup element..");
            WebElement result = super.findElement();
            log("Element successfully located.");
            return result;
        }catch (Throwable e){
            log("Problem in locating element..");
            throw e;
        }
    }
}

Here are few of remarkable points in the implementation:

  1. Since we extend default locator we need to take care of data required to construct that part of base class. That data is SearchContext and Field which we’re just going to take from our class constructor parameters.

  2. Like in default locator we take Field object as constructor parameter. As I mentioned before the field has the information about the annotations which were used to annotate that field. Hence here we can extract the annotation value associated with the given field.

  3. We can omit annotating the field. In that case the elementName field has to have some value to avoid possible NPEs. Thus we build up default name of a unique sequence of chars in order to distinguish the field that does not have associated name.

  4. We implement log method which takes care of deciding if the log message makes the sense (if the field does not have the name set, then it does not).

  5. We override findElement method that logs the lookup attempt with the element name, calls the base implementation and catches (and then re-throws) exceptions to log the name of problematic elements.

Implement ElementLocatorFactory

Implementation of ElementLocatorFactory is really straightforward. Since we run all the custom logic within ElementLocator here we just need to create and return our WECElementLocator. Here is the code:

package click.webelement.pagefactory.customelement.easy;

import org.openqa.selenium.SearchContext;
import org.openqa.selenium.support.pagefactory.ElementLocator;
import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;
import java.lang.reflect.Field;

public class WECElementLocatorFactory implements ElementLocatorFactory {

    SearchContext context;

    public WECElementLocatorFactory(SearchContext context){
        this.context = context;
    }

    @Override
    public ElementLocator createLocator(Field field) {
        return new WECElementLocator(context, field);
    }
}

Now we have everything implemented. Lets now fix our page object from the beginning of the post and look at the final test.

Page object with our custom initialization logic

The class below is different from the one from example description with only its constructor part. You can see that we use PageFactory.initElements(..) method that takes ElementLocatorFactory as the first argument. The implementation of the factory and the locator is what have been discussed earlier in the post.

package click.webelement.pagefactory.customelement.easy;

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

public class PageObjectWithName {

    @Name("My test name")
    @FindBy(xpath = "//input[@type='button']")
    WebElement button;

    public PageObjectWithName(SearchContext context) {
        PageFactory.initElements(new WECElementLocatorFactory(context), this);
    }

    public void clickButton(){
        button.click();
    }

}

Test implementation

The test is written with the help of JUnit framework. Even if you are not familiar with JUnit you will easily convert the code below to any other test execution framework or to pure "main" method.

package click.webelement.pagefactory.customelement.easy;

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 ElementNameTest {

    WebDriver driver;

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

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

    @Test
    public void testElementWithName() {
        driver.get("file:///path_to_demo_page/button.html");
        new PageObjectWithName(driver).clickButton();
    }

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

}

If you were following the examples carefully, the test will pass successfully. This will demonstrate the logging of lookup attempt and its successful result. You then can spoil the locator of the button in page object and make sure that the negative cases are also handled in the proper way, i.e. the element name is logged. If you still have the questions please send them to me using this form. I will amend the article according to your feedback.