Selenium Node Locators for Wt and JWt applications

  • Posted by koen
  • Monday, May 13, 2013 @ 15:02

In this post we would like to clarify methods available for automated testing of Wt or JWt applications.

The most straight forward method is to implement server-based tests using WTestEnvironment. In this way one can define scripts which are server-side only, and which can be used to test functional aspects of an application. Because no browser is involved the test isn’t end-to-end (but it does capture everything in your application, it just does not test correct rendering by the Wt library). It may also be quite labor intensive to develop these scripts.

An attractive alternative are browser-based tests which are available through a number of frameworks (such as Selenium). These framewoks typically have an interactive script recorder where one can record scripts which can be replayed in one or more different browsers. This has obvious benefits over the previous method, but requires more tooling (and plumbing).

There are a few reasons however why Wt or JWt applications do not get along with browser-based automated testing frameworks, out of the box. Auto-generated id’s change from session to session and can thus not be used to identify a widget. Hard-coding an ID (using WWidget::setId() is really not recommended because it breaks possible reuse of your widgets, and widget reuse is really something we wish for you to have!

Fortunately, Selenium supports custom Locator Builders as extensions, and this is just what we need to make Selenium able to record test cases for Wt and JWt applications.

Below is a custom Selenium Locator Builder extension which you can start using in your Selenium IDE with the following steps:

  1. Save the script in a file (e.g. wtlocator.js)

  2. In Selenium IDE, open Options → Options… [General] and specify this file as Selenium IDE extensions file

  3. Close and Restart Selenium IDE

  4. In Selenium IDE, open Options → Options… [Locator Builders] and drag and drop xpath:wt as the top entry

If you now record a test case, it will now create locators that build an XPath expression using the following components:

  • only named widgets (WObject::setObjectName()) are used in the path

  • anchor clicks which reference internal paths are identified by href, to record navigation events

  • disambiguation based on index

Selenium xpath:wt Locator Builder script
LocatorBuilders.add('xpath:wt',
function(e) {
  function objectNameSelector(e) {
    var result = e.tagName.toLowerCase();

    var id = e.id;
    var objectName = e.id.substr(0, e.id.lastIndexOf('_'));

    result += "[starts-with(@id, '" + objectName + "_')]";

    return result;
  }

  function tagSelector(e) {
    return e.tagName.toLowerCase();
  }

  function hrefSelector(e) {
    var result = e.tagName.toLowerCase();
    result += '[@href="' + e.getAttribute('href') + '"]';
    return result;
  }

  function determineIndex(child, ancestor, expr) {
    var d = ancestor.ownerDocument;
    var nsr = null;

    var iterator =
      d.evaluate(".//" + expr, ancestor,
                 nsr, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

    var n = iterator.iterateNext();
    var i = 1;

    while (n) {
      if (n.id == child.id)
        return "[" + i + "]";
      ++i;
      n = iterator.iterateNext();
    }

    return "";
  }

  function addChildXPath(xpath, child_xpath, child, ancestor) {
    var index = determineIndex(child, ancestor, child_xpath);

    if (xpath.length > 0)
      xpath = "//" + child_xpath + index + xpath;
    else
      xpath = "//" + child_xpath + ")" + index;

    return xpath;
  }

  var xpath = "", child_xpath = "";

  if (e.id && e.id.indexOf('_') != -1)
    child_xpath = objectNameSelector(e);
  else
    child_xpath = tagSelector(e);

  var child = e;

  while (e.parentNode.tagName && e.parentNode.tagName.toLowerCase() != 'body') {
    var p = e.parentNode;

    if (p.id && p.id.indexOf('_') != -1) {
      xpath = addChildXPath(xpath, child_xpath, child, p);

      child_xpath = objectNameSelector(p);
      child = p;
    } else if (p.tagName.toLowerCase() == 'a' && p.href.length > 0) {
      xpath = addChildXPath(xpath, child_xpath, child, p);

      child_xpath = hrefSelector(p);
      child = p;
    }

    e = p;
  }

  xpath = "xpath=(" + addChildXPath(xpath, child_xpath, child, e.parentNode);

  return xpath;
}
);

By using WObject::setObjectName() to name important widgets, you can make the recorded test-cases more robust (to later changes), without any downsides with respect to reusability of your widgets.

One interesting idea that was generated while discussing this here at Emweb was to provide setObjectName() as an option with WTemplate::bindWidget(), which would automatically augment an application with a decent amount of structural named widgets.

Or what do you think?

Tags:
2 comments
  • Posted by anonymous
  • 5 months ago
Did you implement the bindWidget solution?
We are using the XPath script, but it doesn't seem very robust.
  • Posted by anonymous
  • 4 years ago
I like this a lot! And, yes, please give the option to automatically set the object names at widget bind-time.

Contact us for more information
or a personalised quotation