4

I can only use hardcoded values in page object @FindBy annotations But I would like to resolve locators dynamically.

public class LoginPage extends BasePage {

    // hardocded value works ok
    @FindBy(name = "login field")
    WebElement usernameFld;

    // does not compile
    // this is a kind of what I would like to have  
    @FindBy( getLocatorFromExternalSource() ) 
    WebElement passwordFld;

}

I have seen a few posts mentioning that such things can be solved by implementing custom annotations/decorators/factories but did not find examples yet.

QUESTION: Can someone please give an example of how to implement custom ElementLocatorFactory so locators could be resolved dynamically?

I know I can just use plain old-style calls like:

driver.findElement( getLocatorFromExternalSource("passwordFld") ).click()

but I would like to use

passwordFld.click() 

instead.

ludenus
  • 1,161
  • 17
  • 30
  • Are your locators changing a lot? Why do you want to read them from an external file instead of just putting them in your page objects where they are easily found and organized? – JeffC Oct 07 '16 at 18:20
  • 1. I am trying to write platform agnostic test for ios and android devices and do not want to duplicate page objects 2. UI localization might be the next point where externalized locators are useful 3. Some locators can change in runtime (elements sorted in another order etc) – ludenus Oct 07 '16 at 20:04

4 Answers4

0

Annotations are MetaData, so they need to be available in start at runtime during class loading. what you seek does not has straight forward answer, but hacks are there using Reflections, IMHO i would avoid it and if you need to externalize the locators then I suggest you to implement Object repository where you would read your locators at runtime on the fly from some external source.

Mrunal Gosar
  • 4,595
  • 13
  • 48
  • 71
0

I'm personally not a big fan of PageFactory stuff but if you are already using it, I would do something like the below.

public class LoginPage extends BasePage
{
    WebDriver driver;
    WebElement usernameFld;
    @FindBy(name = "login field")
    WebElement usernameIOs;

    @FindBy(name = "something else")
    WebElement usernameAndroid;

    public LoginPage(WebDriver webDriver, Sites site)
    {
        this.driver = webDriver;
        switch (site)
        {
            case IOS:
                usernameFld = usernameIOs;
                break;
            case ANDROID:
                usernameFld = usernameAndroid;
                break;
        }
    }

    public void setUsername(String username)
    {
        usernameFld.sendKeys(username);
    }
}

elsewhere you would define

public enum Sites
{
    IOS, ANDROID
}

I prefer to declare locators and then scrape the elements as I need them. I find that this is a lot more performant and you get fewer stale element problems, etc.

public class LoginPage extends BasePage
{
    WebDriver driver;
    By usernameLocator;
    By usernameIOsLocator = By.name("login field");
    By usernameAndroidLocator = By.name("something else");

    public LoginPage(WebDriver webDriver, Sites site)
    {
        this.driver = webDriver;
        switch (site)
        {
            case IOS:
                usernameLocator = usernameIOsLocator;
                break;
            case ANDROID:
                usernameLocator = usernameAndroidLocator;
                break;
        }
    }

    public void setUsername(String username)
    {
        driver.findElement(usernameLocator).sendKeys(username);
    }
}
JeffC
  • 22,180
  • 5
  • 32
  • 55
  • Thanks for the answer. I think I got your idea. Advantage is that it makes everything clear by choosing locator explicitly. Disadvantage is that you would have to modify all your switch cases for all page objects once you add a new platform say IOS9, IOS10, iPad. I would prefer to add/switch page config and keep the code intact. – ludenus Oct 09 '16 at 20:00
  • It's really not that much different than what you are doing. When you add a new config, you will have to add a new locator for each element on each page in your config file. I would have to do the same here. The difference is, my way keeps the page object methodology clean in that everything to do with the page is in the page object. Your config file is going to get unwieldy fast because it contains all locators for all elements times the number of configs you support. – JeffC Oct 09 '16 at 20:29
  • I agree - the amount of locators is the same. But I prefer to keep test data apart from code. – ludenus Oct 09 '16 at 21:13
0

I've found some clue here: https://stackoverflow.com/a/3987430/1073584 and finally managed to get what I wanted by implementing 3 classes: CustomPageFactory, CustomElementLocator, CustomAnnotations.

Now I am able to externalize my locators to typesafe config and use the following code to init page objects

//======= LoginPage

public class LoginPage extends AbstractPage {
    Config config;

    @FindBy(using = "CONFIG") // take locator from config
    WebElement passwordFld;

    // .. other fields skipped


    public LoginPage(WebDriver webDriver, Config config) throws IOException {
        super(webDriver);
        this.config = config;
        PageFactory.initElements(new CustomPageFactory(webDriver, config), this);
    }

}

//===== login.page.typesafe.config:

passwordFld = {
   name = "password field"
}

//============= CustomPageFactory

package com.company.pages.support;

import com.typesafe.config.Config;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.pagefactory.ElementLocator;
import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;

import java.lang.reflect.Field;

public class CustomPageFactory implements ElementLocatorFactory {
    private Config config;
    private WebDriver driver;

    public CustomPageFactory(WebDriver driver, Config config) {
        this.driver = driver;
        this.config = config;
    }

    public ElementLocator createLocator(Field field) {
        return new CustomElementLocator(driver, field, config);
    }
}

//================= CustomElementLocator

package com.company.pages.support;

import com.typesafe.config.Config;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.support.pagefactory.DefaultElementLocator;

import java.lang.reflect.Field;

public class CustomElementLocator extends DefaultElementLocator {

    private Config config;

    public CustomElementLocator(SearchContext searchContext, Field field, Config config) {
        super(searchContext, new CustomAnnotations(field, config));
        this.config = config;
    }


}

//====== CustomAnnotations

package com.company.pages.support;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigObject;
import org.openqa.selenium.By;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.pagefactory.Annotations;

import java.lang.reflect.Field;

public class CustomAnnotations extends Annotations {

    Config config;

    public CustomAnnotations(Field field, Config config) {
        super(field);
        this.config = config;
    }

    @Override
    protected By buildByFromShortFindBy(FindBy findBy) {

        if (findBy.using().equals("CONFIG")) {

            if (null != config) {

                ConfigObject fieldLocators = config.getObject(getField().getName());

                if (fieldLocators.keySet().contains("className"))
                    return By.className(fieldLocators.get("className").unwrapped().toString());

                if (fieldLocators.keySet().contains("css"))
                    return By.cssSelector(fieldLocators.get("css").unwrapped().toString());

                if (fieldLocators.keySet().contains("id"))
                    return By.id(fieldLocators.get("id").unwrapped().toString());

                if (fieldLocators.keySet().contains("linkText"))
                    return By.linkText(fieldLocators.get("linkText").unwrapped().toString());

                if (fieldLocators.keySet().contains("name"))
                    return By.name(fieldLocators.get("name").unwrapped().toString());

                if (fieldLocators.keySet().contains("partialLinkText"))
                    return By.partialLinkText(fieldLocators.get("partialLinkText").unwrapped().toString());

                if (fieldLocators.keySet().contains("tagName"))
                    return By.tagName(fieldLocators.get("tagName").unwrapped().toString());

                if (fieldLocators.keySet().contains("xpath"))
                    return By.xpath(fieldLocators.get("xpath").unwrapped().toString());
            }

        }

        return super.buildByFromShortFindBy(findBy);
    }

}
ludenus
  • 1,161
  • 17
  • 30
  • 1
    thanks for your help, but this solution is applicable for selenium v2.53. I have upgraded by selenium-java to 3.5.3 and couldnt see the method buildByFromShortFindBy belong to Annotations class anymore, it got moved to AbstractFindByBuilder and hence wondering how to wire & override this function to implement my CustomPageFactory? – Bhuvanesh Mani Sep 18 '17 at 19:45
  • @BhuvaneshMani you are right, this code no longer works with newer versions of selenium – ludenus Dec 04 '17 at 21:56
0

I think you are wondering to perform the operations on webElements by their name

U need to put some code in your constructor

public AnyConstructorName(AndroidDriver<AndroidElement> driver) {
        this.driver =driver;
        PageFactory.initElements(driver, this);
}

above code will work if you are accepting the driver from some other class If not the case remove this.driver =driver from your constructor

Initialize the web elements like

@FindBy(xpath ="//android.widget.TextView[@text='Payments']")

WebElement pay;

u can perform pay.click();

or any operation you want

But can perform these action within your page class only

abishek kachroo
  • 35
  • 1
  • 11
  • `@FindBy(xpath ="//android.widget.TextView[@text='Payments']")` I was looking for a way NOT to hardcode locator within @FindBy but to read it from external source instead – ludenus Dec 04 '17 at 21:55