//
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.

Advertisements

Discussion

11 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: