16.14. JavaScript Extension API to Support Custom AJAX/DHTML/JavaScript Widgets

16.14.1. Concept and Setup
16.14.2. Support for Simple Widgets
16.14.3. Extending the Name Generator and Identification
16.14.4. Support for Complex Widgets

To allow creating robust test scripts, a test framework and the automated tests should interact with the application and the HTML on high-level widgets instead of the low-level DOM elements. This way tests work on an abstract GUI level without knowledge of the actual application's internals (which the DOM is). If a test script works on the DOM directly this is nearly as fragile as GUI tests accessing widgets by screen coordinates.

The advantage of high-level tests which work on widgets is that they will not break just because the DOM representation or widget implementation changes. And this happens a lot while the application and the framework evolve.

Squish follows this principle to allow creating tests working on high-level widgets. Squish already comes with built-in support for popular AJAX and DHTML frameworks and recognizes their widgets. But given the amount of available AJAX, DHTML and JavaScript frameworks and custom widgets, it is not possible that Squish supports all widgets out-of-the box.

For this purpose Squish offers a JavaScript extension API allowing to extend Squish's widget support to recognize custom AJAX, DHTML and JavaScript widgets, identify them properly, interact with them and provide their API to test scripts.

This chapter gives an overview and examples allowing you to implement support for your custom widgets yourself. Alternatively, contact squish@froglogic.com if you want froglogic to implement the extension for your custom widgets or framework.

16.14.1. Concept and Setup

This extension is enabled by specifying the location of the JavaScript file implementing the extension. When Squish hooks into the Web browser, it will evaluate the JavaScript extension code file inside the browser.

In this JavaScript file it is possible to use the programming interface provided by Squish to hook into the object recognition, name generation and other tasks. Also, by implementing certain JavaScript hook-functions, it is possible to expose the API of your custom widgets to the test scripts.

To specify your JavaScript extension file (assuming that the file is named C:\squishext\myextension.js) add a line such as

Wrappers/Web/ExtensionScripts="C:\\squishext\\myextension.js"

to the squish.ini file located in the the etc sub-directory of your Squish installation.

In case you do distributed testing, this needs to be done only on the machine where squishrunner or the Squish IDE are used.

The API which can be used to implement the extensions is documented here. The following chapters will explain the API and show examples of its usage.

16.14.2. Support for Simple Widgets

To avoid recording superfluous mouse click events Squish only records clicks on DOM elements which are known to react on clicks. To determine that, Squish checks whether the element has a mouse event handler such as onClick set or whether the element is a known clickable widget (form input elements, links, etc.)

If your JavaScript library comes with custom clickable widgets such as special buttons you can make your widgets known to Squish using the Squish.registerWidget() JavaScript function.

This function expects a named argument list which specifies the DOM class of the element and the type of events that should be recorded.

Let us assume you have a special button implementation which is represented in the DOM as

<SPAN class='ajaxbutton'>Click Me</SPAN>

To let Squish know that it should record mouse click events for this type of widget, add the following line to your extension JavaScript file:

Squish.registerWidget({Class: "ajaxbutton", Event: "click"});

Now, when you record a script and click this button, Squish will record mouseClick() statements.

To identify a widget on replay Squish defaults to a built-in name generation algorithm. In this specific example the innerText will be used since no id or name is set. This approach suffices in this case.

The generated name for this object will be {tagName='SPAN' innerText='Click Me'}.

If desired, the name generation and identification can be extended by definining additional properties. More about that in Extending the Name Generator and Identification (Section 16.14.3).

16.14.3. Extending the Name Generator and Identification

To generate a name for a widget, Squish generates a list of property pairs identifying the object. Squish uses a set of pre-defined properties such as tagName, id, name, title and some more.

If the properties do not uniquely identify the object, an occurrence property is added to the name which specifies which of the objects matching the other properties is supposed to be selected.

Except tagName the properties are optional and can freely be chosen.

In some cases it might be desirable to use custom properties in the name for specific objects. To do this, it is possible to specify an own hook function which will then be called to generate the name of your own widgets. If necessary, a hook function performing the property matching on object searches can be installed.

16.14.3.1. Implementing a Custom Name Generator

As an example lets take a menu element which looks like

<SPAN class='menu' id='fileMenu'>
  <SPAN class='menuItem' menuID='fileOpen'>Open</SPAN>
  <SPAN class='menuItem' menuID='fileQuit'>Quit</SPAN>
</SPAN class='menu'>

First, let us register the menu items as clickable widgets with

Squish.registerWidget({Class: "menuItem", Event: "click""});

More about that in Support for Simple Widgets (Section 16.14.2)

Now, we want the menuID attribute, the DOM class name and the ID of the parent element to be used in the object name. Therefore we implement a function which generates a name for menuItem SPANs:

var myuiExtension = new Object;

myuiExtension.nameOf = function(o) {
    if (o.tagName == "SPAN" && Squish.hasClassName(o, "menuItem")) {
        var n = '{' + Squish.propertiesToName(o, ["tagName", "menuID"]);
        n += "parentID='" + o.parentNode.id + "' ";
        n += "className='menuItem'}';
        return escape(Squish.uniquifyName(n, o));
    }
    return undefined;
}

We add this function as a property of the myuiExtension object. In the implementation we check if the object is a menu item (which we want to handle). If this is not the case we return undefined so Squish will call the default name generator or any other installed name generation hook.

To check if the element is the one we want to handle we check if its tag name is SPAN and if it is of the DOM class menuItem. For this check we can use the helper function Squish.hasClassName() which checks if any of the specified DOM classes of the object matches the desired one.

In case we decided to handle an object we have to assemble its name. We will return it as string of the format {property1='value1' property2='value2' ... propertyN='valueN'}.

The Squish.propertiesToName() function can be used to conveniently generate a name from properties than can directly be queried from an object. This function expects the object and an array of property names as arguments and returns a string of the format property1='value1' property2='value2' ... propertyN='valueN'.

So we specify the mandatory tagName and the menuID property this way.

Then we add the ID of the parent element as parentID property to the name. As it is not a regular property of the object itself it will be a so-called pseudo-property.

Finally, we add className='menuItem' to the name. We cannot use the Squish.propertiesToName() for in this case since the DOM property name is class while Squish uses the JavaScript className identifier. Also, a DOM class may contain multiple values while we only want to specify menuItem.

After we built the name we make the name unique. We don't have to do this manually but can use the Squish.uniquifyName() function for that which expects the generated name and the actual object which should be identified by the name. This function will, if necessary, add an occurrence property to the name.

To ensure that special characters are escaped properly we use the standard JavaScript escape() function and return the result.

Now we have to let Squish know about this function to be used for name generation:

Squish.addNameOfHook(myuiExtension.nameOf);

As a result the name of the Open menu item will look like {tagName='SPAN' menuID='fileOpen' parentID='fileMenu' className='menuItem'}.

16.14.3.2. Implementing a Custom Name Matcher

When searching an object by name, Squish iterates over all objects in the DOM tree and queries the specified object properties to see if their values match.

Using a custom name generator hook it is possible to also add pseudo-properties to a name as we did for the parentID. As Squish has no knowledge about this property we need to implement a function performing the lookup and matching.

myuiExtension.matchObject = function(o, prop, value) {
    if (prop == "parentID")
	return Squish.matchProperty(value, o.parentNode.id);

    return undefined;
}

Again, we implement this function as a property of the myuiExtension object. Squish will pass the object, the property name and the expected value as specified in the name to this function.

In the implementation we will first check whether we want to handle a requested property. If not, we simply return undefined so Squish will try to resolve it through other means.

In our case we decide to handle the parentID property and we do matching ourself and return true or false depending on whether the value matches or not.

The value is not just a string but an object which contains the string value and flags denoting whether the matching should be based on a simple string comparison, a regular expression or wildcards. See ##### on that topic.

The exact details are nothing to worry about, though. The Squish.matchProperty() function can be used to take care of all that. The function expects the value object as the first argument and the string of the property value of the specified object as second argument. In our example we get the string value by querying the id of the specified object's parent DOM node.

The custom name matching setup is completed by installing the hook:

Squish.addMatchObjectHook(myuiExtension.matchObject);

16.14.4. Support for Complex Widgets

Using the extension mechanism it is also possible to add dedicated support for complex widgets such as tree views, tables, menus and calendar controls.

Common to these widgets is that they are all item-based. A table contains cells, a tree consists of nodes, a menu is made up of a list of entries and so on.

To establish a high-level interaction with such widgets, actions such as clicks on an item and expansion/collapsing of nodes need to be recognized and replayed independent of the underlying HTML representation. In addition, certain states and properties, such as the current selection need to be queryable from test scripts to allow robust, automatic verifications.

Using the item view and item abstractions and the necessary JavaScript hooks and APIs, Squish allows adding dedicated support for any custom item view DHTML/AJAX/JS widget and to provide test scripts access to the widget's and item's states, properties and functions.

This chapter explains how to implement such dedicated item view support for record and replay using Squish JavaScript extension API. As an example AJAX widget we will use the tree control from Google's Web Toolkit (GWT) here. We will implement the necessary support to properly record and replay clicks on items and item handles, to identify the tree and items independent of the DOM and to allow querying the selection.

Once the necessary hooks are implemented, clickItem() and clickTreeHande() calls will be recorded when interacting with the supported item view widget. In addition using the item view and item abstraction API the test scripts will be able to access the widget and work with its items, states and properties.

16.14.4.1. DOM structure of the widget

Before we can start implementing the necessary support, we need to look at the internal DOM structure of the widget since our custom support will use the Web application's DOM internally to provide the abstraction and encapsulation to the test script.

Below you find a screenshot of the GWT Tree widget as it appears in a Web browser:

The respective DOM hierarchy representing this tree widget has the following structure (simplified to only display relevant details):

<DIV class="gwt-Tree">
    <TABLE>
        <TR>
          <TD">
            <IMG src="tree_open.gif"/>
          </TD>
          <TD>
            <SPAN class="gwt-TreeItem">Beethoven</SPAN>
          </TD>
        </TR>
    </TABLE>
    <SPAN>
      <DIV>
        <TABLE>
            <TR>
              <TD>
                <IMG src="tree_closed.gif"/>
              </TD>
              <TD>
                <SPAN class="gwt-TreeItem gwt-TreeItem-selected">Concertos</SPAN>
              </TD>
            </TR>
          </TBODY>
        </TABLE>
      </DIV>
      <DIV>
        <TABLE>
            <TR>
              <TD>
                <IMG src="tree_closed.gif"/>
              </TD>
              <TD>
                <SPAN class="gwt-TreeItem">Quartets</SPAN>
              </TD>
            </TR>
          </TBODY>
        </TABLE>
      </DIV>
    </SPAN>
    ....
</DIV>

So we can see that a tree widget is represented using a DIV element with the class name gwt-Tree.

An item in the tree is contained in a TABLE inside a DIV element. The actual item is a SPAN element of the class gwt-TreeItem. The item text is the inner text of this element. If an item is selected, its class name also contains gwt-TreeItem-selected.

The item handle (the + or - symbol in front of the item) is represented by an IMG element in the TD element above (it's the item's parent's previous sibling's first child). Using the src property of the IMG it is possible to find out if the item is expanded (src=tree_open.gif) or collapsed src=tree_closed.gif).

The relationship between the items (the actual tree structure) is modeled using nested SPAN elements which contain a set of siblings relative to the parent.

Using this information it is possible to detect a GWT tree and its items and handles in the DOM structure of a web page.

16.14.4.2. Hooks for recording high-level GUI operations

[Note]Note

Function to implement in the toolkit's extension object

  • eventObject(object)

  • typeOf(object)

  • nameOf(object)

The first step is to implement the JavaScript hooks for recording high-level GUI operations on the GWT tree. This means, if the user clicks an item, a clickItem("<treename>", "<itemname>") function call should be recorded. Similarly, a click on an tree item's handle should be recorded as clickTreeHandle("<treename>", "<itemname>").

For this, we need to implement three different hooks:

  • Event object: A function which returns the real DOM element for the source element of an event.

  • Type of object: A function which returns the high-level type name (as a string) for a DOM object.

  • Name of object: A function which returns the Squish object name (as a string) for a DOM object.

Once we implemented these function we need to register them with Squish so our functions will be called from the event recorder. When Squish calls them, it will pass the DOM source object of the event as an argument. Based on that we decide whether we will react or not.

In the implementations of these functions we will handle three different high-level objects:

  • The tree object

  • A tree item

  • A tree item's handle

So for all three functions we need a way to determine the high-level type. First we declare constants for the types we handle. We will define all extension functions and constants in our own extension object gwtExtension. This way all the toolkit's extension code will be cleanly encapsulatd in its own object:

var gwtExtension = new Object;

gwtExtension.Tree = 0;
gwtExtension.TreeItem = 1;
gwtExtension.TreeItemHandle = 2;

Now we implement a function which returns the high-level type for a specified DOM object:

gwtExtension.getType = function(o)
{
    if (Squish.hasClassName(o, 'gwt-TreeItem'))
	return gwtExtension.TreeItem;
    else if (o.tagName == 'IMG' && (o.src.indexOf('tree_open') != -1 || 
				    o.src.indexOf('tree_closed') != -1) && 
	     Squish.hasClassName(gwttreeExtension.itemOfHandle(o), 'gwt-TreeItem'))
	return gwtExtension.TreeItemHandle;
    else if (Squish.hasClassName(o, 'gwt-Tree'))
	return gwtExtension.Tree;

    return undefined;
}
  • gwtExtension.TreeItem: We return this value if the object's class contains gwt-TreeItem.

  • gwtExtension.TreeItemHandle: We return this value if the object is the image displaying the + (open) or - (closed) symbol and if there is a tree item which correlates to this image. To determine the correlation, we implement a gwttreeExtension.itemOfHandle function which looks at the object's parent's next sibling's first child so we can check if this is a GWT tree item.

  • gwtExtension.Tree: We return this value if the object's class contains gwt-Tree.

Using that function it is quite simple to implement the three mentioned hook functions:

gwtExtension.eventObject = function (o)
{
    if (!o)
        return undefined;

    switch (gwtExtension.getType(o)) {
    case gwtExtension.TreeItem:
    case gwtExtension.TreeItemHandle:
	return o;
    }

    return undefined;
}

If the high-level object type is a tree item or handle, we return this object. This tells Squish that this is an object we want to record events on and which should not be ignored by the event recorder.

If Squish comes across such an object which events should be recorded on, it will need to find out the type of this object. For that we implement the next hook:

gwtExtension.typeOf = function (o)
{
    switch (gwtExtension.getType(o)) {
    case gwtExtension.TreeItem:
	return 'custom_item_gwttree';
    case gwtExtension.TreeItemHandle:
	return 'custom_itemhandle_gwttree';
    case gwtExtension.Tree:
	return 'custom_itemview_gwttree';
    }

    return undefined;
}

The type names returned from this function follow a convention which needs to be adhered to. Squish provides a common item view abstraction. Such an item view consists of three main components. To denote which of the components the DOM object represents, the returned type name has to start with a defined prefix:

  • custom_item_: Item in the view

  • custom_itemhandle_: Handle of an item in the view

  • custom_itemview_: The view widget which contains the items

Following the defined prefix we need to add the specific type name which we use for the item view which we support. In our case this is gwttree.

The third hook function we need to implement returns the name of a specific high-level object:

gwtExtension.nameOf = function (o)
{
    switch (gwtExtension.getType(o)) {
    case gwtExtension.TreeItem:
	return escape(gwtExtension.nameOfTreeItem(o));
    case gwtExtension.TreeItemHandle:
	return escape(gwtExtension.nameOfTreeItem(gwttreeExtension.itemOfHandle(o)));
    case gwtExtension.Tree:
	return Squish.uniquifyName(gwtExtension.nameOfTree(o), o);
    }
	
    return undefined;
}

In this function we use the functions gwtExtension.nameOfTree() and gwtExtension.nameOfTreeItem() to create names for the respective objects. Here are the implementations:

gwtExtension.nameOfTree = function (o)
{
    return '{' + Squish.propertiesToName(o, ["tagName", "id", "name"]) + "className='gwt-Tree'}";
}

This function creates a unique name for the tree object. More about how names are built can be read in Implementing a Custom Name Generator (Section 16.14.3.1).

Here is the function which generates a name for a tree item:

gwtExtension.nameOfTreeItem = function (o)
{
    var tree = o;
    while (tree && (!Squish.hasClassName(tree, 'gwt-Tree')))
	tree = tree.parentNode;
    var treeName = Squish.nameOf(tree); 

    var item = o;

    return Squish.createItemName(treeName, item.innerText);
}

An item's name consists of the name of the item view containing the item (the tree widget) and the actual name of the item (usually the item's text (innerText) is used).

So first we have to determine the tree widget. This can be done by walking up the parents until we get an object of class gwt-Tree. Once we have located the tree object, we can call Squish.nameOf() to get its name.

For the item's name we use its innerText.

To build a name in the correct format, we can use the convenience function Squish.createItemName() which expects the name of the item view's (tree) as first argument and the item's name as second argument.

Now that we have implemented all necessary hook functions we need to register them:

Squish.addNameOfHook(gwtExtension.nameOf);
Squish.addTypeOfHook(gwtExtension.typeOf);
Squish.addEventObjectHook(gwtExtension.eventObject);

If we now record events on a GWT tree, the expected high-level operations will be recorded.

16.14.4.3. Hooks for replaying high-level GUI operations

[Note]Note

Function to implement in the view's extension object

  • findItem(view, name)

  • itemHandle(item)

Once the recording hooks are implemented and recording works, the next step is to implement the hooks to enable the automated script replay of the recorded GUI operations.

To support replaying clicks on items (clickItem()) only one additional function needs to be implemented. This is a function which needs to search for an item by its name in the tree and returns a reference to the DOM object representing the item. Squish will use this function to get the item to send a click event to.

The name of this function needs to follow a certain naming convention. The function has to be called findItem and it has to exist in an object of the name <type>Extension. <type> is exactly what we appended to the type names of the objects in our typeOf() hook function. So in our case this is gwttree which means that we need to implement a function called gwttreeExtension.findItem which takes a reference to the DOM object representing the tree and the name of the requested item as arguments:

gwttreeExtension.findItem = function (tree, name)
{
    var node = tree.firstChild;
    
    while (node) {
	if (node.firstChild) {
	    var n2 = gwttreeExtension.findItem(node, name);
	    if (n2) {
		return n2;
	    }
	}
	if (node.className && 
	    Squish.hasClassName(node, 'gwt-TreeItem') &&
	    Squish.cleanString(node.innerText) == name) {
	    return node;
	}
	node = node.nextSibling;
    }

    return undefined;
}

The function iterates through all child elements of the tree until it finds an element of class gwt-TreeItem whose innerText matches the requested item text.

The other available function is required if items have handles to expand and collapse an item. Therefore this function is only necessary when implementing support for a tree widget.

.

For this purpose a itemHandle() function needs to be implemented in the same object which returns the handle belonging to the given item. For GWT tree items this looks like this:

gwttreeExtension.itemHandle = function (node)
{
    return node.parentNode.previousSibling.firstChild;
}

That's already all that is needed to enable replaying of high-level operations on items and item handles.

16.14.4.4. Exposing item view API

[Note]Note

Function to implement in the view's extension object

  • itemText(item)

  • isItemSelected(item)

  • isItemOpen(item)

  • setItemSelected(item, selected)

  • setItemOpen(item, open)

  • childItem(itemOrView)

  • parentItem(item)

  • nextSibling(item)

  • itemView(item)

  • numColumns(view)

  • columnCaption(view, column)

To allow Squish to properly detect an item view and its items to be able to record and replay high-level GUI operations, nothing more is needed. But Squish's item view abstraction also provides the tester with APIs to iterate through the items and query and set the selection and other properties. To allow that, some more functions need to be implemented which will be discussed in this section.

16.14.4.4.1. Item text

Squish's test script item-view API allows retrieving a tree item (using tree.findItem(...) and querying the text property on the item object.

By default, Squish returns the innerText of the item as its text. If this does not provide an adequate represenation you can implement a itemText(i) function in the view's extension object which will then be called instead:

gwttreeExtension.itemText = function (i)
{
    return i.innerText;
}

If existent, Squish will call this function and pass a reference to the DOM object representing the desired item. In our case (for GWT tree) this wouldn't be necessary since Squish would return the innerText by default.

16.14.4.4.2. Item selection

Squish's test script item-view API allows to get an item of a tree (using tree.findItem(...) and query and modify the selected property on the item object.

To allow querying the selection state of an item, a isItemSelected functions needs to be implemented in the tree's extension object. For the GWT tree this looks like the following:

gwttreeExtension.isItemSelected = function (i)
{
    return Squish.hasClassName(i, "gwt-TreeItem-selected");
}

If existent, Squish will call this function and pass a reference to the DOM object representing the desired item to it. Our implementation simply checks is the item is selected by checking the DOM class name and returning true or false respectively.

