Skip to content
September 5, 2012 / selenium34

Optional Elements

One of the criticisms (from A Smattering of Selenium 107) of the PageObject pattern default PageFactory is that we are never certain if an item is on the page until attempting to utilize the item. In today’s post, we will address this shortcoming. This will be another code-heavy post.

We needed to know immediately if an item was not on a page, if it was a required field; to do that we decided to further extend our PageFactory (as seen in Custom Locators Parts 1 & 2). We decided to keep this information in the metadata of the fields, just as was done with the FindBy annotation. Here is the new annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface OptionalElement {
}

This is simply a marker annotation, we only utilize it when we have an Optional Item, which is why we didn’t have a boolean value here to designate whether or not the item was required. We default any field without this as required.

To utilize this functionality, we are now blowing away the usage of the DefaultFieldDecorator, so here is the updated version of our CustomFieldDecorator. Note: this is almost a verbatim copy of the DefaultFieldDecorator. We simply check to see if an item is on the page as soon as we finish loading the page, and we had to modify the proxyForListLocator to allow for our JQueryLocator to work properly.

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(); //This will break your initialization if the element is not located & the item is not optional
        return proxy;
      } catch (final NoSuchElementException nsee) {
        if (!locator.isOptionalElement()) {
          throw nsee;
        }
        return null;
      } 
    } 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;
  }
}

We will now add a field to our JQueryHomePage which will never be found and test it.  Here is the field:

@FindBy(id = "jq-intro_DOES_NOT_EXIST") @OptionalElement
private WebElement optionalIntroDivWithDefault;

And here is our new test, which will now pass.

@Test
public void ensureNullValue() {
  Assert.assertNull("A null value is expected for optional elements which do not exist on the page.",
  homePage.getOptionalIntroDivWithDefault());
}

We can now receive null for our WebElements which we expected to be proxied. This makes us pretty happy, however we think we can go a step further. For example, a lot of context is lost in the following example:

@Test(expected = NoSuchElementException.class)
public void ensureNoSuchElementExceptionThrown() {
  homePage.getOptionalIntroDivWithDefault().getText();
}

We will receive a NullPointerException in this case, instead of our desired NoSuchElementException. In our next post we hope to fix this aggravation.

Advertisements

4 Comments

Leave a Comment
  1. Iain Rose / Sep 7 2012 4:14 pm

    Hi there, I wrote the posts you linked to. Small point but my criticisms were not of the PageObject pattern, only the PageFactory’s implementation of it.

    Good to see there is an alternative to duplicating your locators though.

    • selenium34 / Sep 7 2012 4:23 pm

      Sorry for the misunderstanding, thanks for chiming in. Our next post will discuss your unhappiness with respect to locating elements within elements.

Trackbacks

  1. Better Optional Elements « Ramblings on Selenium
  2. A Smattering of Selenium #118 « Official Selenium Blog

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: