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 invokeclick()
method againstmyButton
field?As the initialization has completed,
myButton
field is assigned with the instance ofProxy
class (proxy object). That object hasclick()
method since we configured our proxy-object to complyWebElement
interface. Also our object has so calledInvocationHandler
assigned (all is a part of standardjava.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:
-
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.
-
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
. You can read more about waiters and AjaxElementLocatorFactory in Page Object design in this my arcticle. 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.