Selenium 2.0 / Webdriver extending FindBy annotation to support dynamic id/xpath

by Yagish Sharma on January 4, 2011

Don't be shellfish...FacebookTwitterGoogle+LinkedInRedditEmail

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.

Don't be shellfish...FacebookTwitterGoogle+LinkedInRedditEmail

Previous post:

Next post: