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 ofWebElement
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 ofWebElement
orList<WebElement>
. It usesElementLocatorFactory
that producesElementLocator
object for each given field. Then Selenium assigns to each of those field its associated proxy object that overtakes the calls to element’sfindElement(..)
andfindElements(..)
methods and dispatches them to corresponding implementations ofElementLocator
object.You can extend
ElementLocatorFactory
andElementLocator
classes to meet your needs and then use them inPageFactory.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:
-
Since we extend default locator we need to take care of data required to construct that part of base class. That data is
SearchContext
andField
which we’re just going to take from our class constructor parameters. -
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. -
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. -
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). -
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.