How to Test Web Applications

Squish's Web-specific API enables you to find and query objects, access properties and methods, and evaluate arbitrary JavaScript code in the Web-application's context:

In addition, the Web convenience API provides functions for executing common actions on Web sites, such as clicking a button or entering some text.

For examples of how to use the scripting Web API to access and test complex Web elements, see How to Test Web Elements.

Note: There are two ways to work with web applications: directly, or using the web proxy mechanism. For testing web applications with Safari on macOS, Microsoft Internet Explorer on Windows, or Firefox on Unix it is best not to use the proxy mechanism since the mechanism imposes a few limitations. For more about how to use the web proxy mechanism see Web Proxy.

How to Find and Query Web Objects

Squish provides two functions—Object findObject(objectName) and Object waitForObject(objectOrName)—that return a reference to the object (HTML or DOM element), for a given qualified object name. The difference between them is that Object waitForObject(objectOrName) waits for an object to become available (up to its default timeout, or up to a specified timeout), so it is usually the most convenient one to use. However, only Object findObject(objectName) can be used on hidden objects.

See the Web Object API for full details of Squish's Web classes and methods.

There are several ways to indentify a particular Web object:

  • Multiple-property (real) names—These names consist of a list of one or more property–name/property–value pairs, separated by spaces if there is more than one, and the whole name enclosed in curly braces. Given a name of this kind, Squish will search the document's DOM tree until it finds a matching object. An example of such a name is: "{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}".
  • Single property value—Given a particular value, Squish will search the document's DOM tree until it finds an object whose id, name or innerText property has the specified value.
  • Path—The full path to the element is given. An example of such a path is "DOCUMENT.HTML1.BODY1.FORM1.SELECT1".

To find an object's name, you can use the Spy to introspect the Web application's document. See the How to Use the Spy section for details.

If we want to interact with a particular object—for example, to check its properties, or to do something to it, such as click it, we must start by getting a reference to the object.

If we use the Object findObject(objectName) function, it will either return immediately with the object, or it will throw a catchable exception if the object isn't available. (An object might not be available because it is an AJAX object that only appears under certain conditions, or it might only appear as the result of some JavaScript code executing, and so on.) Here's a code snippet that shows how to use Object findObject(objectName) without risking an error being thrown, by using the Boolean object.exists(objectName) function:

radioName = ("{tagName='INPUT' id='r1' name='rg' form='myform' " +
        "type='radio' value='Radio 1'}")
if object.exists(radioName):
    radioButton = findObject(radioName)
    clickButton(radioButton)
var radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' " +
        "type='radio' value='Radio 1'}";
if (object.exists(radioName)) {
    var radioButton = findObject(radioName);
    clickButton(radioButton);
}
my $radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' " .
        "type='radio' value='Radio 1'}"
if (object::exists($radioName)) {
    my $radioButton = findObject($radioName);
    clickButton($radioButton);
}
radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' " +
        "type='radio' value='Radio 1'}"
if Squish::Object.exists(radioName)
    radioButton = findObject(radioName)
    clickButton(radioButton)
end
set radioName {{tagName='INPUT' id='r1' name='rg' form='myform' \
        type='radio' value='Radio 1'}}
if {[object exists $radioName]} {
    set radioButton [findObject $radioName]
    invoke clickButton $radioButton
}

This will only click the radio button if it exists, that is, if it is accessible at the time of the Boolean object.exists(objectName) call.

An alternative approach is to use the Object waitForObject(objectOrName) function:

radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
clickButton(radioButton)
var radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}");
clickButton(radioButton);
my $radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " .
        "form='myform' type='radio' value='Radio 1'}");
clickButton($radioButton);
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
clickButton(radioButton)
set radioButton [waitForObject {{tagName='INPUT' id='r1' name='rg' \
        form='myform' type='radio' value='Radio 1'}}]
invoke clickButton $radioButton

This will wait up to 20 seconds (or whatever the default timeout has been set to), and providing the radio button becomes accessible within that time, it is clicked.

Using the Object findObject(objectName) and Object waitForObject(objectOrName) functions in conjunction with appropriate object identifiers means that we can access all the elements in a Web document's object tree, and test their properties, and generally interact with them.

How to Use XPath