If you also want to allow the test script to change the selection state of an item, you can implement a setItemSelected(item, selected) function in the same extension object. item is the reference to the item's DOM object and selected is a boolean value specifying the desired selection state.

16.14.4.4.3. Item handle's state

Squish's item view API which can be used in test script allows to get access to the item's handle and to query if the item is opened or closed using the opened property. This of course only makes sense for hierarchical item views such as tree widgets.

To allow querying this state of an item, a isItemOpened functions needs to be implemented in the tree's extension object. For the GWT tree this looks like the following:

gwttreeExtension.isItemOpen = function (i)
{
    return gwttreeExtension.itemHandle(i).src.indexOf('tree_open') != -1;
}

If existent, Squish will call this function and pass a reference to the DOM object representing the desired item to it. Our implementation simply gets the handle of the item and checks whether the handle displays the opened or closed image to return true or false respectively.

If you also want to allow the test script to change the open state of an item, you can implement a setItemOpen(item, open) function in the same extension object. item is the reference to the item's DOM object and open is a boolean value specifying the desired state.

16.14.4.4.4. Item traversal

The item view API also provides the scripter with a childItem() function on the tree and childItem(), nextSibling() and parentItem() functions on the item. Additionally, the item API offers a itemView() function to allow accessing the view of the item.

Using these APIs it is possible to iterate over the items for any kind of operation. To support this, functions named childItem(), nextSibling(), parentItem() and itemView() need to be implemented in the view's extension object. When Squish calls any of these functions, a reference to the context item is passed into the function as argument. One exception here is childItem() which might also get a reference of the tree object passed to it to get the first item of the tree.

Here are the implementations of these function for the GWT tree:

gwttreeExtension.childItem = function (parent)
{
    if (Squish.hasClassName(parent, "gwt-Tree")) {
	return Squish.getElementByClassName(parent, 'gwt-TreeItem', 'SPAN');
    } else {
	while (parent && parent.tagName != 'TABLE')
	    parent = parent.parentNode;
	if (!parent || !parent.nextSibling)
	    return undefined;
	parent = parent.nextSibling;
	return Squish.getElementByClassName(parent, 'gwt-TreeItem', 'SPAN');
    }
}

gwttreeExtension.nextSibling = function (node)
{
    while (node && node.tagName != 'DIV')
	    node = node.parentNode;
    if (!node || !node.nextSibling)
	return undefined;
    node = node.nextSibling;
    return Squish.getElementByClassName(node, 'gwt-TreeItem', 'SPAN');
}

gwttreeExtension.parentItem = function (node)
{
    while (node && node.tagName != 'DIV')
	    node = node.parentNode;
    if (!node || !node.parentNode || !node.parentNode.previousSibling)
	return undefined;
    node = node.parentNode.previousSibling;
    return Squish.getElementByClassName(node, 'gwt-TreeItem', 'SPAN');
}

gwttreeExtension.itemView = function (node)
{
    var t = node;
    while (t && (!Squish.hasClassName(t, 'gwt-Tree')))
	t = t.parentNode;
    return t;
}

The implementations are quite self-explanatory. Looking at the DOM structure of the tree it is rather obvious how to implement these functions.

16.14.4.4.5. Columns

If the item view supports multiple columns, the functions columnCaption(view, column) and numColumns(view) can be implemented in the view's extension object to allow the test script to query the column information using the equally named API calls through the item view API.

16.14.4.5. Testing and conclusion

This concludes this section. A few tests scripts which use the provided high-level API and item view abstractions on a GWT tree widget can be found in examples/web/suite_gwt.