Skip to content
September 21, 2012 / selenium34

Child Elements

I’m sorry for the delay between these posts, but sometimes time gets away from us. When we look at our sites, we realized that they are very modular — look even at this site, we have a center column and a sidebar. We needed a way to verify that our modules were showing up in these correct areas, which led us to some initial frustration. Without storing your locator as static items you couldn’t do the following:

@FindBy(id = "jq-intro")
private WebElement introDiv;
@FindBy(css = "#jq-intro h2")
private WebElement introText; //Ensure that this is located within the introDiv

That wasn’t good enough; we wanted to enforce these types of rules, and yell out if items were located where they were unexpected (without having to use our jQuery locator or create a css locator based off the original). So today we are going to show you a slight spin-off of what we created to get around this limitation. This will again be a code heavy post, to keep it “short” there are no comments as in the past. This should be posted to github before long with some documentation / comments. I’ll update the blog once that has happened.

So let’s start (as usual) with a new annotation. We don’t allow for the $ here, as it seems unneeded — if you’re reverting to jQuery to find the item, it seems like that would be the proper locator to use. This will be the annotation that let’s us know that we should look inside of another WebElement:

public @interface ChildOf {
  String fieldName();
  How how();
  String using() default = "";
}

So let’s take the above example, and use our new annotation:

@FindBy(id = "jq-intro")
private WebElement introDiv;
@ChildOf(fieldName = "introDivWithDefault", how = How.TAG_NAME, using = "h2")
private WebElement introText;

Now we have to setup the plumbing for our PageFactory to find these items. To do this, we first need a ChildElementLocatorFactory:

public class ChildElementLocatorFactory implements ElementLocatorFactory {
  final Map<String, List<WebElement>> parentCandidates;
  public ChildElementLocatorFactory(final Map<String, List<WebElement>> parentCandidates) {
    this.parentCandidates = parentCandidates;
  }
  @Override
  public ElementLocator createLocator(final Field field) {
    if (field.isAnnotationPresent(ChildOf.class)) {
      return new ChildElementLocator(parentCandidates, field);
    }
    return null; //When the factory returns null, the PageFactory doesn't look for the item
  }
}

We also need to create our ElementLocator.

  public class ChildElementLocator implements ElementLocator {
    private final List<WebElement> parentCandidates;
    private final How how;
    private final String using;
    public ChildElementLocator(final Map<String, List<WebElement>> parentCandidates, final Field field) {
      final ChildOf childOf = field.getAnnotation(ChildOf.class);
      this.how = childOf.how();
      if (How.ID_OR_NAME.equals(how)) {
        this.using = field.getName();
      } else {
        this.using = childOf.using();
    }
    if (!parentCandidates.containsKey(childOf.fieldName())) {
      throw new NoSuchElementException("No parents with the field name '" + childOf.fieldName()
        + "' were located. Unable to locate element with: " + how + " using: " + using);
    }
    this.parentCandidates = parentCandidates.get(childOf.fieldName());
  }
  @Override
  public WebElement findElement() {
    NoSuchElementException finalException = null;
    for (final WebElement candidate : parentCandidates) {
      try {
        final WebElement e = candidate.findElement(buildBy());
        e.isDisplayed();
        return e;
      } catch (final NoSuchElementException nsee) {
        finalException = nsee;
      }
    }
    throw finalException;
  }
  @Override
  public List<WebElement> findElements() {
    for (final WebElement candidate : parentCandidates) {
      final List<WebElement> elements = candidate.findElements(buildBy());
      if (elements.size() > 0) {
        return elements;
      }
    }
    return Collections.emptyList();
  }

  private By buildBy() {
    //Implementation basically same as Annotations#buildByFromLongFindBy from Selenium code
    //Not showing here to shorten post
  }
}

And with that we need to update our PageFactory. This is long and uncommented, but again should be up on github soon.

public class CustomPageFactory {
  public static void initElements(final WebDriver driver, final Object page) {
    final Map<String, List<WebElement>> locatedFields = initElements(new CustomElementLocatorFactory(driver), page);
    initElements(new ChildElementLocatorFactory(locatedFields), page);
  }
  public static void initElements(final ChildElementLocatorFactory factory, final Object page) {
    PageFactory.initElements(new DefaultFieldDecorator(factory), page);
  }
  public static Map<String, List<WebElement>> initElements(final CustomElementLocatorFactory factory, final Object page) {
    return initElements(new CustomFieldDecorator(factory), page);
  }
  public static Map<String, List<WebElement>> initElements(final CustomFieldDecorator decorator, final Object page) {
    Map<String, List<WebElement>> locatedFields = new HashMap<>();
    Class<?> proxyIn = page.getClass();
    while (proxyIn != Object.class) {
      locatedFields = mergeValues(locatedFields, proxyFields(decorator, page, proxyIn));
      proxyIn = proxyIn.getSuperclass();
    }
    return locatedFields;
  }
  private static Map<String, List<WebElement>> mergeValues(final Map<String, List<WebElement>> foundInSubclasses,
    final Map<String, List<WebElement>> currentlyLocated) {
    if (foundInSubclasses.entrySet().size() == 0) {
      return currentlyLocated;
    }
    for (final Map.Entry<String, List<WebElement>> entry : foundInSubclasses.entrySet()) {
      if (currentlyLocated.containsKey(entry.getKey())) {
        final List<WebElement> locatedInSubclasses = entry.getValue();
        final List<WebElement> justFound = currentlyLocated.get(entry.getKey());
        locatedInSubclasses.addAll(justFound);
      } else {
        entry.setValue(currentlyLocated.get(entry.getKey()));
      }
    }
    return foundInSubclasses;
  }

  private static Map<String, List<WebElement>> proxyFields(final FieldDecorator decorator, final Object page, final Class<?> proxyIn) {
    final Map<String, List<WebElement>> locatedFields = new HashMap<>();
    final Field[] fields = proxyIn.getDeclaredFields();
    for (final Field field : fields) {
      final Object value = decorator.decorate(page.getClass().getClassLoader(), field);
      if (value != null) {
        try {
          field.setAccessible(true);
          field.set(page, value);
          if (locatedFields.containsKey(field.getName())) {
            if (!(value instanceof List)) {
              locatedFields.get(field.getName()).add((WebElement) value);
            }
          } else {
            if (!(value instanceof List)) {
              locatedFields.put(field.getName(), new ArrayList<WebElement>(Arrays.asList((WebElement) value)));
            }
          }
        } catch (final IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    }
    return locatedFields;
  }
}

Now we can add the following to our JQueryHomePageTest:

@Test
public void getText() {
  Assert.assertThat("jQuery\nis a new kind of JavaScript Library.",
  Matchers.equalToIgnoringCase(homePage.getTagline().getText()));
}

One thing to note: this will work with inheritance (on fields with the same name) as long as you annotate child elements with @OptionalElement.

Advertisements

7 Comments

Leave a Comment
  1. Twaldigas / Feb 26 2013 2:23 pm

    Thank you very much for this and previously posts. Your custom PageFactory works fine and helped me to fix the SERE problem in my tests.

    Now I want to work with your @ChildOf annotation. But the tests throw a NPE in ChildElementLocator.java. I hope with the following description you can help me to fix it.

    Test

    1. Open a page with a pagination.
    2. Get the current page number.
    3. Click “next” to go on the next page.
    4. Get the current page number.

    PageObject

    I work with PageObjects. I have a PageObject with the WebElements paginationContainer and currentPageNumber:

    @OptionalElement
    @FindBy(id = “pagination”)
    private WebElement paginationContainer;

    @ChildOf(fieldName = “paginationContainer”, how = How.CLASS_NAME, using = “page-block”)
    private WebElement currentPage;

    The PageObjects extends from a Functions.java. This class contains no WebElements. It contains only methods to deal with it. Examples:

    public boolean isElementPresent(final WebDriver driver, final By by) {
    return driver.findElements(by).size() > 0;
    }

    public boolean isElementPresent(final WebElement element) {
    try {
    element.getTagName();
    } catch (final NoSuchElementException ignored) {
    return false;
    } catch (final StaleElementReferenceException ignored) {
    return false;
    }
    return true;
    }

    My assumption is the NPE throw if a method from Functions.java will be called. I try to fix it and write following code at the begin of the while-loop in the CustomPageFactory.java:

    if (proxyIn.getName().equals(Functions.class.getName())) {
    proxyIn = proxyIn.getSuperclass();
    continue;
    }

    The NPE was gone, but now the ChildOf WebElements is always the same. In the test I get me the current page and I get always 1, also the test was paginate to page 2 or 3.

    I have no idea to fix it. I hope you can help me.

    I work with the same version you uploaded to github.

    Thank you very much and sorry for my poor english.

    Best regards,
    Twaldigas

    • selenium34 / Feb 26 2013 2:50 pm

      I’m afraid without seeing more of the code or the stack trace I won’t be able to do much to help you.

      Could you put the code up in a gist, so that I could take a closer look?

      • Twaldigas / Feb 26 2013 3:18 pm

      • selenium34 / Feb 26 2013 3:38 pm

        Are you certain you have no other changes in the code from github? Line 48 of the ChildElementLocator is a closing bracket for me. Also do you have a URL which I can run this test against as well? Hard to reproduce without HTML to test against.

      • Twaldigas / Feb 26 2013 3:50 pm

        I added my ChildElementLocator.java and the html snippet of the pagination container.

      • selenium34 / Mar 5 2013 1:58 pm

        I’ve been absolutely slammed, and probably will be for awhile. I’ll get to this as soon as I can. Sorry for the delay.

Trackbacks

  1. A Smattering of Selenium #123 « 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: