How to extend Selenium's FindBy annotation - java

I'm trying to use Selenium PageObjects to model a page that lacks a lot of convenient id or class attributes on the tags, so I'm finding that I need to develop more creative ways to identify elements on a page. Among them is a pattern like the following:
<div id="menuButtons">
<a><img src="logo.png" alt="New"></a>
<a><img src="logo2.png" alt="Upload"></a>
</div>
It would be convenient to be able to create a custom findBy search to be able to identify a link by the alt text of its contained image tag, so I could do something like the following:
#FindByCustom(alt = "New")
public WebElement newButton;
The exact format of the above isn't important, but what's important is that it continue to work with PageFactory.initElements.

The author of this article extended the 'FindBy` annotation to support his needs. You can use this to override the 'FindBy' and make your on implementation.
Edited code sample:
private static class CustomFindByAnnotations extends Annotations {
protected By buildByFromLongFindBy(FindBy findBy) {
How how = findBy.how();
String using = findBy.using();
switch (how) {
case CLASS_NAME:
return By.className(using);
case ID:
return By.id(using);
case ID_OR_NAME:
return new ByIdOrName(using);
case LINK_TEXT:
return By.linkText(using);
case NAME:
return By.name(using);
case PARTIAL_LINK_TEXT:
return By.partialLinkText(using);
case TAG_NAME:
return By.tagName(using);
case XPATH:
return By.xpath(using);
case ALT:
return By.cssSelector("[alt='" + using " + ']");
default:
throw new IllegalArgumentException("Cannot determine how to locate element " + field);
}
}
}
Please note I didn't try it myself. Hope it helps.
If you simply want the <a> tag you can use xpath to find the element and go one level up using /..
driver.findElement(By.xpath(".//img[alt='New']/.."));
Or you can put the buttons in list and access them by index
List<WebElement> buttons = driver.findElements(By.id("menuButtons")); //note the spelling of findElements
// butttons.get(0) is the first <a> tag

Related

What is a good Selenium strategy if you don't know if certain elements exist or not?

I am writing a web spider for a site on the public internet - that is, I can't control the site that is the target. For some reason certain elements I am interested in change their name, id and similar between reloads.
Since Selenium, AFAIK, doesn't have a method that returns true/false if an element exists or not (instead it throws exceptions), what is a good strategy to handle this situation?
In pseudo code
if element A exists click A
else if element B exists click B
else if element C exists click C
Currently I have to surround every if with a try/catch.
Does Selenium have something built in for this purpose or should I write my own helper method?
public Boolean elementExists(WebDriver driver, String xpath) {
// this will not throw any exception, if no element is found, the list will be empty
List<WebElement> elements = driver.findElements(By.xpath(xpath));
if (elements.size() > 0) {
return true;
}
else {
return false;
}
}
Any locator can be interpreted with xpath.
For id "foo" the xpath will be "//*[#id = 'foo']"
For name "foo" the xpath will be "//*[#name = 'foo']"
etc

Super class that can override #FindBy

I am porting over an existing project and I have a super class called DetailPage that is the only thing I can change/edit. It's a selenium testing project and in previous versions I had hundreds of individual pages that extended DetailPage and had the following lines:
#FindBy(id="someID")
private WebElement someIDBox;
to search a page for a given id and assign that element to a variable. I now have a similar website that is nonW3C so instead of finding by id I now need to find by name ie:
#FindBy(name="someID")
private WebElement someIDBox;
is what I now want.
So my question is how can I (if possible) make a super class function in DetailPage that would notice I say id and override with name?
I DO NOT want a function that just replaces the text id with name in the individual pages as I need to preserve those.
The author of this article extended the 'FindBy` annotation to support his needs. You can use this to override the 'FindBy' and make your on implementation.
Edited code sample:
private static class CustomFindByAnnotations extends Annotations {
protected By buildByFromLongFindBy(FindBy findBy) {
How how = findBy.how();
String using = findBy.using();
switch (how) {
case CLASS_NAME:
return By.className(using);
case ID:
return By.id(using); // By.name(using); in your case
case ID_OR_NAME:
return new ByIdOrName(using);
case LINK_TEXT:
return By.linkText(using);
case NAME:
return By.name(using);
case PARTIAL_LINK_TEXT:
return By.partialLinkText(using);
case TAG_NAME:
return By.tagName(using);
case XPATH:
return By.xpath(using);
default:
throw new IllegalArgumentException("Cannot determine how to locate element " + field);
}
}
}
Please note I didn't try it myself. Hope it helps.
This does not meet the criteria that I asked but thanks to #guy I was introduced to ByIdOrName which helps with my problem. So I made a little script that went through all my files in the workspace and replaced
#FindBy(id="someID")
private WebElement someIDBox;
with
#FindBy(how = How.ID_OR_NAME, using="someID")
private WebElement someIDBox;
while this made it so I had to alter all test pages(which is what I wanted to avoid) it does not alter how other tests for other websites work which is key!

