Extending Selenium 2.0 / WebDriver to support Ajax

by Yagish Sharma on January 4, 2011

Don't be shellfish...FacebookTwitterGoogle+LinkedInRedditEmail

Recently I started using Selenium 2.0, which uses WebDriver to execute Automated test cases. It works great with normal static, page refreshing applications, but where Ajax is used extensively, this does not works reliably.

Selenium does comes with AjaxElementLocatorFactory, which creates instances of AjaxElementLocator. Now the idea is, if you send an Ajax request, this ElementLocator waits for 250 milliseconds to look for the element, till it ultimately times out (configurable). The only exposed API from Selenium, that I found, was PageFactory, whose main purpose is to create a DefaultElementLocatorFactory, which does not wait.

So here are some of the modifications I had to make (using trial and error), to get this framework to work reliably (if you had similar luck, join the bandwagon :)).

First modification – Use what is provided. To expose AjaxElementLocatorFactory, I had to create a new extension of PageFactory class. Here it is -

  
        package com.brim.selenium.locator;
        import java.lang.reflect.InvocationTargetException;
		import java.lang.reflect.Method;

		import org.apache.commons.lang.StringUtils;
		import org.openqa.selenium.WebDriver;
		import org.openqa.selenium.support.PageFactory;
		import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
		import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;

		import com.brim.selenium.util.SeleniumUtility;

		public class AjaxEnabledPageFactory extends PageFactory {
		    // instantiatePageMethod is defined private in PageFactory, so use reflection to expose it,
			// or copy and paste. I hate copy/paste so implemented it using reflection
			
			static Method instantiatePageMethod = null;
			static {
				Method[] methods = PageFactory.class.getDeclaredMethods();
				for (Method method : methods) {
					if (StringUtils.equals(method.getName(), "instantiatePage")) {
						instantiatePageMethod = method;
						instantiatePageMethod.setAccessible(true);
					}
				}
			}
            
            // these are defined static in PageFactory, so had to copy this methods, to call my instantiatePage method
            // unfortunately, Java does not allow over-riding static methods, though compiler does not complain :( 			
			public static  T initElements(WebDriver driver, Class pageClassToProxy) {
				T page = instantiatePage(driver, pageClassToProxy);
				initElements(driver, page);
				return page;
			}
             
			 // here is where i create AjaxElementLocatorFactory instance
			 // SeleniumUtility.timeOutInSeconds parameter is my own static parameter which is read from a property file
			 // more about it later
			public static void initElements(WebDriver driver, Object page) {
				final WebDriver driverRef = driver;
				initElements(new AjaxElementLocatorFactory(driverRef, SeleniumUtility.timeOutInSeconds), page);
			}
             
			 // another interesting issue i found, that sporadically Selenium/Webdriver throws
			 // StaleElementException, so it finds it, and while editing it throws that its stale,
			 // StaleReferenceAwareFieldDecorator does just that, if it sees the element is stale, look again. 
			 // More about it later.
			public static void initElements(ElementLocatorFactory factory, Object page) {
				final ElementLocatorFactory factoryRef = factory;
				initElements(new StaleReferenceAwareFieldDecorator(factoryRef), page);
			}
            
			 // and here is the call to PageFactory's private instantiatePage method using my new AjaxEnabledPageFactory class.
			protected static  T instantiatePage(WebDriver driver,
					Class pageClassToProxy) {
				try {
					return (T) instantiatePageMethod.invoke(
							AjaxEnabledPageFactory.class, driver, pageClassToProxy);
				} catch (IllegalArgumentException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				return null;
			}
		}

  
 

So, now the first modification is done, how to use it-

    
	  PageClass pageObject = AjaxEnabledPageFactory
				.initElements(driver, PageClass.class);
	
  

Now, when I run my testcases, with configurable timeOut of 5 seconds, Selenium tries every 250 milliseconds to look the element, for max of 5 seconds and then throws ElementNotFoundException. Cool, everything should work now, I thought so too, then sporadically I will get StaleElementReferenceException. Now, PageFactory uses DefaultFieldDecorator, which decorates every WebElement / RenderedWebElement instance with a proxy. Any time, you execute a method on a WebElement, proxy forwards the request to the locator to locate the element first and then execute the method. But, sometimes, locator locates the webElement, but just before it tries to type in the textbox or click the button, the element is removed from DOM. So much for Ajax :). Simple solution, capture the StaleElementReferenceException, look again using locator and execute the method. Here is how to do it.

      
	    import java.lang.reflect.InvocationHandler;
		import java.lang.reflect.InvocationTargetException;
		import java.lang.reflect.Method;
		import java.lang.reflect.Proxy;

		import org.apache.log4j.Logger;
		import org.openqa.selenium.RenderedWebElement;
		import org.openqa.selenium.StaleElementReferenceException;
		import org.openqa.selenium.WebElement;
		import org.openqa.selenium.internal.WrapsElement;
		import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator;
		import org.openqa.selenium.support.pagefactory.ElementLocator;
		import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;
		import org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler;
	   
	    public class StaleReferenceAwareFieldDecorator extends DefaultFieldDecorator {
			private static final Logger logger = Logger
					.getLogger(StaleReferenceAwareFieldDecorator.class.getName());

			public StaleReferenceAwareFieldDecorator(ElementLocatorFactory factory) {
				super(factory);
			}

			protected WebElement proxyForLocator(ClassLoader loader,
					ElementLocator locator, boolean renderedProxy) {
				InvocationHandler handler = new StaleReferenceAwareElementLocator(
						locator);

				WebElement proxy;
				if (renderedProxy) {
					proxy = (RenderedWebElement) Proxy
							.newProxyInstance(loader, new Class[] {
									RenderedWebElement.class, WrapsElement.class },
									handler);
				} else {
					proxy = (WebElement) Proxy.newProxyInstance(loader, new Class[] {
							WebElement.class, WrapsElement.class }, handler);
				}
				return proxy;
			}

			private static class StaleReferenceAwareElementLocator extends
					LocatingElementHandler {
				private final ElementLocator locator;

				public StaleReferenceAwareElementLocator(ElementLocator locator) {
					super(locator);
					this.locator = locator;
				}
                
				// here is where the magic happens. For a configurable number of times (I configured
				// 5 times, locater finds the element, and then tries to invoke the method on the element
				// In case StaleElementReferenceException is thrown, try again.
				public Object invoke(Object object, Method method, Object[] objects)
						throws Throwable {
					int count = 0;
					WebElement element = null;
					while (count < SeleniumUtility.numberOfTries) {
						try{
							element = locator.findElement();
						}catch(Exception ex){
							//logger.debug("Element not found", ex);
							SeleniumUtility.waitFor(250);
							count++;
							continue;
						}
						if ("getWrappedElement".equals(method.getName())) {
							return element;
						}

						try {
							return invokeMethod(method, element, objects);
						} catch (StaleElementReferenceException ex) {
							//logger.debug("Error locating element", ex);
						}
						count++;
					}
					throw new RuntimeException("Cannot invoke " + method.getName()
							+ " on element " + SeleniumUtility.getElementName(element)
							+ ". Cannot find it");
				}

				private Object invokeMethod(Method method, WebElement element,
						Object[] objects) throws Throwable {
					logger.debug("Invoking " + method.getName() + " on "
							+ SeleniumUtility.getElementName(element));
					try {
						return method.invoke(element, objects);
					} catch (InvocationTargetException e) {
						// Unwrap the underlying exception
						throw e.getCause();
					} catch (IllegalArgumentException e) {
						// Unwrap the underlying exception
						throw e.getCause();
					} catch (IllegalAccessException e) {
						// Unwrap the underlying exception
						throw e.getCause();
					}
				}
			}
		}
      
   

Using these extensions, I atleast got rid of Ajax related issues. There were more modifications to extend the WebDriver’s FindBy annotations to include dynamic pages, where a link when clicked adds a new row in a table, and the id/name differs just by the index.
I will cover it in next article. Btw, I used Selenium2.0a7 release.

Don't be shellfish...FacebookTwitterGoogle+LinkedInRedditEmail

Previous post:

Next post: