//
you're reading...
WebDriver

PageFactory, Page Objects and locators from an external file

The reason for this post is basically aimed at answering a forum question that came up.

The question read :

Can we pass the value for @FindBy from a xml file…? Means for our project we maintain a xml file which contain  all parameter( config, test data, object identification value , etc…)
So, what i want to know can we read that identification type & it’s value from xml file & pass to @FindBy annotation…?
currently we pass values hard coded like below.
for example:
@FindBy(name=”uid”)
WebElement userName;

I have never bothered to explore the PageObjects pattern that Selenium gives and which is widely used by a lot of the Selenium users, mainly because I have never been involved heavily into UI automation test script creation.

But this question sounded a bit interesting. So I decided to go after it and try to solve it in my own way.
Without any further adieu, here’s the recipe for this solution.
The ingredients that are required :

  1. A customised org.openqa.selenium.support.pagefactory.ElementLocator
  2. A customised org.openqa.selenium.support.pagefactory.ElementLocatorFactory
  3. A customised approach to deciphering the annotations and reading out the values from the annotations.
  4. A custom annotation that captures some meta data which can be used to read the locator from a JSON file for a given web element.

For this example, I am going to work with a JSON file that acts as the place where all locators for all the pages are going to be stored into.

Here’s how the JSON file looks like :


[
  {
    "pageName": "HomePage",
    "name": "abTesting",
    "locateUsing": "xpath",
    "locator": "//a[contains(@href,'abtest')]"
  },
  {
    "pageName": "HomePage",
    "name": "checkBox",
    "locateUsing": "xpath",
    "locator": "//a[contains(@href,'checkboxes')]"
  },
  {
    "pageName": "CheckboxPage",
    "name": "checkBox1",
    "locateUsing": "xpath",
    "locator": "//input[@type='checkbox'][1]"
  },
  {
    "pageName": "CheckboxPage",
    "name": "checkBox2",
    "locateUsing": "xpath",
    "locator": "//input[@type='checkbox'][2]"
  }
]

The structure of the above JSON file should be self explanatory. It contains a JSONArray. Each element is a JSONObject which has 4 attributes :

  • pageName : Represents the name of the page to which the element belongs to.
  • name : A symbolic reference to the element from the PageObject java class.
  • locateUsing : The actual location strategy to be used. (In our example we are going to be just supporting xPath )
  • locator : The actual locator that is to be used.

Here’s how our custom annotation looks like :

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 SearchWith {
    String inPage() default "";

    String locatorsFile() default "";

    String name() default "";
}

This interface is kind of like the @FindBy annotation. It basically captures the location of the JSON file in which all our locators are situated, the name of the page (this will help us find our required JSONObject in our JSON file) and the name of the html element (imagine this to be the key in our JSONObject ).

Now here’s how our custom implementation of “org.openqa.selenium.support.pagefactory.ElementLocator” looks like :

import org.openqa.selenium.By;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.pagefactory.AbstractAnnotations;
import org.openqa.selenium.support.pagefactory.ElementLocator;

import java.util.List;

public class FileBasedElementLocator implements ElementLocator {

    private final SearchContext searchContext;
    private final boolean shouldCache;
    private final By by;
    private WebElement cachedElement;
    private List<WebElement> cachedElementList;


    public FileBasedElementLocator(SearchContext searchContext, AbstractAnnotations annotations) {
        this.searchContext = searchContext;
        this.shouldCache = annotations.isLookupCached();
        this.by = annotations.buildBy();
    }

    @Override
    public WebElement findElement() {
        if (cachedElement != null && shouldCache) {
            return cachedElement;
        }

        WebElement element = searchContext.findElement(by);
        if (shouldCache) {
            cachedElement = element;
        }

        return element;

    }

    @Override
    public List<WebElement> findElements() {
        if (cachedElementList != null && shouldCache) {
            return cachedElementList;
        }

        List<WebElement> elements = searchContext.findElements(by);
        if (shouldCache) {
            cachedElementList = elements;
        }

        return elements;

    }
}

Here’s how our custom implementation of org.openqa.selenium.support.pagefactory.ElementLocatorFactory looks like :

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 FileBasedElementLocatorFactory implements ElementLocatorFactory {
    private final SearchContext searchContext;

    public FileBasedElementLocatorFactory(SearchContext searchContext) {
        this.searchContext = searchContext;
    }

    @Override
    public ElementLocator createLocator(Field field) {
        return new FileBasedElementLocator(searchContext, new CustomAnnotations(field));
    }
}

Lastly here’s how our custom implementation of org.openqa.selenium.support.pagefactory.AbstractAnnotations looks like :

import com.google.common.base.Preconditions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.openqa.selenium.By;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.pagefactory.AbstractAnnotations;
import organized.chaos.annotations.SearchWith;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.lang.reflect.Field;
import java.util.Iterator;

class CustomAnnotations extends AbstractAnnotations {
    private final Field field;

    CustomAnnotations(Field field) {
        this.field = field;
    }

    @Override
    public By buildBy() {
        SearchWith search = field.getAnnotation(SearchWith.class);
        Preconditions.checkArgument(search != null, "Failed to locate the annotation @SearchWith");
        String elementName = search.name();
        String pageName = search.inPage();
        String locatorsFile = search.locatorsFile();
        Preconditions
            .checkArgument(isNotNullAndEmpty(elementName), "Element name is not found.");
        Preconditions.checkArgument(isNotNullAndEmpty(pageName), "Page name is missing.");
        Preconditions.checkArgument(isNotNullAndEmpty(locatorsFile), "Locators File name not provided");
        File file = new File(locatorsFile);
        Preconditions.checkArgument(file.exists(), "Unable to locate " + locatorsFile);
        try {
            JsonArray array = new JsonParser().parse(new FileReader(file)).getAsJsonArray();
            Iterator&amp;lt;JsonElement&amp;gt; iterator = array.iterator();
            JsonObject foundObject = null;
            while (iterator.hasNext()) {
                JsonObject object = iterator.next().getAsJsonObject();
                if (pageName.equalsIgnoreCase(object.get("pageName").getAsString()) &&
                    elementName.equalsIgnoreCase(object.get("name").getAsString())) {
                    foundObject = object;
                    break;
                }
            }
            Preconditions.checkState(foundObject != null, "No entry found for the page [" + pageName + "] in the "
                + "locators file [" + locatorsFile + "]");
            String locateUsing = foundObject.get("locateUsing").getAsString();
            if (! ("xpath".equalsIgnoreCase(locateUsing))) {
                throw new UnsupportedOperationException("Currently " + locateUsing + " is NOT supported. Only xPaths "
                    + "are supported");
            }

            String locator = foundObject.get("locator").getAsString();
            Preconditions.checkArgument(isNotNullAndEmpty(locator), "Locator cannot be null (or) empty.");
            return new By.ByXPath(locator);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

    }

    @Override
    public boolean isLookupCached() {
        return (field.getAnnotation(CacheLookup.class) != null);
    }

    private boolean isNotNullAndEmpty(String arg) {
        return ((arg != null) && (! arg.trim().isEmpty()));
    }
}

Now that we have all the basic infrastructure in place, lets quickly create a couple of PageObject classes that uses our new custom annotation:

import org.openqa.selenium.WebElement;
import organized.chaos.annotations.SearchWith;

public class HomePage {
    public static final String PAGE = "HomePage";
    @SearchWith (inPage = HomePage.PAGE, locatorsFile = "src/main/resources/locators.json", name = "abTesting")
    private WebElement abTestingLink = null;

    @SearchWith (inPage = HomePage.PAGE, locatorsFile = "src/main/resources/locators.json", name = "checkBox")
    private WebElement checkBoxLink = null;

    public HomePage() {
    }

    public CheckBoxPage navigateToCheckBoxPage() {
        checkBoxLink.click();
        return new CheckBoxPage();
    }
}

Here’s another PageObject class :

import org.openqa.selenium.WebElement;
import organized.chaos.annotations.SearchWith;

public class CheckBoxPage {
    private static final String PAGE = "CheckBoxPage";

    @SearchWith (inPage = CheckBoxPage.PAGE, locatorsFile = "src/main/resources/locators.json", name = "checkBox1")
    private WebElement checkBoxOne;

    @SearchWith (inPage = CheckBoxPage.PAGE, locatorsFile = "src/main/resources/locators.json", name = "checkBox2")
    private WebElement checkBoxTwo;

    public void unCheckCheckBoxTwo() {
        if (checkBoxTwo.isSelected()) {
            checkBoxTwo.click();
        }
    }

    public boolean isCheckBoxTwoUnchecked() {
        return (! checkBoxTwo.isSelected());
    }
}

And here’s how a test class which makes use of all this can look like :

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import organized.chaos.pages.CheckBoxPage;
import organized.chaos.pages.HomePage;
import organized.chaos.support.FileBasedElementLocatorFactory;

public class AlteredPageFactoryDemo {

    private RemoteWebDriver driver;

    @BeforeClass
    public void setup() {
        driver = new ChromeDriver();
    }

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

    @Test
    public void testMethod() {
        driver.get("https://the-internet.herokuapp.com/");
        HomePage homePage = new HomePage();
        ElementLocatorFactory factory = new FileBasedElementLocatorFactory(driver);
        PageFactory.initElements(factory, homePage);
        CheckBoxPage checkboxPage = homePage.navigateToCheckBoxPage();
        PageFactory.initElements(factory, checkboxPage);
        checkboxPage.unCheckCheckBoxTwo();
        Assert.assertTrue(checkboxPage.isCheckBoxTwoUnchecked());
    }
}

Now hope this gives you an idea of how to go about externalising the locators from the Page Objects dependent annotations and housing the actual locators in an external file and still use all of this with PageFactory.

Discussion

37 thoughts on “PageFactory, Page Objects and locators from an external file

  1. Thanks for your idea..This is very interesting.. By the way how is the performance of the lookups for this implementation. Everytime a page object element is needed, it has to go through the complete JSON file to find them ?

    Posted by Deepan Chakkaravarthy | August 19, 2016, 4:43 am
  2. Do you provide training on selenium?

    Posted by Jubayeer | October 14, 2016, 2:39 am
  3. succeded
    greate job! I was wondered how to make it and you saved my time!thank you

    Posted by Sergii | December 13, 2016, 9:38 pm
  4. Hi, Am new to building framework, can you please tell me “organized.chaos.annotations.SearchWith” what its jar that i have to download

    Posted by Grindale | February 8, 2017, 2:30 pm
  5. got interface SearchWith

    Posted by Grindale | February 8, 2017, 4:30 pm
  6. hi, that’s a great work , but when I tried to Implement I got the following Null point exception error,

    << ERROR!
    java.lang.NullPointerException: null
    at com.OBP.Customisations.FileBasedElementLocator.findElement(FileBasedElementLocator.java:31)
    at org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler.invoke(LocatingElementHandler.java:38)
    at com.sun.proxy.$Proxy15.getTagName(Unknown Source)

    here is my Page Factory Initialization,
    ElementLocatorFactory factory = new FileBasedElementLocatorFactory(driver);
    LoginPage login = new LoginPage();
    PageFactory.initElements(factory, login);

    What i observed is the Overridden method of interface ElementLocatorFactory, "createlocator " in FileBasedElementLocatorFactory isn't being invoked.

    Can you please help me out in figuring this.
    Regards

    Posted by NVT | March 30, 2017, 11:28 am
  7. How would you indicate an xpath that is dynamic and determined at runtime?