Get the By locator of an already found WebElement

Is there an elegant way to get the By locator of a Selenium WebElement, that I already found/identified?
To be clear about the question: I want the "By locator" as used to find the element. I am in this case not interested in a specific attribute or a specific locator like the css-locator.
I know that I could parse the result of a WebElement's toString() method:
WebElement element = driver.findElement(By.id("myPreciousElement"));
System.out.println(element.toString());
Output would be for example:
[[FirefoxDriver: firefox on WINDOWS (....)] -> id: myPreciousElement]
if you found your element by xpath:
WebElement element = driver.findElement(By.xpath("//div[#someId = 'someValue']"));
System.out.println(element.toString());
Then your output will be:
[[FirefoxDriver: firefox on WINDOWS (....)] -> xpath: //div[#someId = 'someValue']]
So I currently wrote my own method that parses this output and gives me the "recreated" By locator.
BUT is there a more elegant way already implemented in Selenium to get the By locator used to find the element? I couldn't find one so far.
If you are sure, there is none out of the box, can you think of any reason why the API creators might not provide this functionality?
*Despite the fact that this has nothing to do with the question, if someone wonders why you would ever need this functionality, just 2 examples:
if you use PageFactory you most likely will not have the locators as as member variables in your Page class, but you might need them later on when working with the page's elements.
you're working with APIs of people who just use the Page Object Pattern without PageFactory and thus expect you to hand over locators instead of the element itself.*
tldr; Not by default, no. You cannot extract a By from a previously found WebElement. It is possible, however, through a custom solution.
It's possible to implement a custom solution, but Selenium does not offer this out-of-the-box.
Consider the following, on "why"..
By by = By.id("someId");
WebElement e = driver.findElement(by);
you already have the By object, so you wouldn't need to call something like e.getBy()
No, there's not. I have implemented a possible solution as a proxy:
public class RefreshableWebElement implements WebElement {
public RefreshableWebElement(Driver driver, By by) {
this.driver = driver;
this.by = by;
}
// ...
public WebElement getElement() {
return driver.findElement(by);
}
public void click() {
getElement().click();
}
// other methods here
}
There is no elegant way provided by Selenium. And this is horrible
1) PageObject and PageFactory implies that we have WebElements in Page classes, but we don't have locators of those elements.
2) If I find element as descendant of current element using webElement.findElement(By), then I don't have the locator of this descendant even if I stored parent's locator in the variable.
3) If I use findElements function that returns List of elemetns, then I don't have locator for each specific element.
4) Having locator for element is useful at least because ExpectedConditions with locator as parameter are much better implemented than ExpectedConditions with WebElement as parameter.
For me Selenium is ill-conceived and poorly implemented library
Currently there is no specific method from selenium's end to do so. What you can do is write your custom method. You will get the clue of what selector type and path is used by just printing the webElement you have.
It looks something like this
[[ChromeDriver: chrome on XP (d85e7e220b2ec51b7faf42210816285e)] -> xpath: //input[#title='Search']]
Now, what you need to do is to extract the locator and its value. You can try something like this
private By getByFromElement(WebElement element) {
By by = null;
//[[ChromeDriver: chrome on XP (d85e7e220b2ec51b7faf42210816285e)] -> xpath: //input[#title='Search']]
String[] pathVariables = (element.toString().split("->")[1].replaceFirst("(?s)(.*)\\]", "$1" + "")).split(":");
String selector = pathVariables[0].trim();
String value = pathVariables[1].trim();
switch (selector) {
case "id":
by = By.id(value);
break;
case "className":
by = By.className(value);
break;
case "tagName":
by = By.tagName(value);
break;
case "xpath":
by = By.xpath(value);
break;
case "cssSelector":
by = By.cssSelector(value);
break;
case "linkText":
by = By.linkText(value);
break;
case "name":
by = By.name(value);
break;
case "partialLinkText":
by = By.partialLinkText(value);
break;
default:
throw new IllegalStateException("locator : " + selector + " not found!!!");
}
return by;
}
For me worked with commons-lang3
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
For remote web element use method like:
protected String getLocator(WebElement element) {
try {
Object proxyOrigin = FieldUtils.readField(element, "h", true);
Object locator = FieldUtils.readField(proxyOrigin, "locator", true);
Object findBy = FieldUtils.readField(locator, "by", true);
if (findBy != null) {
return findBy.toString();
}
} catch (IllegalAccessException ignored) {
}
return "[unknown]";
}
I had written this utility function which returns a string combination of locator strategy + locator value.
private String getLocatorFromWebElement(WebElement element) {
return element.toString().split("->")[1].replaceFirst("(?s)(.*)\\]", "$1" + "");
}
My solution when I ran into needing the By locator to use for an ExpectedConditions and I had my locators in the Page Object Factory was to use a String that had the locator in it and then build my By object and the element locator from that.
public class PageObject {
private static final String XPATH_NAME = "...";
public #iOSXCUITFindBy(xpath = XPATH_NAME)
List<MobileElement> mobileElementName;
public By getByXPath(){
return new By.ByXPath(XPATH_NAME);
}
public PageObject() {
PageFactory.initElements(driver, this);
}
}

How do I get an HTML Element By CSS class name via a JavaFX WebEngine?

I am able to get an element by Id like this in JavaFX.
Element nameField = engine.getDocument().getElementById( "name" );
How do I do the same given element's classname?
Thanks.
I came across this and saw there wasn't much for answers. The way I found way to work with dom classes is not great but it gets the job done.
To add a class on a Node you obtained, use the setAttribute() method. Be careful to maintain any classes that might already exist on the Node.
Document doc = engine.getDocument();
if (doc != null){
Element elem = doc.getElementById('someID');
String classNames = elem.getAttribute("class");
classNames += " someClass"; //note the whitespace!
elem.setAttribute("class", classNames);
}
Then, if you wish to search the DOM by class you can execute javascript to do so using the executeScript on the WebEngine. The return type depends on what you're asking the script to do.
Small note: disabling javascript via engine.setJavaScriptEnabled(false); does not prohibit use of the engine.executeScript() method.
HTMLCollection result = (HTMLCollection)engine.executeScript("document.getElementsByClassName('someClass')");
However inefficient, I did this to determine what I would be getting back from executeScript() before writing any further code:
Object result = engine.executeScript("document.getElementsByClassName('someClass')");
System.out.println(result.getClass().getName());
Like I said it isn't great, but you can write some wrapper functions to make it easier to work with. Hope this helps!
You can access any component by using: the lookUp method.
For Example:
Button has class: specialClazz and your mainPane is a StackPane: rootPane
So you just do:
rootPane.lookUp(".specialClazz"); // .-selector!
For an ID:
rootPane.lookUp("#specialID"); // #-selector!
I'd use javax.xml.xpath. See XPath Tutorial - Example 6: Values of attributes can be used as selection criteria.
Note also that there is not necessarily a (single) element's class name. id is unique in a Document whereas class is not. So it should rather read elements' class name. (Subtle, but important. :-) In other words: there is not necessarily just one Element returned when searching for a given class.
The above answer:
"HTMLCollection result = (HTMLCollection)engine.executeScript("document.getElementsByClassName('someClass')");"
is bad. Because executeScript return's JSObject but not HTMLCollection; it cannot be casted.
The corrent is below java9 modular code sample (java14 has var):
and you should add vm options to project:
--add-exports javafx.web/com.sun.webkit.dom=atools
var doc = webEngine.getDocument();
if (doc instanceof HTMLDocument htmlDocument) { //actually htmlDoc is HTMLDocumentImpl but java9 have to reflect
try {
Method method = null;
var clazz = htmlDocument.getClass();
method = clazz.getSuperclass().getMethod("getElementsByClassName", String.class);
HTMLCollection collection = (HTMLCollection) method.invoke(htmlDocument,"button");
//if collection not only 1
//you should special the document more deeply
for (int i = 0; i < collection.getLength(); i++) {
var btn = collection.item(i);
//actual is HTMLButtonElementImpl
//HTMLButtonElement htmlButtonElement = (HTMLButtonElement) btn;
var method1 = btn.getClass().getMethod("click");
method1.invoke(btn);
Log.d("");
break;
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
If you are not using java9+ modular, these reflects can replace with HTMLButtonElementImpl,HTMLDocumentImpl.

I don't want to write driver.findelement(By.xpath("")) again and again

Hi this is the code below: What I want to do is this build a function in which i just pass the value of XPath, so i don't have to write driver.findElement(By.xpath("")) again and again.
driver.findElement(By.xpath("//*[#id='lead_source']")).sendKeys("Existing Customer");
driver.findElement(By.xpath("//*[#id='date_closed']")).sendKeys("08/07/2013");
driver.findElement(By.xpath("//*[#id='sales_stage']")).sendKeys("Opportuntiy Qualification");
driver.findElement(By.xpath("//*[#id='opportunity_monthly_volume']")).sendKeys("10895");
driver.findElement(By.xpath("//*[#id='probability']")).sendKeys("90");
driver.findElement(By.xpath("//*[#id='opportunity_sales_rep']")).sendKeys("Sales Rep");
driver.findElement(By.xpath("//*[#id='opportunity_sales_regions']")).sendKeys("Northeast");
driver.findElement(By.xpath("//*[#id='opportunity_current_lab']")).sendKeys("Current lab");
driver.findElement(By.cssSelector(Payermixcss +"opportunity_medicare")).sendKeys("5");
The best way would be to use the PageObject pattern. You could do something like this:
public class MyFormPageObject {
public MyFormPageObject enterLeadSource(String value) {
driver.findElement(By.id("lead_source")).sendKeys(value);
return this;
}
public MyFormPageObject enterDateClosed(String value) {
driver.findElement(By.id("date_closed")).sendKeys(value);
return this;
}
//...
}
// then in your test code
myFormPO.enterLeadSource("Existing Customer").enter("08/07/2013");
Note that as mentionned, you should use By.id if you have an identifier, because XPath is slower and not always well supported by all implementation of WebDriver.
Extract a method to upper level and just pass the values as parameters
eg:
yourMethod(Path path) {
driver.findElement(By.xpath(path))
}
As per your concern you can you use page object model and create the method and pass the variable to exact method.. I don't konw java but i know and concepts
private string variable= "Xpath value"
Pass this variable to method and it will interact with POM.. Before that u should know about POM. Then u can easily understand the concepts. Hope it will help full you...
To reduce the amount of code you have to write, you could use a function like this:
private WebElement findElementByXpath(String xpath) {
return driver.findElement(By.xpath(xpath));
}
The first line of your code would be:
findElementByXpath("//*[#id='lead_source']").sendKeys("Existing Customer");
It does not really reduce the length of the code but it only takes one CTRL + SPACE for autocompletion in Eclipse IDE.

Categories

Resources