When building test cases for Apache Wicket web applications using the Selenium-IDE tool, it is often useful to identify page elements by their wicket:id as this is a fixed value, while the element id may vary. This article presents a simple Selenium user-extensions.js locator plugin to identify elements by wicket:id.

The Wicket template system is built around the use of the wicket:id attribute which associates Java components with their presentation elements.

A Wicket page class
public class MyPage extends WebPage {
    public MyPage() {
        add(new FeedbackPanel("feedback"));

        Form<Void> form = new Form<Void>("newUser");
        form.add(new TextField<String>("userName"));
        form.add(new CheckBox("newsletter"));
        add(form);
    }
}
The associated HTML template
<?xml version="1.0"?>
<html xmlns:wicket="http://wicket.apache.org/">
<body>
    <div wicket:id="feedback"> </div>

    <form wicket:id="newUser">
        <label>User Name</label>
        <input wicket:id="userName" type="text" name="userName" />
        <label>Newsletter</label>
        <input wicket:id="newsletter" type="checkbox" name="newsletter" />
        <input type="submit" />
    </form>
</body>
</html>

Selenium-IDE provides a number of methods for locating page elements: by ‘identifer’ (element id or name), element id, input name, DOM, CSS, XPath and more. To identify an element based on wicket:id we can use XPath however the presence of the ‘wicket’ namespace makes it a bit complicated.

<!-- Doesn't work -->
// form[@wicket:id='newUser']

<!-- Works -->
//form[@*[local-name()='wicket:id']='newUser']

The @*[local-name()='wicket:id'] uses a predicate to find the attribute whose local part of their expanded name is wicket:id. A namespaced XML attribute’s ‘expanded name’ comprises the ‘local part’ (what appears in the XML element) and the namespace URI (in this case ‘http://wicket.apache.org’). The inner attribute predicate is contained in an outer predicate that compares the attribute’s value to ‘newUser’.

Simple wicket:id locator plugin

Writing this XPath expression everytime you want to locate an element will quickly become tiresome. Selenium-IDE provides the user-extensions.js plugin system which makes it easy to create a wicket:id locator plugin.

PageBot.prototype.locateElementByWicketId = function(text, inDocument) {
    return this.locateElementByXPath("//*[@*[local-name()='wicket:id']='" + text + "']", inDocument);
};

The plugin is just a front-end to the built-in XPath locator plugin and simply assesmbles our XPath expression from a given wicket:id value. Now we can identify the form using wicketId=newUser as the locator in a Selenium command.

Nested wicket:id locator plugin

The simple plugin will find a single element by wicket:id but what if we have more than one element with the same wicket:id at different points in the HTML hierarchy. For example, the page could contain two panels (<div wicket:id="panel1"> and <div wicket:id="panel2">) and each panel contains an element with wicket:id="userName". It would be useful to be able to specify the ancestors of the userName element we are trying to locate.

The following extension to the wicketId locator plugin allows you to specify a series of wicket:id values separated by either a single slash (/) for a direct descendant or a double slash (//) for an indirect descendant. For our two panels, consider the following hierarchies:

  • panel1

    • userName

  • panel2

    • form

      • userName

For panel1 we can use wicketId=panel1/userName and for panel2 we can use wicketId=panel2//userName. (Of course, wicketId=panel1//userName will work as well.)

PageBot.prototype.locateElementByWicketId = function(text, inDocument) {
    return this.locateElementByXPath(wicketId_createPath(text), inDocument);
};

function wicketId_createPath(text) {
    var path = "//*";

    var start = 0;
    var sep = undefined;
    while (true) {
        // Find the start of the next separator.
        var pos = text.indexOf('/', start);

        // If we have found a separator, append to the path.
        if (sep) {
            path += sep + '*';
        }

        if (pos === -1) {
            // No more separators: append the remainder as the final path element and exit.
            var wid = text.slice(start);
            if (wid.length > 0) {
                path += "[@*[local-name()='wicket:id']='" + wid + "']";
            }

            break;
        } else {
            // Extract up to the start of the separator and append to the path.
            var wid = text.slice(start, pos);
            if (wid.length > 0) {
                path += "[@*[local-name()='wicket:id']='" + wid + "']";
            }

            // Determine if we have a single- or double-slash separator.
            if (text.charAt(pos + 1) === '/') {
                sep = '//';
                ++pos;
            } else {
                sep = '/';
            }
        }

        // Continue the search from after the separator.
        start = pos + 1;
    }

    return path;
}