    Posted by Greg | June 1, 2017, 11:04 pm
  8. I am able to succeeded thank you. Interestingly, out of 5 locators in json, only 3 are identifying and other 2 elements are not identifying. I am unsure here what might have went wrong.? If there is a code implementation problem then first 3 shouldn’t consider, right.? Also, I used these unidentified 2 locators in legacy way like, i.e., @FindBy(name=””), then these are working. Any thoughts, what might have went wrong with this approach.?

    Posted by AutomationUser | June 13, 2017, 1:20 am
  9. I pasted your same code what you have provided, to see if the code has any errors or not. I made sure there is no compilation errors, then I modified json data as follows. then ‘rememberMeCheckbox’ and ‘submitButton’ are not identifying where as remaining elements are identifying. Note: for submit button element, I changed locateUsing to xpath as “//*[contains(text(), ‘Log in’)]”, then also it is not working.

    [
    {
    “pageName”: “LoginPage”,
    “name”: “username”,
    “locateUsing”: “name”,
    “locator”: “userName”
    },
    {
    “pageName”: “LoginPage”,
    “name”: “password”,
    “locateUsing”: “name”,
    “locator”: “password”
    },
    {
    “pageName”: “LoginPage”,
    “name”: “rememberMeCheckbox”,
    “locateUsing”: “name”,
    “locator”: “remember-checkbox”
    },
    {
    “pageName”: “LoginPage”,
    “name”: “submitButton”,
    “locateUsing”: “tagName”,
    “locator”: “button”
    },
    {
    “pageName”: “LoginPage”,
    “name”: “loginSectionTitle”,
    “locateUsing”: “css”,
    “locator”: “.add-entity-popover .title”
    }
    ]

    Posted by AutomationUser | June 13, 2017, 3:40 pm
  10. Excellent.! SimpleSe is simply superb work..!! Doesn’t it missing feeding data from external file to test.? Just a thought, if possible please update SimpleSe library to feed test data from external file. Once again, great work..!!

    Posted by AutomationUser | June 13, 2017, 11:42 pm
  11. PageObject homePage = new PageObject(driver, “src/test/resources/HomePage.json”); this line essentially causes SimpleSe to read locators from an external file system only.

    Posted by Confusions Personified | June 13, 2017, 11:55 pm
  12. I agree with you. SimpleSe has way to read externalised locator, i.e., using SimpleSe we are able to feed locator from external file and also supporting localisation. May be I confused by simply saying data, what actually I meant data was, to read expected data (to assert) from external file (may be aka data provider). Just a thought anyway, Cheers..!!

    Posted by AutomationUser | June 14, 2017, 12:26 am
  13. how can I work with contains function in xpath? I want to implement a function which gets an argument (String) and passes the argument to xpath String like: //a[contains(text(),'” + argument + ‘)]”. It means I need a dynamic xpath.
    Could you give me a hand?

    Posted by minh | July 11, 2017, 1:04 pm
  14. “Iterator<JsonElement>” what is this line

    Posted by Firoj Khan | August 29, 2017, 11:14 am
  15. I have created custom annotation for Native application with Appium. I did everything as per the thread. However, I am getting NoSuchElementException for custom factory. It is giving error in below line of ElementLocator class.

    searchContext.findElement(by);

    Posted by Kevin | January 5, 2018, 2:02 am
  16. Great work

    Posted by BintAishi | February 8, 2018, 1:34 am
  17. And could you help me on this plz,,,,,,,,,,,,,,below page works as expected.
    public class JenkinsLoginPage {
    public void loginToJenkins() {
    WebDriver driver;
    System.setProperty(“webdriver.chrome.driver”, “pathtoDriver\\chromedriver.exe”);
    driver = new ChromeDriver();
    driver.manage().window().maximize();
    driver.navigate().to(“jenkinsurl”);
    PageObject loginPage = new PageObject(driver, “pathtojson\\JenkinsLoginPage.json”);
    TextField txtUsername = loginPage.getTextField(“username”);
    TextField txtPassword = loginPage.getTextField(“password”);
    Button btnLogin = loginPage.getButton(“button”);
    txtUsername.type(“myusername”);
    txtPassword.type(“mypassword”);
    btnLogin.click();
    }
    }
    //my test class
    JenkinsLoginPage page=JenkinsLoginPage();
    @Test
    public void testJenkinsLogin() {
    page.loginToJenkins();
    }

