In my previous article, I suggested ways to extend Selenium 2.0 to support Ajax reliably.
While working with Selenium 2.0, I really liked usage of FindBy annotations, its concise and pretty cool. Reduces the code by tons of lines, when used with PageFactory. While writing my test cases, I hit a functionality, where I click a link, which makes an ajax call and adds a new row in the table. The only change in the id / name/ xpath was the index of the item (calling it item, thanks to working with JSF and Wicket, I think in UI components).
Since FindBy annotations only take static strings for id / name / xpath, due to the nature of Java annotations, I didn’t wanted to fallback to using WebDriver.findElement api. So, I extended Selenium’s / WebDriver’s FindBy annotation processor. God Bless Open Source
.
Some of the code, I am going to reuse is present in my previous article. Here is how to do it.
Since what I need is a new ElementLocator, so I will start creating that first, next will show how to expose it to be used by a Page Object. The magic happens in AnnotationsAcceptingIndex private class. The code is self explanatory. Excuse for some copy/paste and code smell. This class extends the Selenium’s Annotations class, which handles the FindBy and FindBys annotations, checks the using attribute of FindBy annotation and replace the # sign with the index. Pretty easy. eh!!
package com.brim.selenium.locator;
import java.lang.reflect.Field;
import org.apache.commons.lang.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.RenderedWebElement;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ByIdOrName;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.pagefactory.AjaxElementLocator;
import org.openqa.selenium.support.pagefactory.Annotations;
import org.openqa.selenium.support.ui.Clock;
import org.openqa.selenium.support.ui.SlowLoadableComponent;
import org.openqa.selenium.support.ui.SystemClock;
public class AjaxListItemLocator extends AjaxElementLocator {
protected final int timeOutInSeconds;
private Clock clock;
private WebDriver driver;
private Field field;
// the only thing that is differnt from AjaxElementLocator
// the index to replace.
private int index;
public AjaxListItemLocator(WebDriver driver, Field field, int index,
int timeOutInSeconds) {
this(((Clock) (new SystemClock())), driver, field, timeOutInSeconds);
this.driver = driver;
this.field = field;
this.index = index;
}
private AjaxListItemLocator(Clock clock, WebDriver driver, Field field,
int timeOutInSeconds) {
super(clock, driver, field, timeOutInSeconds);
this.timeOutInSeconds = timeOutInSeconds;
this.clock = clock;
}
public WebElement findElement() {
SlowLoadingElement loadingElement = new SlowLoadingElement(clock,
timeOutInSeconds);
try {
return loadingElement.get().getElement();
} catch (NoSuchElementError e) {
throw new NoSuchElementException(String.format(
"Timed out after %d seconds. %s", timeOutInSeconds, e
.getMessage()), e.getCause());
}
}
protected long sleepFor() {
return timeOutInSeconds;
}
private class SlowLoadingElement extends
SlowLoadableComponent {
private NoSuchElementException lastException;
private WebElement element;
private By by;
public SlowLoadingElement(Clock clock, int timeOutInSeconds) {
super(clock, timeOutInSeconds);
}
// here is the magic, AnnotationsAcceptingIndex class excepts the field, and also the index
protected void load() {
AnnotationsAcceptingIndex annotations = new AnnotationsAcceptingIndex(
AjaxListItemLocator.this.field,
AjaxListItemLocator.this.index);
by = annotations.buildBy();
this.element = AjaxListItemLocator.this.driver.findElement(by);
this.element.isEnabled();
}
protected long sleepFor() {
return AjaxListItemLocator.this.sleepFor();
}
protected void isLoaded() throws Error {
try {
load();
if (!isElementUsable(element)) {
throw new NoSuchElementException("Element is not usable");
}
} catch (NoSuchElementException e) {
lastException = e;
// Should use JUnit's AssertionError, but it may not be present
throw new NoSuchElementError("Unable to locate the element", e);
}
}
protected boolean isElementUsable(WebElement element) {
try {
return ((RenderedWebElement) element).isDisplayed();
} catch (Exception ex) {
}
return false;
}
public NoSuchElementException getLastException() {
return lastException;
}
public WebElement getElement() {
return element;
}
}
// This class sees the FindBy annotation's using clause and replace the # sign with the index.
// extends the org.openqa.selenium.support.pagefactory.Annotations class
private static class AnnotationsAcceptingIndex extends Annotations {
private int index;
private Field field;
public AnnotationsAcceptingIndex(Field field, int index) {
super(field);
this.field = field;
this.index = index;
}
// replace the # with the index
private String getId(String idPrefix, int index) {
return StringUtils.replace(idPrefix, "#", String.valueOf(index));
}
protected By buildByFromLongFindBy(FindBy findBy) {
How how = findBy.how();
// get the using attribute
String using = findBy.using();
// now replace the # sign with index
using = getId(using, this.index);
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);
default:
// Note that this shouldn't happen (eg, the above matches all
// possible values for the How enum)
throw new IllegalArgumentException(
"Cannot determine how to locate element " + field);
}
}
protected By buildByFromShortFindBy(FindBy findBy) {
if (!"".equals(findBy.className()))
return By.className(findBy.className());
if (!"".equals(findBy.id()))
return By.id(getId(findBy.id(), this.index));
if (!"".equals(findBy.linkText()))
return By.linkText(findBy.linkText());
if (!"".equals(findBy.name()))
return By.name(getId(findBy.name(), this.index));
if (!"".equals(findBy.partialLinkText()))
return By.partialLinkText(findBy.partialLinkText());
if (!"".equals(findBy.tagName()))
return By.tagName(findBy.tagName());
if (!"".equals(findBy.xpath()))
return By.xpath(getId(findBy.xpath(), this.index));
// Fall through
return null;
}
}
private static class NoSuchElementError extends Error {
private NoSuchElementError(String message, Throwable throwable) {
super(message, throwable);
}
}
}
Now we have the element locator, lets now create a factory which extends the Ajax supporting factory, defined in my previous article and expose the API to be used. SeleniumUtility is just a helper class which reads a property file for values, so will leave it out.
import java.lang.reflect.Field;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
import org.openqa.selenium.support.pagefactory.ElementLocator;
public class AjaxListItemLocatorFactory extends AjaxElementLocatorFactory {
private WebDriver driver;
private int index;
// SeleniumUtility is just a helper
public AjaxListItemLocatorFactory(WebDriver driver, int index) {
this(driver, index, SeleniumUtility.timeOutInSeconds);
}
public AjaxListItemLocatorFactory(WebDriver driver, int index, int timeOutInSeconds) {
super(driver, timeOutInSeconds);
this.driver = driver;
this.index = index;
}
public ElementLocator createLocator(Field field) {
return new AjaxListItemLocator(this.driver, field, this.index, SeleniumUtility.timeOutInSeconds);
}
}
Now, how to use it. For example, I have a PageObject, which contains a link WebElement on click of which an element is added/removed. Here is how to use it.
public class MyListItem implements
IndexAwareItem {
// Every item have an index
int index = -1;
// example for id or name
@FindBy(how = How.ID_OR_NAME, using = "firstName#")
RenderedWebElement firstNameElementTextBox;
// example to use xpath
@FindBy(how = How.XPATH, using = "//input[@type='text' and @name='lastName#'")
RenderedWebElement lastNameElementTextBox;
WebDriver driver;
// necessary constructor to be used using PageFactory
public MyListItem(WebDriver driver){
this.driver = driver;
}
// these methods are defined in IndexAwareItem interface
public int getIndex() {
return this.index;
}
public void setIndex(int index) {
this.index = index;
}
public void populateItem(){
// do something here related to test
}
}
public interface IndexAwareItem {
public int getIndex();
public void setIndex(int index);
}
So we defined the List Item, now use it in a page.
public class MyPageClass{
@FindBy(how = How.ID_OR_NAME, using = "addNewItem")
RenderedWebElement addNewItemLink;
WebDriver driver;
public MyPageClass(WebDriver driver){
this.driver = driver;
}
public void addNewItems(){
addNewItemLink.click();
populateListItem(driver, 0);
addNewItemLink.click();
populateListItem(driver, 1);
}
public void populateListItem(WebDriver driver, int index){
MyListItem listItem = AjaxEnabledPageItemFactory
.initElements(driver, MyListItem.class, index);
// pass the index to MyListItem
listItem.setIndex(index);
listItem.populateItem();
return listItem;
}
}
So much to add index in the FindBy annotation, yep, thats what I thought as well. Its even more fun seeing it working.
Appreciate your comments.
{ 1 comment… read it below or add one }
I refractored the AjaxListItemLocatorFactory. Here is the refractored class –
public class AjaxEnabledPageItemFactory extends AjaxEnabledPageFactory {
@SuppressWarnings("unchecked")
public static T initializePageItem(WebDriver driver,
Class pageClass, int index) {
Object pageItem = createInstance(driver, pageClass);
PageFactory.initElements(new StaleReferenceAwareFieldDecorator(
new AjaxListItemLocatorFactory(driver, index,
SeleniumUtility.timeOutInSeconds)), pageItem);
return (T) pageItem;
}
}