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.
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.
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).
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.
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'}.
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);
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.
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.
![]() | Note |
|---|---|
Function to implement in the toolkit's extension 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.
![]() | Note |
|---|---|
Function to implement in the view's extension object
|
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.
![]() | Note |
|---|---|
Function to implement in the view's extension object
|
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.
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.
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.
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.
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.