    Posted by BintAishi | February 8, 2018, 2:25 am
  18. But not this one,,,,,,,,and i cant understand why. does it have anything related to page initialization?
    public class JenkinsNewLoginPage {
    WebDriver driver;
    PageObject loginPage;
    public void initBrowser() {
    System.setProperty(“webdriver.chrome.driver”, “pathtoDriver\\chromedriver.exe”);
    driver = new ChromeDriver();
    driver.manage().window().maximize();
    driver.navigate().to(“jenkinsurl”);
    loginPage = new PageObject(driver, “pathtojson\\JenkinsLoginPage.json”);
    }
    TextField txtUsername = loginPage.getTextField(“username”);
    TextField txtPassword = loginPage.getTextField(“password”);
    Button btnLogin = loginPage.getButton(“button”);
    public void loginToJenkins() {
    txtUsername.type(“myusername”);
    txtPassword.type(“mypassword”);
    btnLogin.click();
    }
    }
    //test class
    JenkinsNewLoginPage page=new JenkinsNewLoginPage();
    @Test
    public void testJenkinsLogin() {
    page.initBrowser();
    page.loginToJenkins();
    }

    Posted by BintAishi | February 8, 2018, 2:29 am
  19. import organized.chaos.annotations.SearchWith; getting error on this import

    Posted by CHETHAN ANNEM | July 24, 2018, 10:42 pm
  20. “List findElements” method in FileBasedElementLocator is not getting triggered on trying to store the result of an xpath in a List.

    Posted by Tuhin Roy | September 19, 2018, 11:20 pm
  21. I took this code sample as the base and expanded it to address a number of limitations. It now;

    * Deals with all valid locator types (not just xpath)
    * Works for List, not just WebElelement
    * Works with some fields using @FindBy and others using @SearchWith
    * Can read filename of locators file from a system property
    * Resolves locators only once and caches them to avoid re-reading the whole file every time

    I published my code on github here – https://github.com/shchukax/search-with

    Posted by Aleks G | November 29, 2018, 9:48 pm
    • Aleks, am glad to know that you found this post useful. I just thought I would call out a couple of things.
      1. This post and the code within was never meant to be a full fledged implementation which could be consumed as is. It was meant to just show the possibilities of externalising the locators using page factories.
      2. After I created this post, I went on to build a library called SimpleSe (https://github.com/RationaleEmotions/SimpleSe) which does away with page factories itself, lets a user externalise the locators, supports localisation and doesnt need one to create a page object class explicitly just to house the locators and trigger the search logic. You might want to take a look at it as well.

      Posted by Confusions Personified | November 29, 2018, 10:54 pm
      • Thanks! I’ll have a look.
        I certainly appreciate that you never intended for the implementation to be production grade – but it a very good start for me to get the code that’s much closer to production grade. Also, for me, the idea of page objects way more than just to hold locators. Page objects are used to separate application logic from test logic. From my test I could do “loginPage.login(user,pass)” – and not worry about how it’s done.

        Posted by Aleks G | December 3, 2018, 2:50 pm
      • Yes. The Page Objects shouldnt be confined just abstracting out locators, but if you look at SimpleSe, you would realise that it facilitates whatever way you would like to construct/visualise your page objects as. It just provides an easier way to represent your locators, without cluttering you with all the extra boiler plate code in terms decorating all of your fields with the needed annotations etc.,

        Posted by Confusions Personified | December 5, 2018, 3:05 pm

Leave a comment