For every object returned by the Object findObject(objectName) and Object waitForObject(objectOrName) functions, it is possible the evaluate an XPath statement. The object on which the XPath statement is evaluated is used as the context node.

For example, to retrieve the reference to a link referring to the URL www.froglogic.com which is a child of the DIV element with the id "mydiv", we can use the following code:

div = findObject("{tagName='DIV' id='mydiv'}")
link = div.evaluateXPath("A[contains(@href," +
        "'www.froglogic.com')]").snapshotItem(0)
var div = findObject("{tagName='DIV' id='mydiv'}");
var link = div.evaluateXPath("A[contains(@href," +
        "'www.froglogic.com')]").snapshotItem(0);
my $div = findObject("{tagName='DIV' id='mydiv'}");
my $link = $div->evaluateXPath("A[contains(@href," .
        "'www.froglogic.com')]")->snapshotItem(0);
div = findObject("{tagName='DIV' id='mydiv'}")
link = div.evaluateXPath("A[contains(@href," +
        "'www.froglogic.com')]").snapshotItem(0)
set div [findObject {{tagName='DIV' id='mydiv'}}]
set link [invoke [invoke $div evaluateXPath \
        "A[contains(@href, 'www.froglogic.com')]"] snapshotItem 0]

The XPath used here says, "find all A (anchor) tags that have an href attribute, and whose value is www.froglogic.com". We then call the snapshotItem method and ask it to retrieve the first match—it uses 0-based indexing—which is returned as an object of type HTML_Object Class.

Each XPath query can produce a boolean (true or false), a number, a string, or a group of elements as the result. Consequently, the HTML_XPathResult HTML_Object.evaluateXPath(statement) method returns an object of type HTML_XPathResult Class on which you can query the result of the XPath evaluation.

How to Access Table Cell Contents has an example of using the HTML_XPathResult HTML_Object.evaluateXPath(statement) method to extract the contents of an HTML table's cell.

For more information about how you can create XPath queries to help produce flexible and compact test scripts, see XPath Tutorial from the W3Schools Online Web Tutorials website.

See also the Squish for Web tutorial Inserting Additional Verification Points.

How to Access Web Object Properties

Using the script API it is possible to access most of the DOM properties for any HTML or DOM element in a Web application. See the Web Object API for full details of Squish's Web classes and methods.

Here is an example where we will change and query the value property of a form's text element.

entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}")
entry.value = "Some new text"
test.log(entry.value)
var entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}");
entry.value = "Some new text";
test.log(entry.value);
my $entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}");
$entry->value = "Some new text";
test::log($entry->value);
entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}")
entry.value = "Some new text"
Test.log(entry.value)
set entry [waitForObject {{tagName='INPUT' id='input' \
        form='myform' type='text'}}]
[property set $entry value "Some new text"]
test log [property get $entry value]

Squish provides similar script bindings to all of the standard DOM elements' standard properties. But it is also possible to access the properties of custom objects using the property method. For example, to check a DIV element's offset width, we can write code like this:

div = findObject("DOCUMENT.HTML1.BODY1......DIV")
test.compare(div.property("offsetWidth"), 18)
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test.compare(div.property("offsetWidth"), 18);
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test::compare($div->property("offsetWidth"), 18);
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
Test.compare(div.property("offsetWidth"), 18)
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
test compare [invoke $div property "offsetWidth"] 18

Note that for hidden elements we must always use the Object findObject(objectName) function rather than the Object waitForObject(objectOrName) function.

How to Call Web Object Functions

In addition to properties, you can call standard DOM functions on all Web objects from test scripts, using the API described in the Web Object API.

For example, to get the first child node of a DIV element, you could use the following test script which makes use of the HTML_Object HTML_Object.firstChild() function:

div = findObject("DOCUMENT.HTML1.BODY1......DIV")
child = div.firstChild()
test.log(child.tagName)
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
var child = div.firstChild();
test.log(child.tagName);
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
my $child = $div->firstChild();
test::log($child->tagName);
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
child = div.firstChild()
Test.log(child.tagName)
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
set child [invoke $div firstChild]
test log [property get $child tagName]

Or, to get the text of the selected option from a select form element, we could use the following code:

element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
option = element.optionAt(element.selectedIndex)
test.log(option.text)
var element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
var option = element.optionAt(element.selectedIndex);
test.log(option.text);
my $element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
my $option = $element->optionAt($element->selectedIndex);
test::log($option->text);
element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
option = element.optionAt(element.selectedIndex)
Test.log(option.text)
set element [findObject ":{tagName='INPUT' id='sel' \
        form='myform' type='select-one'}"]
set option [invoke $element optionAt [property get element selectedIndex]]
test log [property get $option text]

Squish provides script bindings like those shown here to all the standard DOM elements' standard functions. And in addition, it is also possible to call custom functions via a generic invoke function. For example, to call a custom customFunction function with string argument "an argument", on a DIV element, we could write code like this:

div = findObject("DOCUMENT.HTML1.BODY1......DIV")
div.invoke("customFunction", "an argument")
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
div.invoke("customFunction", "an argument");
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
$div->invoke("customFunction", "an argument");
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
div.invoke("customFunction", "an argument")
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
invoke $div "customFunction" "an argument"

Beyond the DOM API bindings and the invoke function, Squish offers a Browser object which can be used by test scripts to query which browser is being used, as the following Python snippet shows:

# This will print out the name of the browser:
test.log("We are running in " + Browser.name())
if Browser.id() == InternetExplorer:
    ...
elif Browser.id() == Mozilla:
    ...
elif Browser.id() == Firefox:
    ...
elif Browser.id() == Safari:
    ...

How to Use evalJS

In addition to test scripts being able to access all the properties and methods of DOM elements, it is also possible to let Squish execute arbitrary JavaScript code in the Web browser's JavaScript interpreter and to retrieve the results. For this purpose, Squish provides the Object evalJS(browserTab, code) function. Here is an example of its use:

style_display = evalJS("var d = document.getElementById(" +
        "'busyDIV'); d ? d.style.display : ''")
var style_display = evalJS("var d = document.getElementById(" +
        "'busyDIV'); d ? d.style.display : ''");
my $style_display = evalJS("var d = document.getElementById(" .
        "'busyDIV'); d ? d.style.display : ''");
style_display = evalJS("var d = document.getElementById(" +
        "'busyDIV'); d ? d.style.display : ''")
set style_display [invoke evalJS "var d = document.getElementById(\
        'busyDIV'); d ? d.style.display : ''"]

The Object evalJS(browserTab, code) function returns the result of the last statement executed—in this case the last statement is d ? d.style.display : '' so if the document contains an element with ID "busyDIV", style_display will be set to that element's style.display property's value—otherwise it will be set to an empty string.

How to Use retrieveJSObject

In addition to test scripts being able to run some JavaScript snippet and retrieve the string value of the result, Squish also can retrieve references to the actual JavaScript objects from the Web browser's interpreter. This is useful in cases where a JavaScript function does not return a simple value, like a string or number, but instead returns an object itself. In such a case, the reference allows retrieving properties from that object or calling methods on that object. For this purpose, Squish provides the JsObject retrieveJSObject(javascriptcode) function. Here is an example of its use:

jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;")
var jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;");
my $jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;");
jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;")
set jsobject [invoke retrieveJSObject {var globalObject = \{ 'id': 'obj1', 'name': function() \{ return 'name1'; \} \};globalObject;\} }]

The JsObject retrieveJSObject(javascriptcode) function returns the result of the last statement executed—in this case the last statement is globalObject; so a reference to the just created globalObject is returned. Now its possible to fetch the id property of that object or call the name function. Here is an example of logging both in the test results

test.log("id: " + jsobject.property("id"))
test.log("name: " + jsobject.call("name"))
test.log("id: " + jsobject.property("id"));
test.log("name: " + jsobject.call("name"));
test::log("id: " . $jsobject->property("id"));
test::log("name: " . $jsobject->call("name"));
test.log("id: " + jsobject.property("id"))
test.log("name: " + jsobject.call("name"))
test log "id: " [invoke $jsobject property "id"]
test log "name: " [invoke $jsobject call "name"]

How to Use the Web Convenience API

This section describes the script API Squish offers on top of the DOM API to make it easy to perform common user actions such as clicking a link, entering text, etc. All the functions provided by the API are listed in the Web Object API section in the Tools Reference. Here we will show a few examples to illustrate how the API is used.

In the example below, we click a link, select an option, and enter some text.

clickLink(":{tagName='A' innerText='Advanced Search'}")
selectOption(":{tagName='INPUT' id='sel' form='myform' " +
        "type='select-one'}", "Banana")
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text")
clickLink(":{tagName='A' innerText='Advanced Search'}");
selectOption(":{tagName='INPUT' id='sel' form='myform' " +
        "type='select-one'}", "Banana");
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text");
clickLink(":{tagName='A' innerText='Advanced Search'}");
selectOption(":{tagName='INPUT' id='sel' form='myform' " .
        "type='select-one'}", "Banana");
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text");
clickLink(":{tagName='A' innerText='Advanced Search'}")
selectOption(":{tagName='INPUT' id='sel' form='myform' " +
        "type='select-one'}", "Banana")
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text")
invoke clickLink ":{tagName='A' innerText='Advanced Search'}"
invoke selectOption ":{tagName='INPUT' id='sel' form='myform' \
        type='select-one'}" "Banana"
invoke setText ":{tagName='INPUT' id='input' form='myform' \
        type='text'}" "Some Text"

In these cases we identified the object using real (multi-property) names; we could just have easily used symbolic names, or even object references, instead. Note also that the full API contains far more functions than the three mentioned here (clickLink(objectOrName), selectOption(objectOrName, text), and setText(objectOrName, text)), although all of them are just as easy to use.

How to Synchronize Web Page Loading for Testing

In many simple cases, just waiting for a particular object to become available using the Object waitForObject(objectOrName) function is sufficient.

However, in some cases we need to ensure that the page has loaded before we attempt to access its objects. The special Boolean isPageLoaded(browserTab) function makes it possible to synchronize a test script with a Web application's page loaded status.

We could use this function to wait for a Web page to be fully loaded before clicking a particular button on the page. For example, if a page has a Login button, we could ensure that the page is loaded before attempting to click the button, using the following code:

loaded = waitFor("isPageLoaded()", 5000)
if loaded:
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"))
else:
    test.fatal("Page loading failed")
var loaded = waitFor("isPageLoaded()", 5000);
if (loaded)
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"));
else
    test.fatal("Page loading failed");
my $loaded = waitFor("isPageLoaded()", 5000);
if ($loaded) {
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"));
}
else {
    test::fatal("Page loading failed");
}
loaded = waitFor("isPageLoaded", 5000)
if loaded
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"))
else
    Test.fatal("Page loading failed")
end
set loaded [waitFor {invoke isPageLoaded} 5000]
if {$loaded} {
    invoke clickButton [invoke waitForObject \
        ":{tagName='INPUT' type='button' value='Login'}"]
} else {
    test fatal "Page loading failed"
}

It is necessary to use the Boolean isPageLoaded(browserTab) function to ensure that the page is loaded and its web objects are potentially accessible. To access a particular object we must still use the Object waitForObject(objectOrName) function—and we may even have to specify a longer timeout than the default 20 000 milliseconds to allow for network latency.

How to Test Web Elements

In this section we will cover how to test specific HTML elements in a Web application. This will allow us to verify that elements have properties with the values we expect and that form elements have their expected contents.

One aspect of testing that can be quite challenging is the creation of test verifications. As shown in the section Inserting Additional Verification Points in Tutorial: Starting to Test Web Applications, most of this can be done using the Spy and its point & click interface. But in some cases it is actually more convenient—and more flexibile—to implement verification points directly in code.

To test and verify a widget and its properties or contents in code, first we need access to the widget in the test script. To obtain a reference to the widget, the Object waitForObject(objectOrName) function is used. This function finds the widget with the given name and returns a reference to it. For this purpose we need to know the name of the widget we want to test, and we can get the name using the Spy tool (see How to Use the Spy) and adding the object to the Object Map (so that Squish will remember it) and then copying the object's name (preferably its symbolic name) to the clipboard ready to be pasted into our test. If we need to gather the names of lots of widgets it is probably faster and easier to record a dummy test during which we make sure that we access every widget we want to verify in our manually written test script. This will cause Squish to add all the relevant names to the Object Map, which we can then copy and paste into our code.

How to Test the State of Web Elements

One of the most common test requirements is to verify that a particular element is enabled or disabled at some point during the test run. This verification is easily made by checking an element's disabled property.

entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
test.verify(not entry.disabled)
var entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}");
test.verify(!entry.disabled);
my $entry = waitForObject("{tagName='INPUT' id='input' " .
        "form='myform' type='text'}");
test::verify(!$entry->disabled);
entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
Test.verify(!entry.disabled)
set entry [waitForObject "{tagName='INPUT' id='input' \
        form='myform' type='text'}"]
test verify [expr ![property get $entry disabled]]

Here we have verified that a text entry element is enabled (i.e., that its disabled property is false). To check that the element is disabled, we would eliminate the negation (not or ! depending on language).

Form Checkboxes and Radiobuttons

To verify that a radiobutton or checkbox is checked, we just need to query its checked property.

radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
test.verify(radiobutton.checked)
var radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}");
test.verify(radiobutton.checked);
my $radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " .
        "form='myform' type='radio' value='Radio 1'}");
test::verify($radiobutton->checked);
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
Test.verify(radiobutton.checked)
set radiobutton [waitForObject ":{tagName='INPUT' id='r1' name='rg' \
        form='myform' type='radio' value='Radio 1'}"]
test verify [property get $radiobutton checked]

The coding pattern shown here—get a reference to an object, then verify the value of one of its properties—is very common and can be applied to any element.

Form Text fields

Both the text and textarea form elements have a value property, so it is easy to check what they contain.

entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
test.compare(entry.value, "Ternary")
var entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}");
test.compare(entry.value, "Ternary");
my $entry = waitForObject("{tagName='INPUT' id='input' " .
        "form='myform' type='text'}");
test::compare($entry->value, "Ternary");
entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
Test.compare(entry.value, "Ternary")
set entry [waitForObject "{tagName='INPUT' id='input' \
        form='myform' type='text'}"]
test compare [property get $entry value] "Ternary"

This follows exactly the same pattern as we used for the earlier examples.

Form Selection Boxes

Web forms usually present single selection lists (of element type select-one) in comboboxes and multiple selection lists (of element type select) in listboxes. We can easily check which items are selected.

selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select-one'}")
test.compare(selection.selectedIndex, 2)
test.compare(selection.selectedOption, "Cavalier")
var selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select-one'}");
test.compare(selection.selectedIndex, 2);
test.compare(selection.selectedOption, "Cavalier");
my $selection = waitForObject(":{tagName='INPUT' id='sel' " .
        "form='myform' type='select-one'}");
test::compare($selection->selectedIndex, 2);
test::compare($selection->selectedOption, "Cavalier");
selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select-one'}")
Test.compare(selection.selectedIndex, 2)
Test.compare(selection.selectedOption, "Cavalier")
set selection [waitForObject ":{tagName='INPUT' id='sel' \
        form='myform' type='select-one'}"]
test compare [property get $selection selectedIndex] 2
test compare [property get $selection selectedOption] "Cavalier"

Here we retrieve the selected item from a single selection list box and verify that the third item (the item at index position 2), is selected, and that it has the text "Cavalier".

selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select'}")
test.verify(selection.optionAt(0).selected)
test.verify(not selection.optionAt(1).selected)
test.verify(selection.optionAt(2).selected)
test.compare(selection.optionAt(1).text, "Round Head")
var selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select'}");
test.verify(selection.optionAt(0).selected);
test.verify(!selection.optionAt(1).selected);
test.verify(selection.optionAt(2).selected);
test.compare(selection.optionAt(1).text, "Round Head");
my $selection = waitForObject(":{tagName='INPUT' id='sel' " .
        "form='myform' type='select'}");
test::verify($selection->optionAt(0)->selected);
test::verify(!$selection->optionAt(1)->selected);
test::verify($selection->optionAt(2)->selected);
test::compare($selection->optionAt(1)->text, "Round Head");
selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select'}")
Test.verify(selection.optionAt(0).selected)
Test.verify(!selection.optionAt(1).selected)
Test.verify(selection.optionAt(2).selected)
Test.compare(selection.optionAt(1).text, "Round Head")
set selection [waitForObject ":{tagName='INPUT' id='sel' \
        form='myform' type='select'}"]
test verify [property get [invoke selection optionAt 0] selected]
test verify [expr ![property get [invoke selection optionAt 1] selected]]
test verify [property get [invoke selection optionAt 2] selected]
test.compare [property get [invoke selection optionAt 1] text] \
        "Round Head"

In this example, we retrieve a reference to a mulitple selection list—normally represented by a listbox—and then retrieve its option items. We then verify that the first option (at index position 0) is selected, that the second option (at index position 1) is not selected, and that the third option (at index position 2) is selected. We also verify the second option's text is "Round Head".

See also the HTML_Select Class class, its HTML_Option HTML_Select.optionAt(index) function, and its text and selected properties.

How to Access Table Cell Contents

Another common requirement when testing Web applications is to retrieve the text contents of particular cells in HTML tables. This is actually very easy to do with Squish.

All HTML elements retrieved with the Object findObject(objectName) function and the Object waitForObject(objectOrName) function have an HTML_XPathResult HTML_Object.evaluateXPath(statement) method that can be used to query the HTML element, and which returns the results of the query. We can make use of this to create a generic custom getCellText function that will do the job we want. Here's an example implementation:

def getCellText(tableObject, row, column):
    return tableObject.evaluateXPath("TBODY/TR[%d]/TD[%d]" % (
        row + 1, column + 1)).stringValue
function getCellText(tableObject, row, column)
{
    return tableObject.evaluateXPath("TBODY/TR[" + (row + 1) +
        "]/TD[" + (column + 1) + "]").stringValue;
}
sub getCellText
{
    my ($tableObject, $row, $column) = @_;
    ++$row;
    ++$column;
    return $tableObject->evaluateXPath(
        "TBODY/TR[$row]/TD[$column]")->stringValue;
}
def getCellText(tableObject, row, column)
    tableObject.evaluateXPath(
        "TBODY/TR[#{row + 1}]/TD[#{column + 1}]").stringValue
end
proc getCellText {tableObject row column} {
    incr row
    incr column
    set argument "TBODY/TR[$row]/TD[$column]"
    return [property get [invoke $tableObject \
        evaluateXPath $argument] stringValue]
}

An XPath is kind of like a file path in that each component is separated by a /. The XPath used here says, "find every TBODY tag, and inside each one find the row-th TR tag, and inside that find the column-th TD tag". The result is always an object of type HTML_XPathResult Class; here we return the result query as a single string value using the result's stringValue property. (So if there was more than one TBODY tag in the document that had a cell at the row and column we wanted, we'd actually get the text of all of them.) We must add 1 to the row and to the column because XPath queries use 1-based indexing, but we prefer our functions to have 0-based indexing since that is the kind used by all the scripting languages that Squish supports. The function can be used like this:

table = waitForObject(htmlTableName)
text = getCellText(table, 23, 11)
var table = waitForObject(htmlTableName);
var text = getCellText(table, 23, 11);
my $table = waitForObject($htmlTableName);
my $text = getCellText($table, 23, 11);
table = waitForObject(htmlTableName)
text = getCellText(table, 23, 11)
set table [waitForObject $htmlTableName]
set text [getCellText $table 23 11]

This code will return the text from the cell at the 22nd row and 10th column of the HTML table whose name is in the htmlTableName variable.

Squish's XPath functionality is covered in How to Use XPath.

Non-Form Elements and Synchronization

Of course it is also possible to verify the states and contents of any other element in a Web application's DOM tree.

For example, we might want to verify that a table with the ID result_table contains the text—somewhere in the table, we don't care where—"Total: 387.92".

table = waitForObject("{tagName='TABLE' id='result_table]'}")
contents = table.innerText
test.verify(contents.find("Total: 387.92") != -1)
var table = waitForObject("{tagName='TABLE' id='result_table]'}");
var contents = table.innerText;
test.verify(contents.indexOf("Total: 387.92") != -1);
my $table = waitForObject("{tagName='TABLE' id='result_table]'}");
my $contents = $table->innerText;
test::verify(index($contents, "Total: 387.92") != -1);
table = waitForObject("{tagName='TABLE' id='result_table]'}")
contents = table.innerText
Test.verify(contents.find("Total: 387.92") != -1)
set table [waitForObject "{tagName='TABLE' id='result_table]'}"]
set contents [property get $table innerText]
test verify [expr [string first "Total: 387.92" $contents] != -1]

The innerText property gives us the entire table's text as a string, so we can easily search it.

Here's another example, this time checking that a DIV tag with the ID syncDIV is hidden.

div = waitForObject(":{tagName='DIV' id='syncDIV'}")
test.compare(div.style().value("display"), "hidden")
var div = waitForObject(":{tagName='DIV' id='syncDIV'}");
test.compare(div.style().value("display"), "hidden");
my $div = waitForObject(":{tagName='DIV' id='syncDIV'}");
test::compare($div->style()->value("display"), "hidden");
div = waitForObject(":{tagName='DIV' id='syncDIV'}")
Test.compare(div.style().value("display"), "hidden")
set div [waitForObject ":{tagName='DIV' id='syncDIV'}"]
test compare [invoke $div style [invoke value "display"]] "hidden"

Notice that we must use the HTML_Style HTML_Object.style() function (rather than writing, say div.style.display).

Often such DIV elements are used for synchronization. For example, after a new page is loaded, we might want to wait until a particular DIV element exists and is hidden—perhaps some JavaScript code in the HTML page hides the DIV, so when the DIV is hidden we know that the browser is ready because the JavaScript has been executed.

def isDIVReady(name):
    if not object.exists(":{tagName='DIV' id='%s'}" % name):
       return False
    return waitForObject(":{tagName='DIV' id='syncDIV'}").style().value(
        "display") == "hidden"

# later on...
waitFor("isDIVReady('syncDIV')")
function isDIVReady(name)
{
    if (!object.exists(":{tagName='DIV' id='" + name + "'}"))
       return false;
    return waitForObject(":{tagName='DIV' id='syncDIV'}").style().value(
        "display") == "hidden";
}

// later on...
waitFor("isDIVReady('syncDIV')");
sub isDIVReady
{
    my $name = shift @_;
    if (!object::exists(":{tagName='DIV' id='$name'}")) {
       return 0;
    }
     return waitForObject(":{tagName='DIV' id='syncDIV'}")->style()->value(
        "display") eq "hidden";
}

# later on...
waitFor("isDIVReady('syncDIV')");
def isDIVReady(name)
    if !Squish::Object.exists(":{tagName='DIV' id='#{name}'}")
       return false
    end
    waitForObject(":{tagName='DIV' id='syncDIV'}").style().value(
        "display") == "hidden"
end

# later on...
waitFor("isDIVReady('syncDIV')")
proc isDIVReady {name} {
    if {![object exists ":{tagName='DIV' id='${name}'}"]} {
       return false
    }
    set div [waitForObject ":{tagName='DIV' id='syncDIV'}"]
    set display [invoke $div style [invoke value "display"]]
    return [string equal $display "hidden"]
}

# later on...
[waitFor {isDIVReady('syncDIV')}]

We can easily use the Boolean waitFor(condition) function to make Squish wait for the code we give it to execute to complete. (Although it is designed for things that won't take too long.)

How to Do Web Application Load Testing

This example demonstrates how to load test a Web server using Squish. The following set up is assumed: Machine L controls the execution of the load testing scripts and has Squish installed along with a Python interpreter. Machine L also has the test suite to be executed. Machines C1 to Cn are the ones where the Web browsers will be running. They all need to the squishserver executable installed and running. Machine W is the Web server that will be put under load.

As machine W (the Web server) and machine L controlling the tests are physically different machines we need a way to retrieve system information over the network. The Simple Network Management Protocol (SNMP) is ideal for this task.

The load test is done by a Python script (loadtest.py) which is supplied along with Squish's examples: examples/loadtesting/loadtest.py. The script starts all the squishrunner processes and makes them connect to the squishserver processes and the machines C1 to Cn. All the details about the number of instances, start delays, the target host etc., are defined and documented at the top of the script. Simply adapt them to your needs.

The script will record the start and end times of all squishrunner runs. And in a secondary thread the script polls for system information from the webserver and records this too. Data about both the loads and the runs are written to tab separated values files and can therefore be easily evaluated in succeeding steps. (Or you can modify the script to produce the output in another format if you prefer.)

To make squishrunner be able to connect to remote machines, on each remote machine the configuration option ALLOWED_HOSTS in the squishrunnerrc file must be set to the host starting the squishrunner processes on all those machines C1 to Cn. (see Distributed Tests).

Recording system information

Machine W (the Web server) needs to run a SNMP daemon. It can be installed and set up easily. For configuring, the snmpconf command may prove useful. For example:

snmpconf -g basic_setup

Sources, binaries, documentation, and tutorials for this tool can be found at http://net-snmp.sourceforge.net. In addition the load testing script uses the command line SNMP utilities, which are also needed on Machine L. It would also be possible to use a Python SNMP module from its standard library or a third-party SNMP module—that's something you might want to do for yourself if you find you use the script a lot.

So in parallel with the squishrunners we will record system information using the command line SNMP utilities. At the end of the script the information is written to a file. Be aware that you may need to adjust the usage of the snmpget program to match your SNMP settings. The cutAvg function is a helper function for extracting what we need from the snmpget program's output.

def cutAvg(snmpString):
    return snmpString.strip().rsplit("STRING: ", 1)[1]


loads = []
def sysinfo():
    def loadOne(number):
        cmd = "snmpget -v 1 -c commro %s laLoad.%d" % (
            WEBSERVER_HOST, number)
        tmp = os.popen(cmd, "r")
        reply = tmp.read()
        reply = cutAvg(reply)
        tmp.close()
        return reply

    while True:
        l1 = loadOne(1)
        l5 = loadOne(2)
        l15 = loadOne(3)
        loads.append({'ts': time.time(), '1m': l1, '5m': l5, '15m': l15})
        time.sleep(5)

Generating the load

We will store information (host, start time, and end time), related to every squishrunner run in a SquishrunnerRun object. Taking a SquishrunnerRun object as parameter the runsuite function sets the starttime variable, initiates the connection to the squishserver on the given host, and stores the endtime after the squishrunner has finished.

class SquishrunnerRun:
    id = 0
    starttime = 0
    endtime = 0
    host = ''

    def __init__(self, id, host):
        self.id = id
        self.host = host

    def duration(self):
        return self.endtime - self.starttime


def runSuite(srr):
    srr.starttime = time.time()
    srrCmd = " ".join([SQUISHRUNNER_EXEC, "--host", srr.host,
                       "--testsuite", TEST_SUITE,
                       "--reportgen xml,%s%d.xml" % (REPORTS_DIR, srr.id)])
    os.system(srrCmd)
    srr.endtime = time.time()
    print "call %d finished; needed %s" % (
        srr.id, srr.endtime - srr.starttime)

Having defined the SquishrunnerRun class and the runSuite function we are now able to start the testing itself. For each of the RUN_COUNT runs, the next host will be associated with a newly created SquishrunnerRun object. The next runSuite function call will be started within a new thread. After waiting a specified amount of time we will continue. In addition we store all the SquishrunnerRun objects in a list.

runs = []
for i in range(RUN_COUNT):
    tmp = SquishrunnerRun(i,
        SQUISHSERVER_HOSTS[i % len(SQUISHSERVER_HOSTS)])
    runs.append(tmp)
    thread.start_new_thread(runSuite, (tmp,))
    time.sleep(RUN_DELAY)

Now having started all the squishrunner processes, the script must wait until all of them are finished. If a squishrunner process has finished, its endtime is set. So we must wait until none of the SquishrunnerRun's endtimes are set to 0.

def allRunnersFinished():
    for runner in runs:
        if runner.endtime != 0:
            return False
    return True

while not allRunnersFinished():
    pass

Postprocessing

Once the testing itself has finished we must store the results of the test. They will be written to the two files defined as RUNS_FILE and LOADS_FILE.

fh = None
try:
    fh = open(RUNS_FILE, "wt")
    for e in runs:
        fh.write("%s\t%s\t%s\n" % (e.id, e.starttime, e.endtime))
finally:
    if fh is not None:
        fh.close()

fh = None
try:
    fh = open(LOADS_FILE, "wt")
    for e in loads:
        fh.write("%(ts)s\t%(1m)s\t%(5m)s\t%(15m)s\n" % e)
finally:
    if fh is not None:
        fh.close()

The RUNS_FILE contains time stamps marking the start and end of each individual squishrunner run. The LOADS_FILE contains server load averages measured every 5 seconds. The measurement of other information (traffic, number of processes, disk I/O) can easily be configured using suitable SNMP commands. Graphical presentation of the data can be produced with standard charting software.

At some point we hope to provide a ready-made front-end that will make it possible to configure, schedule, and execute test runs, as well provide visual representations of the results.

© 2023 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners.
The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation.
Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.