Skip to content
August 8, 2012 / selenium34

Custom Locators Part 1

I’ll first state that I love the PageObject pattern, but I’ll admit there is one thing I’d like to see done differently to make it a bit more extensible. Getting the browser to locate the vast majority of items is actually quite simple, but there will come a time when you may need another way to get an element on the screen. In our tests we really want to avoid using XPATH, so we have removed it from nearly all of our code; this is mostly due to speed concerns (although to be fair I have not profiled the solution against XPATH), however it is also avoided because it more closely ties our code to how the page currently exists in absolute form. Let’s take a look at how we would typically find an element on one of our pages:

@FindBy(id = "jq-intro")
private WebElement defaultIntroDiv;

I wish, that the How enum in this case was responsible for implementing the ElementLocator interface. This would allow us to modify the @FindBy annotation to utilize an ElementLocator instead of the How enum. We could then create new implementations quite easily, as we would really only need to create one concrete implementation (this would require some changes to core of selenium).  As it stands, multiple classes were needed to extend this functionality.

I’m surprised I haven’t seen much out there about custom PageFactory implementations (I feel this is mostly due to the majority of our needs being met out of the box). It became apparent that if we were going to avoid using XPath, we would need to utilize jQuery in some places. Since all of our pages already include jQuery, this was quite straight-forward. The example you are about to see could be made a bit more robust, by adding in jQuery  to the page if it isn’t detected, but I’ll leave that as an exercise for the reader since it’s beyond our needs.

The first thing we did was create a new annotation.

public @interface JQueryLocator {
  String $();
}

We deliberately broke the best practice on annotations, that single value annotations should have a single attribute name value, to clearly trigger our minds to use jQuery syntax on our elements. We can now annotate our fields in the following manner:

@JQueryLocator($ = "('#jq-intro')")
private WebElement jQueryIntroDiv;

We now need to tell Selenium how to locate these items. The easiest way to do this is to create the following classes: CustomPageFactory, CustomElementLocatorFactory, and CustomFieldDecorator (you can name them what you want).

The first is the CustomPageFactory, we need to make it aware of how to find the elements with jQuery, we follow a similar pattern with our selenium tests, whereby we call the PageFactory in the constructor. This allows us to keep our custom PageFactory quite short.  In fact, it can be written in four lines:

public class CustomPageFactory {

  public static void initElements(final WebDriver driver, final Object page) {
    initElements(new CustomElementLocatorFactory(driver), page);
  }
  public static void initElements(final ElementLocatorFactory factory, final Object page) {
    PageFactory.initElements(new CustomFieldDecorator(factory), page);
  }
}
public class CustomFieldDecorator extends DefaultFieldDecorator implements FieldDecorator {
  public CustomFieldDecorator(final CustomElementLocatorFactory factory) {
    super(factory);
  }
  @Override
  public Object decorate(final ClassLoader loader, final Field field) {
    if (!(WebElement.class.isAssignableFrom(field.getType()) || isDecoratableList(field))) {
      return null;
    }
    final CustomElementLocator locator = (CustomElementLocator) factory.createLocator(field);
    if (locator == null) {
      return null;
    }
    if (WebElement.class.isAssignableFrom(field.getType())) {
      final WebElement proxy = proxyForLocator(loader, locator);
      try {
        proxy.isDisplayed();
        return proxy;
      } catch (final NoSuchElementException nsee) {
        if (!locator.isOptionalElement()) {
          throw nsee;
        }
        return new MissingWebElement(nsee);
     }
    } else if (List.class.isAssignableFrom(field.getType())) {
      return proxyForListLocator(loader, locator);
    } else {
      return null;
    }
  }
  private boolean isDecoratableList(final Field field) {
    if (!List.class.isAssignableFrom(field.getType())) {
      return false;
    }
    // Type erasure in Java isn't complete. Attempt to discover the generic type of the list.
    final Type genericType = field.getGenericType();
    if (!(genericType instanceof ParameterizedType)) {
      return false;
    }
    final Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
    if (!WebElement.class.equals(listType)) {
      return false;
    }
    if (field.getAnnotation(FindBy.class) == null && field.getAnnotation(FindBys.class) == null && field.getAnnotation(JQueryLocator.class) == null) {
      return false;
   }
   return true;
  }
  @Override
  protected WebElement proxyForLocator(final ClassLoader loader, final ElementLocator locator) {
    final InvocationHandler handler = new CustomLocatingElementHandler((CustomElementLocator) locator);
    WebElement proxy;
    proxy = (WebElement) Proxy.newProxyInstance(loader, new Class[] { WebElement.class, WrapsElement.class, Locatable.class }, handler);
    return proxy;
  }
}

You can find the implementation of the MissingWebElement here. In our next post we will show how to create the CustomElementLocatorFactory and the CustomFieldDecorators to use in conjunction with this PageFactory.

Advertisements

7 Comments

Leave a Comment
  1. ZeleniumUser / Nov 6 2012 9:30 pm

    I think we are missing the implementation of CustomFieldDecorator class.
    I have went through both part1 and part2 but it seems that we are missing smth to get this to work.
    Should the CustomFieldDecorator be extending the FieldDecorator class and taking in the factory and page object as an argument ?

    • selenium34 / Nov 6 2012 9:42 pm

      Good catch, I will attempt to update this post tonight. I’m at a conference for the time being. Here is a link to the implementation:

      https://github.com/selenium34/custom-page-factory/blob/master/src/main/java/com/example/CustomFieldDecorator.java

      • ZeleniumUser (Mek) / Nov 6 2012 9:52 pm

        Thanks for the quick reply!
        I will try to work with the given resource for the time being 🙂

      • selenium34 / Nov 6 2012 11:01 pm

        No problem. I’ve added in the implementation to this post. I believe it will still work with the code listed, but there were some modifications for later posts which were made, which might cause some issues. If so, it might be best to simply get the code from github, and plug it into your project.

        Please let me know if you have any questions/issues.

Trackbacks

  1. Custom Locators Part 2 « Ramblings on Selenium
  2. A Smattering of Selenium #112 « Official Selenium Blog
  3. Optional Elements « Ramblings on Selenium

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: