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 :
- A customised org.openqa.selenium.support.pagefactory.ElementLocator
- A customised org.openqa.selenium.support.pagefactory.ElementLocatorFactory
- A customised approach to deciphering the annotations and reading out the values from the annotations.
- 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&lt;JsonElement&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.
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 ?
I didn’t go through the exercise because my code wasn’t optimized.
Or rather use data-structure to store the file content and lookup the locator in the data-structure that was used instead of reading the file every time?
Do you provide training on selenium?
Nope. Not atleast for now Jubayeer
succeded
greate job! I was wondered how to make it and you saved my time!thank you
Glad it helped.
Hi, Am new to building framework, can you please tell me “organized.chaos.annotations.SearchWith” what its jar that i have to download
got interface SearchWith
Cool.
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
How would you indicate an xpath that is dynamic and determined at runtime?
I don’t think the xpath should be part of the PageFactory locators then…
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.?
Hard to say without looking at the code.
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”
}
]
You might want to set debug breakpoints to see what is going wrong. If the code is basically able to identify 3 elements then there’s no reason why it wont work for the rest. To get a better understanding of how PageFactory works you can refer to this blog post of mine : https://rationaleemotions.wordpress.com/2016/09/05/understanding-pagefactory/
On a side note, after I created this blog post, I built some improvizations around the concept of PageObject and created an Oen source library called SimpleSe, which does away with a lot of these things. You could refer to this blog post (https://rationaleemotions.wordpress.com/2017/01/06/simple-se-page-objects-my-first-oss-contribution/) to learn more about that as well.
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..!!
SimpleSe if you notice does work only with externalised locator files. The samples in the readme file showcase that. Please let me know if it’s missing something.
PageObject homePage = new PageObject(driver, “src/test/resources/HomePage.json”); this line essentially causes SimpleSe to read locators from an external file system only.
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..!!
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?
“Iterator<JsonElement>” what is this line
That’s a formatting goof up. It should be read as Iterator
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);
Hard to say without looking at the code.
Great work
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();
}
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();
}
I would urge you to post this along with the required classes on the Selenium users Google forum.
import organized.chaos.annotations.SearchWith; getting error on this import
Not able to add Maven Dependency.
“List findElements” method in FileBasedElementLocator is not getting triggered on trying to store the result of an xpath in a List.
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
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.
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.
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.,