15.1. How to Write Test Scripts

15.1.1. How to Identify and Access Objects
15.1.2. How to Use the Qt API
15.1.3. How to Use the Tk API
15.1.4. How to Use the XView API
15.1.5. How to Use the Web API
15.1.6. How to Use the Java™ API
15.1.7. How to Use Test Statements
15.1.8. How to Use Event Handlers
15.1.9. How to Create and Use Synchronization Points
15.1.10. How to Test Multiple AUTs from a Single Test Script, Using ApplicationContext
15.1.11. How to Test Qt Widgets
15.1.12. How to Test non-Qt Widgets in Qt Applications
15.1.13. How to Test Tk Widgets
15.1.14. How to Test Web Elements
15.1.15. How to Automate Native Browser Dialogs, Java Applets, Flash/Flex, ActiveX, and more
15.1.16. How to Test Java™ Applications
15.1.17. How to Create Semi-Automatic Tests that Query for User Input
15.1.18. How to Create Automatic Screenshots on Test Failures and Errors
15.1.19. How to Interact With Files and With the Environment in Test Scripts
15.1.20. How to Access Databases from Squish Test Scripts
15.1.21. How to Handle Exceptions Raised in Test Scripts

This section discusses Squish's scripting support, the different scripting languages Squish supports, and the script APIs which are available when working with test scripts. It is illustrated by many examples

[Important]Important

The Squish IDE loads and saves test scripts (files with names matching test.*) using the Unicode UTF-8 encoding, and Squish's command line tools assume that this encoding is used for all the scripts they execute. If you opt to edit your test scripts using another editor, make sure that the editor you use loads and saves the scripts using UTF-8; or, if the editor is not Unicode-capable, then the most sensible alternative is to restrict your code to 7-bit ASCII—which all modern editors support—since this is a subset of UTF-8.

Note also that some characters, most notably double quotes (") and backslashes (\), must be quoted in string literals. For example, "C:\\My Documents".

15.1.1. How to Identify and Access Objects

Probably the most important issue to face testers when writing scripts from scratch (or when modifying recorded scripts), is how to access objects in the user interface. We can obtain a reference to an object using the waitForObject() function. This function waits for the object to become visible and available and then returns a reference to it, or raises a catchable exception if it times out. If we need a reference to an object that isn't visible we must use the findObject() function, which does not wait. Both these functions take an object name, but getting the right name can be tricky, so we will explain the issues and solutions here before going into the Squish edition-specific and scripting language-specific details.

Squish supports two completely different naming schemes, "real names" and "symbolic names". Symbolic names are used by Squish when recording scripts. For hand-written code we can use symbolic name or real names (also called "multi-property names"), whichever we find more convenient.

15.1.1.1. How to Access Named Objects

The easiest situation is where an application object has been given an explicit name by the programmer. For example, using the Qt toolkit, an object can be given a name like this:

    cashWidget->setObjectName("CashWidget");

When an object is given a name in this way, we can identify it using a real name that specifies just two properties: the object's type and its object name. Here is how we can access the cashWidget label in the various scripting languages using the waitForObject() function:

Python
    cashWidget = waitForObject("{name='CashWidget' type='QLabel'}")

JavaScript
    var cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");

Perl
    my $cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");

Tcl
    set cashWidget [waitForObject {{name='CashWidget' type='QLabel'}}]

To create a string that represents a real (multi-property) name, we create a string which has an opening brace, then two or more space-separated property items (each having the form, propertyname='value'), and finally a closing brace. One of the properties must be the object's type. If the object has an object name, using just the type and name properties is sufficient (providing that the name is unique amongst objects of the specified type).

Once we have a reference to an object we can access its properties, for example, to check them against expected values, or to change them. We will see how to do this in the Squish edition-specific sections that follow.

Unfortunately, reality is not often so convenient. Programmers may not give unique names to objects, or they might not set explicit names at all, and in any case some objects are created as a result of program execution rather than directly by programmers. Objects that don't have names are unnamed, and in most testing situations the majority of objects we want to test are unnamed. Squish has two solutions to this problem. One solution is an extension of the multiple-properties approach (real names), and the other is to use symbolic names.

15.1.1.2. How to Access Objects Using Real (Multi-Property) Names

When we are faced with unnamed objects we can almost always uniquely identify them by creating a name consisting of multiple properties. For example, here is how we can identify and access the payButton button:

Python
    payButtonName = ("{type='QPushButton' text='Pay' unnamed='1'"
                     "visible='1'}")
    payButton = waitForObject(payButtonName)

JavaScript
    var payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" +
                        "visible='1'}";
    var payButton = waitForObject(payButtonName);

Perl
    my $payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" .
                        "visible='1'}";
    my $payButton = waitForObject($payButtonName);

Tcl
    set payButtonName {{type='QPushButton' text='Pay' unnamed='1'
                        visible='1'}}
    set payButton [waitForObject $payButtonName]

This works because in this particular example there is only one button on the form with the text "Pay".

In some cases, the object we are interested in has neither a name nor any unique text of its own. But even in such cases it is usually possible to identify it. For example, an unnamed spinbox might well be the buddy of an associated label, so we can use this relationship to uniquely identify the spinbox as the following examples show:

Python
    paymentSpinBoxName = ("{buddy=':Make Payment.This Payment:_QLabel'"
                          "type='QSpinBox' unnamed='1' visible='1'}")
    paymentSpinBox = waitForObject(paymentSpinBoxName)

JavaScript
    var paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" +
                             "type='QSpinBox' unnamed='1' visible='1'}";
    var paymentSpinBox = waitForObject(paymentSpinBoxName);

Perl
    my $paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" .
                             "type='QSpinBox' unnamed='1' visible='1'}";
    my $paymentSpinBox = waitForObject($paymentSpinBoxName);

Tcl
    set paymentSpinBoxName {{buddy=':Make Payment.This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}}
    set paymentSpinBox [waitForObject $paymentSpinBoxName]

Here, the buddy is identified using a symbolic name copied from the Object Map (Section 16.9).

If there is no obvious way of identifying an object, either use Squish's How to Use the Spy (Section 15.2.4) tool to get Squish to provide a suitable name, or record a quick throwaway test in which you interact with the object of interest and then look in the Object Map (Section 16.9) to see what real and symbolic names Squish used, and then use one of these names in your test code.

In some cases we might want to use a property whose text varies. For example, if we want to identify a window whose caption text changes depending on the window's contents. This is possible using Squish's sophisticated matching capabilities and is described later in Improving Object Identification (Section 16.8).

If the waitForObject() function cannot find the object with the given name a LookupError exception is raised which if left uncaught leads to an error entry to be added to Squish's log. This is normally what we want since it probably means we mistyped one of the property's values. However, if an object may exist only in some cases (for example, if a particular tab of a tab widget is chosen), we can use the object.exists() function to check if an object of the given name exists, and if it does to perform any tests we want on it in that case. For example, in Python we could write this:

moreOptionsButtonName = "{type='QPushButton' name='More Options'}"
if object.exists(moreOptionsButtonName):
    moreOptionsButton = waitForObject(moreOptionsButtonName)
    clickButton(moreOptionsButton)

One advantage of this approach is that if the object does not exist the script finds out straight away. Compare it with this approach:

try:
    moreOptionsButtonName = "{type='QPushButton' name='More Options'}"
    moreOptionsButton = waitForObject(moreOptionsButtonName)
except LookupError:
    pass # button doesn't exist so don't click the button
else:
    clickButton(moreOptionsButton)

This is potentially slower than using the object.exists() function since the waitForObject() function will wait for 20 seconds (the default timeout, which can be changed by giving a second argument), although both approaches are valid.

15.1.1.3. How to Access Objects Using Symbolic Names

When Squish records a test it uses symbolic names to identify the widgets. Some symbolic names are quite easy to understand, for example, ":fileNameEdit_QLineEdit", while others can be more cryptic, for example, ":CSV Table - before.csv.File_QTableWidget"—this symbolic name includes the window caption which shows the name of the current file. Symbolic names are generated programmatically by Squish although they can also be used in hand-written code, or when modifying or using extracts from recorded tests.

Symbolic names have one major advantage over real names: if a property that a real name depends on changes (i.e., due to a change in the AUT), the real name will no longer be valid, and all uses of it in test scripts will have to be updated. But if a symbolic name has been used, the real name that the symbolic name refers to, (i.e., the name's properties and their values), can simply be updated in the Object Map, and no changes to tests are necessary. (See Object Map (Section 16.9).)

Squish distinguishes between the two by the fact that symbolic names begin with a colon (:) while real names are always enclosed in braces ({}).

15.1.2. How to Use the Qt API

One of Squish's most useful features is the ability to access the complete Qt API (and optional application API) from test scripts. This gives test engineers a huge amount of flexibility allowing them to test just about anything in the AUT.

With Squish's Qt API it is possible to find and query objects, call methods, and access properties and enums. Furthermore, Squish 4 automatically recognizes Qt QObject and QWidget properties and slots. This means that building custom wrappers is rarely necessary since application developers can expose custom object properties by using the Q_PROPERTY macro, and can expost custom object methods by making them into slots. This even applies (from Qt 4.6) to automatically recognizing the properties and slots of QGraphicsWidgets and QGraphicsObjects and custom subclasses that derive from them.

In addition, Squish provides a How to Use the Qt Convenience API (Section 15.1.2.4) to execute common GUI actions such as clicking a button or selecting a menu item

The chapter How to Test Qt Widgets (Section 15.1.11) later in this manual presents several different examples that show how to use the scripting Qt API to access and test complex Qt applications.

15.1.2.1. How to Access Qt Objects

As we saw in How to Identify and Access Objects (Section 15.1.1), we can call waitForObject() (or findObject() for hidden objects), to get a reference to an object with a specific real or symbolic name. Once we have such a reference we can use it to interact with the object, access the object's properties, or call the object's methods.

Here are some examples where we access a QRadioButton, and if it isn't checked, we click it to check it, so at the end it should be checked whether it started out that way or not.

Python
    cashRadioButtonName = ("{text='Cash' type='QRadioButton' visible='1'"
                           "window=':Make Payment_MainWindow'}")
    cashRadioButton = waitForObject(cashRadioButtonName)
    if not cashRadioButton.checked:
        clickButton(cashRadioButton)
    test.compare(cashRadioButton.checked, True)

JavaScript
    var cashRadioButtonName = "{text='Cash' type='QRadioButton' visible='1'" +
                              "window=':Make Payment_MainWindow'}";
    var cashRadioButton = waitForObject(cashRadioButtonName);
    if (!cashRadioButton.checked) {
        clickButton(cashRadioButton);
    }
    test.compare(cashRadioButton.checked, true);

Perl
    my $cashRadioButtonName = "{text='Cash' type='QRadioButton' " .
                              "visible='1'window=':Make Payment_MainWindow'}";
    my $cashRadioButton = waitForObject($cashRadioButtonName);
    if (!$cashRadioButton->checked) {
        clickButton($cashRadioButton);
    }
    test::compare($cashRadioButton->checked, 1);

Tcl
    set cashRadioButtonName {{text='Cash' type='QRadioButton' visible='1' 
            window=':Make Payment_MainWindow'}}
    
    set cashRadioButton [waitForObject $cashRadioButtonName]
    if {![property get $cashRadioButton checked]} {
        invoke clickButton $cashRadioButton
    }
    test compare [property get $cashRadioButton checked] true

In this example we get the value of a property, set the property (indirectly by clicking the widget), and then get the value of the property again so that we can test that it has the correct value.

Here is another example, this time one that sets and gets a QLineEdit's, text property, and prints the property's value to Squish's test log.

Python
lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName")
lineedit.text = "A new text"
text = lineedit.text
test.log(str(text))
JavaScript
var lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
lineedit.text = "A new text";
var text = lineedit.text;
test.log(String(text));
Perl
my $lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
$lineedit->text = "A new text";
my $text = $lineedit->text;
test::log("$text");
Tcl
set lineedit [waitForObject "Addressbook.ABCentralWidget1.FirstName"]
property set $lineedit text "A new text"
set text [property get $lineedit.text]
test log [toString $text]

Notice that here we have used symbolic rather than real names. Although it is best to use real names in hand-written code, when Squish records a script it uses symbolic names, so when we copy and paste or modify code from a recorded script we will often end up using symbolic names like these.

15.1.2.2. How to Call Functions on Qt Objects

With Squish it is possible to call every public function on any Qt object. In addition it is possible to call static functions provided by Qt.

In the example below we change the button text of the button we queried in the previous section using QButton::setText().

Python
button = waitForObject("Addressbook.ABCentralWidget1.AddButton")
button.setText("Changed Button Text")
JavaScript
var button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
button.setText("Changed Button Text");
Perl
my $button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
$button->setText("Changed Button Text");
Tcl
set button [waitForObject "Addressbook.ABCentralWidget1.AddButton"]
invoke $button setText "Changed Button Text"

Similarly, static Qt functions can be called. As an example, we will query the currently active modal widget (e.g. a dialog box) using the static QApplication::activeModalWidget() function. If this returns a valid object, we will print the object's object name (or "unnamed" if no name has been set) to the test log. To check if the object is valid (i.e., not null), we can use Squish's isNull() function. To find the object's name we access its objectName property.

Python
widget = QApplication.activeModalWidget()
if not isNull(widget):
    test.log(widget.objectName or "unnamed")
JavaScript
var widget = QApplication.activeModalWidget();
if (!isNull(widget)) {
    var name = widget.objectName;
    test.log(name.isEmpty() ? "unnamed" : name);
}
Perl
my $widget = QApplication::activeModalWidget();
if (!isNull($widget)) {
    test::log($widget->objectName() || "unnamed");
}
Tcl
set widget [invoke QApplication activeModalWidget]
if {![isNull $widget]} {
    set name [property get $widget objectName]
    if {[invoke $name isEmpty]} {
	set name "unnamed"
    }
    test log stdout "$name\n"
}

15.1.2.3. How to Access Qt Enums

In C++ it is possible to declare enumerations—these are names that stand for numbers to make the meaning and purpose of the numbers clear. For example, instead of writing label->setAlignment(1);, the programmer can write label->setAlignment(Qt::AlignLeft); which is much easier to understand. (The term enumeration is often abbreviated to enum; we use both forms in this manual.)

Qt defines a lot of enumerations, and many of Qt's functions and methods take enumerations as arguments. Just as using enumerations makes code clearer for C++ programmers, it can also make test code clearer, so Squish makes it possible to use enums in test scripts. Here's how we would set the alignment of a label in a test script:

Python
label = waitForObject("Addressbook.ABCentralWidget1.labelFirstName")
label.setAlignment(Qt.AlignLeft)
JavaScript
var label = waitForObject("Addressbook.ABCentralWidget1.labelFirstName");
label.setAlignment(Qt.AlignLeft);
Perl
my $label = waitForObject("Addressbook.ABCentralWidget1.labelFirstName");
$label->setAlignment(Qt::AlignLeft);
Tcl
set label [waitForObject Addressbook.ABCentralWidget1.labelFirstName]
invoke $label setAlignment [enum Qt AlignLeft]

15.1.2.4. How to Use the Qt Convenience API

This section describes the script API Squish offers on top of the standard Qt API to make it easy to perform common user actions such as clicking a button or activating a menu option. A complete list of this API is available in the Qt Convenience API (Section 16.1.4) section in the Reference Manual (Chapter 16).

Here are some examples to give a flavor of how the API is used. The first line shows how to click a button, the second line shows how to double-click an item (for example, an item in a list, table, or tree), and the last example shows how to activate a menu option.

Python
clickButton("Addressbook.AddButton")
doubleClickItem("Addressbook.addressList", "Max|Mustermann|*", 0, 0, 0, Qt.LeftButton)
activateItem("Addressbook.menubar", "Quit")
JavaScript
clickButton("Addressbook.AddButton");
doubleClickItem("Addressbook.addressList", "Max|Mustermann|*", 0, 0, 0, Qt.LeftButton);
activateItem("Addressbook.menubar", "Quit");
Perl
clickButton("Addressbook.AddButton");
doubleClickItem("Addressbook.addressList", "Max|Mustermann|*", 0, 0, 0, Qt::LeftButton);
activateItem("Addressbook.menubar", "Quit");
Tcl
invoke clickButton "Addressbook.AddButton"
invoke doubleClickItem "Addressbook.addressList" "Max|Mustermann|*" 0 0 0 [enum Qt LeftButton]
invoke activateItem "Addressbook.menubar" "Quit"

15.1.3. How to Use the Tk API

One of Squish's most useful features is the ability to access the toolkit's API from test scripts. This gives test engineers sufficient flexibility to allow them to test just about any aspect of the AUT.

With Squish's Tk-specific API it is possible to find and query objects, access properties, and evaluate arbitrary Tcl code in the AUT's interpreter.

In addition, Squish provides a convenience API (How to Use the Tk Convenience API (Section 15.1.3.4)) to execute common GUI actions such as clicking a button or selecting a menu item.

The chapter How to Test Tk Widgets (Section 15.1.13) later in this manual presents different examples that show how to use the scripting Tk API to access and test complex Tk widgets.

15.1.3.1. How to Find and Query Tk Objects

Squish provides the waitForObject() function which returns a reference to the object with the given qualified object name. A qualified object name is a name like myapp.frame1.okbutton. The period notation is used as a separator (rather like / or \ in file paths), that is used to identify a particular object by its position in the object hierarchy. The application's main window is the root of the hierarchy, and contains all the application's top-level widgets, some of which contain child widgets, and so on. In the example above, the okbutton is a child of frame, which in turn is a child of myapp (the applicaton's main window).

To find out the name of an object, you can use the Spy tool to introspect the application. See the How to Use the Spy (Section 15.2.4) section for details.

To get a reference to an object—which can then be queried to check the object's properties, or which can be used to interact with the object—use the waitForObject() function. For example, in Tcl you would use code like this:

set button [waitForObject "myapp.frame1.okbutton"]

If waitForObject() can't find the specified object—or if the object is not available before the timeout, for example if it is hidden—a script error is thrown which stops the script execution. In some situations it might be desirable to check to see if the object exists and only interact with the object if it is found. This can be done by using the object.exists() function.

For example, suppose we want to find the okbutton as we did before, and click it—but only if it exists. In Tcl we can achieve this with the following code:

if {[object exists "myapp.frame1.okbutton"]} {
    set button [waitForObject "myapp.frame1.okbutton"]
    invoke clickButton $button
}

Using qualified object names with the waitForObject() function, means that test engineers can query and interact with all the objects in the AUT's object hierarchy.

15.1.3.2. How to Access Tk Object Properties

Using the Tk script API it is possible to access almost all of Tk's widget properties.

For example, if we want to change the text in an entry widget, we can do so using the following Tcl code, and of course, substituting the qualified object name and the new text appropriately:

set entry [waitForObject "myapp.frame1.e1"]
property set $entry text "New text"
set text [property get $entry text]
test log [toString $text]

The first two lines set the new text; the third line creates a new variable, text, and the last line prints the text. When test scripts print to stdout or to stderr, the printed text appears in the Squish IDE's Runner Log; here we've written to the Test Log instead.

15.1.3.3. How to Use tcleval

Although Squish test scripts can access the Tk widget properties, this is not sufficient for testing purposes, because not all the information we want to query is available through these properties. Fortunately, Squish provides a solution for this: the tcleval function. This function can execute arbitrary Tcl code which is interpreted within the scope of the AUT.

For example, if we want to retrieve the contents of a Tk text widget, we cannot do so through the widget's properties since the text is not available as a property. What we can do instead is to call the text widget's get function, since this returns the text widget's text between given indices. So to get the entire text we use indices 1.0 and end. Here's how we can use the tcleval function to call get on a text widget:

set text [invoke tcleval ".textfield get 1.0 end"]

Notice that the entire argument to tceval is passed as a string. The .textfield is the name of the text widget (recall that . is the root of the widget hierarcy in pure Tcl/Tk).

15.1.3.4. How to Use the Tk Convenience API

This section provides a glimpse of the script API Squish offers on top of Tk to make it easy to perform common user actions such as clicking a button. Details of the full API are given in the Tk Convenience API (Section 16.1.5) section of the Reference Manual (Chapter 16). Here we will just show a few examples to give a taste of what the API offers and how to use it.

invoke clickButton "myapp.button1"
invoke doubleClickItem "myapp.list1" "Banana" 0 0 0 1
invoke activateItem "myapp.filemenu" "Quit"
invoke type "myapp.frame1.e1" "New text"

Here, we click a button, double click a list item with the given text, invoke a menu option, and enter some text. These are the most commonly used Tk convenience functions, although there are additional ones in the API.

15.1.4. How to Use the XView API

15.1.5. How to Use the Web API

One of Squish's most useful features is the ability to access the toolkit's API from test scripts. This gives test engineers great deal of flexibility and allows them to test just about anything in the AUT. With Squish's Web-specific API it is possible to find and query objects, access properties and methods, and evaluate arbitrary JavaScript code in the Web-application's context. In addition, Squish provides a convenience API (see How to Use the Web Convenience API (Section 15.1.5.6)) that provides facilities for executing common actions on Web sites such as clicking a button or entering some text.

A variety of examples that show how to use the scripting Web API to access and test complex Web elements is given in the How to Test Web Elements (Section 15.1.14) section.

15.1.5.1. How to Find and Query Web Objects

Squish provides two functions, findObject() and waitForObject(), that return a reference to the object (HTML or DOM element), for a given qualified object name. The difference between them is that waitForObject() 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 findObject() can be used on hidden objects.

See the Web Object API (Section 16.1.9) 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 15.2.4) 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 findObject() function, it will either return immediately with the object, or it will throw a script error 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 Python code snippet that shows how to use findObject() without risking an error being thrown, by using the object.exists() function:

Python
radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}"
if object.exists(radioName):
    radioButton = findObject(radioName)
    clickButton(radioButton)
JavaScript
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);
}
Perl
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);
}
Tcl
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 object.exists() call.

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

Python
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}")
clickButton(radioButton)
JavaScript
var radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
clickButton(radioButton);
Perl
my $radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
clickButton($radioButton);
Python
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 findObject() and waitForObject() 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.

15.1.5.2. How to Use XPath

For every object returned by the findObject() and waitForObject() 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:

Python
div = findObject("{tagName='DIV' id='mydiv'}")
link = div.evaluateXPath("A[contains(@href, 'www.froglogic.com')]").snapshotItem(0)
JavaScript
var div = findObject("{tagName='DIV' id='mydiv'}");
var link = div.evaluateXPath("A[contains(@href, 'www.froglogic.com')]").snapshotItem(0);
Perl
my $div = findObject("{tagName='DIV' id='mydiv'}");
my $link = $div->evaluateXPath("A[contains(@href, 'www.froglogic.com')]")->snapshotItem(0);
Tcl
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—this is returned as an object of type HTML_Object Class (Section 16.1.9.14).

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_Object.evaluateXPath() method returns an object of type HTML_XPathResult Class (Section 16.1.9.21) on which you can query the result of the XPath evaluation.

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

[Tip]Tip

For more information about how you can create XPath queries to help produce flexible and compact test scripts, refer to documentation that specializes in this topic. For example, we recommend the XPath Tutorial from the W3Schools Online Web Tutorials website.

15.1.5.3. 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 (Section 16.1.9) for full details of Squish's Web classes and methods.

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

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

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 properties using the property() method. For example, to check if a DIV element is hidden, we can write code like this:

Python
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
test.compare(div.property("style.display"), "none")
JavaScript
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test.compare(div.property("style.display"), "none");
Perl
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test::compare($div->property("style.display"), "none");
Tcl
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
test compare [invoke $div property "style.display"] "none"

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

15.1.5.4. 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 (Section 16.1.9).

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.firstChild() function:

Python
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
child = div.firstChild()
test.log(child.tagName)
JavaScript
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
var child = div.firstChild();
test.log(child.tagName);
Perl
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
my $child = $div->firstChild();
test::log($child->tagName);
Tcl
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 JavaScript code:

Python
element = findObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
option = element.optionAt(element.selectedIndex)
test.log(option.text)
JavaScript
var element = findObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
var option = element.optionAt(element.selectedIndex);
test.log(option.text);
Perl
my $element = findObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
my $option = $element->optionAt($element->selectedIndex);
test::log($option->text);
Tcl
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 myOwnFunction() function with string argument an argument, on a DIV element, we could write code like this:

Python
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
div.invoke("myOwnFunction", "an argument")
JavaScript
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
div.invoke("myOwnFunction", "an argument");
Perl
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
$div->invoke("myOwnFunction", "an argument");
Tcl
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
invoke $div "myOwnFunction" "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:

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

15.1.5.5. 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 evalJS() function. Here is an example of its use:

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

The evalJS() function returns the result of the last statement executed—in this case the last statement is d ? d.style.display : ''.

15.1.5.6. 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 16.1.9) section in the Reference Manual (Chapter 16). 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.

Python
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")
JavaScript
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");
Perl
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");
Tcl
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, although all of them are just as easy to use.

15.1.5.7. How to Synchronize Web Page Loading for Testing

The special isPageLoaded() 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:

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

Additionally, we can wait for any object to be ready using the waitForObject() function:

Python
login_button = waitForObject(":{tagName='INPUT' type='button' value='Login'}")
clickButton(login_button)
JavaScript
var login_button = waitForObject(":{tagName='INPUT' type='button' value='Login'}");
clickButton(login_button);
Perl
my $login_button = waitForObject(":{tagName='INPUT' type='button' value='Login'}");
clickButton($login_button);
Tcl
set login_button [waitForObject ":{tagName='INPUT' type='button' value='Login'}"]
invoke clickButton $login_button

In advanced AJAX applications, waiting for a page to be loaded is often insufficient, since parts of the page will be loaded using asynchronous AJAX requests. In such cases we must take more sophisticated approaches to synchronization.

In many cases, simply waiting for a particular object to become available will suffice, in which case we can just use the waitForObject() function.

But many AJAX toolkits refresh objects using AJAX requests in the background. In such situations it might be necessary to wait until the background loading has finished in addition to waiting for particular objects to become available.

When background loading is taking place, most Web toolkits display a visual cue—for example, a box which says "loading..."—to indicate to the user that the application is loading. We can use such a visual cue to synchronize our script by waiting until the loading cue has disappeared.

To show how to handle situations where AJAX is used for background loading we will develop an AJAX synchronization function for test scripts used for testing applications based on the Backbase AJAX toolkit. Backbase is just one of many Web toolkits that Squish supports, and although the example is specific to Backbase, it should translate for use with other toolkits without too much trouble.

Backbase uses a text box that displays the text "loading..." when loading is taking place, so we must develop a function that will tell us if loading is in progress:

Python
def isBackbaseLoading():
    if not object.exists("{tagName='DIV' id='loading'}"):
        return False
    
    div = findObject("{tagName='DIV' id='loading'}")
    if isNull(div) or isNull(div.parentElement()):
        return False
    
    div = div.parentElement()
    if div.property("style.display") == "none":
        return False
    
    return True
JavaScript
function isBackbaseLoading()
{
    if (!object.exists("{tagName='DIV' id='loading'}"))
        return false;
    
    var div = findObject("{tagName='DIV' id='loading'}");
    if (isNull(div) || isNull(div.parentElement()))
        return false;
    
    div = div.parentElement();
    if (div.property("style.display") == "none")
        return false;
    
    return true;
}
Perl
sub isBackbaseLoading
{
    if (!object::exists("{tagName='DIV' id='loading'}")) {
        return 0;
    }
    my $div = findObject("{tagName='DIV' id='loading'}");
    if (isNull($div) || isNull($div->parentElement())) {
        return 0;
    } 
    $div = $div->parentElement();
    if ($div->property("style.display") eq "none") {
        return 0;
    }
    return 1;
}
Tcl
proc isBackbaseLoading {} {
    if {![object exists "{tagName='DIV' id='loading'}"]} {
        return false
    }
    set div [findObject "{tagName='DIV' id='loading'}"]
    if {[invoke isNull $div] || [invoke isNull [invoke $div parentElement]]} {
        return false
    }
    set div [invoke $div parentElement]
    if {[string equal [invoke $div property "style.display"] "none"]} {
        return false
    } 
    return true
}

The isBackbaseLoading() function checks to see if there is a DIV element with an id set to loading, and if there is, whether it is displayed.

In practical testing we will always want to synchronize with the loading state after performing certain operations—for example, after the test script has clicked particular items which trigger some background loading. However, the loading might not begin immediately, but instead might occur after some small delay after the operation that made the loading necessary. So we need a function which first waits for the "loading..." cue to appear, and if it does, that then goes on to wait for a short amount of time (to allow the loading to actually begin), and finally that waits until the loading cue disappears again:

Python
def synchBackbase():
    loading = waitFor("isBackbaseLoading()", 2000)
    if not loading:
        return
    waitFor("not isBackbaseLoading()")
JavaScript
function synchBackbase()
{
    var loading = waitFor("isBackbaseLoading()", 2000);
    if (!loading)
        return;
    waitFor("! isBackbaseLoading()");
}
Perl
sub synchBackbase
{
    my $loading = waitFor("isBackbaseLoading()", 2000);
    if (!$loading) {
        return;
    }
    waitFor("! isBackbaseLoading()");
}
Tcl
proc synchBackbase {} {
    set loading [waitFor "isBackbaseLoading()" 2000]
    if {!$loading} {
        return;
    }
    waitFor "![invoke isBackbaseLoading]"
}

Here we wait for up to two seconds to see if loading is taking place, and if it is, we then wait (indefinitely) for the loading to be finished (i.e., for isBackbaseLoading() to return false).

With the synchBackbase() function available, we can call it just after every operation that could cause loading to take place. But if we were to do that our code would end up littered with calls to synchBackbase()—making it less clear—and also it would be quite easy to forget to call it in some places. Fortunately, Squish provides a nice solution to this problem. If we implement a special function called waitUntilObjectReady(), Squish will call it automatically from every waitForObject() call.

Here is a simple implementation that will ensure that background loading is always finished:

Python
sub waitUntilObjectReady(obj):
    synchBackbase()
JavaScript
function waitUntilObjectReady(obj)
{
    synchBackbase();
}
Perl
sub waitUntilObjectReady
{
    synchBackbase();
}
Tcl
proc waitUntilObjectReady {obj} {
    invoke synchBackbase
}

Now, whenever we call waitForObject(), Squish will call our waitUntilObjectReady() function, and this in turn will ensure that the AJAX application has finished loading. This allows us to synchronize the test even if some of our test script's actions cause asynchronous AJAX requests to take place.

We can put these functions into a shared script and include it in all of our test cases—this will allow us to use this advanced synchronization technique for all of our tests. (See also, How to Create and Use Shared Data and Shared Scripts (Section 15.4).)

Although the functions here are specific to the Backbase toolkit, similar functions can be implemented for any other AJAX toolkit. Squish's examples include the examples/web/suite_examples/tst_backbase_pim example which shows these functions in action. (See also How to Create and Use Synchronization Points (Section 15.1.9).)

15.1.6. How to Use the Java™ API

One of Squish's most useful features is its ability to access the toolkit's API from within test scripts. This gives test engineers sufficient flexibility to allow them to test just about anything in the AUT.

With Squish's Java™-specific API, it is possible to find and query objects, and to access properties and methods. When we talk about properties, we mean fields in Java™—these are classes that have methods which follow a particular naming scheme, for example:

SomeType getSomething();
boolean isSomething();
void setSomething(SomeType someValue);

When Squish sees methods with names of the form getXyz() or isXyz(), it creates a property called xyz. The property is read-only unless there is a method with a name of the form setXyz(), in which case the property is read–write. (Squish never creates write-only properties, so if only a setter is present it is treated as a normal method.) So in the example shown here (and assuming only one of getSomething() or isSomething() is defined), Squish will create a property called something.

In addition, Squish provides a convenience API—see How to Use the Java™ Convenience API (Section 15.1.6.4) for an introduction, and Java™ Convenience API (Section 16.1.7) for the whole API. The convenience API makes it easy to execute common actions on GUI applications such as clicking a button or entering some text.

The How to Test Java™ Applications (Section 15.1.16) section later in this manual provides a wide range of examples that show how to use the scripting Java™ API to access and test complex JavaGUI elements, including list, table, and tree widgets. Separate examples are given for AWT/Swing and for SWT applications (although the principles that apply are the same for both).

15.1.6.1. How to Find and Query Java™ Objects

Squish provides the waitForObject() function which returns the object for a given qualified object name as soon as it becomes available—for example, when it becomes visible. (For hidden objects use the findObject() function instead).

Squish supports three notations for identifying an object by name:

  • Symbolic name—these names are generated algorithmically and used in the Squish Object Map (Section 16.9) to make it easier to create tests that are robust in the face of changes to the AUT's object hierarchy. These names are similar to hierarchical names in that they begin with a colon and consist of one or more period-separated texts—for example, :Payment Form.Pay_javax.swing.JButton. Symbolic names are preferred for test scripts (and are the ones Squish uses when recording scripts), since they make test script maintainence easier. (See Editing an Object Map (Section 16.9.2.2) for more about handling object hierarchy changes.)

  • Multiple property name—a list of property-name=value pairs in curly braces that uniquely identifies the object. Squish will search the GUI parent–child hierarchy until it finds a matching object. Here is an example of such a name: {caption='Pay' type='javax.swing.JButton' visible='true' window=':Payment Form_PaymentForm'}. To be valid, a multi-property name (also called a real name) must include a type property and at least one other property. Notice also that in this example another object was referred to (the window); and in this case the reference used a symbolic name. In general using symbolic names is more robust, but if we need to identify an object with a variable property (for example, a caption that might change), then we must use a multi-property name, since this naming scheme supports wildcards. (See Improving Object Identification (Section 16.8) for more about wildcards.)

  • Hierarchical name—from the top Frame (or Shell in SWT) the path to the object, where all parent GUI elements are included with each one separated by a period. Here is an example of such a name: :frame0.JRootPane.null_layeredPane.null_contentPane.JLabel. These names are supported for backwards compatibility but should not be used in new tests.

To find the name of an object, you can use the Spy tool to introspect the AUT. See the How to Use the Spy (Section 15.2.4) section for details.

It is perfectly okay to use both real and symbolic names in tests. The most common scenario is to use symbolic names (often cut and pasted from the Object Map or from a recorded script), and to only use real names when the wildcard functionality is required.

To get a reference to an object you can use either a symbolic name or a real (multi-property) name—or even a hierarchical name. The name is passed to the waitForObject() function. For example:

forenameTextField = waitForObject(":Address Book - Add.Forename:_javax.swing.JTextField")

There are four basic idioms that can be used to access objects. The first is simply to use the waitForObject() function as shown here. This is ideal for most situations where the object in question is expected to be visible. For situations where the object may not be visible (for example, an object on a Tab page widget that isn't currently shown), or may not even exist (for example, an object that is only created by the AUT in certain circumstances), there are three approaches we can use, depending on our needs.

If we expect the object to be present and visible, but want to account for rare occasions when it isn't we can use code like this (using Python in this example):

Python
try:
    textField = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")
    # here we can use the textField object reference
except LookupError, err:
    test.fail("Could not find the account name text field")
JavaScript
try {
    var textField = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField");
    // here we can use the textField object reference
} catch(err) {
    test.fail("Could not find the account name text field");
}
Perl
eval {
    my $textField = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField");
    # here we can use the textField object reference
} or do {
    test::fail("Could not find the account name text field");
};
Tcl
catch {
    set textField [waitForObject ":Credit Card.Account Name:_javax.swing.JTextField"]
    # here we can use the textField object reference
} result options
if {[dict get $options -code]} {
    test::fail("Could not find the account name text field");
}

If we expect an object to be absent (for example, a button that should disappear in some situations), we can check like this (again using Python):

Python
code = 'waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")'
test.exception(code, "Correctly didn't find the text field")
JavaScript
var code = 'waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")';
test.exception(code, "Correctly didn't find the text field");
Perl
my $code = 'waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")';
test::exception($code, "Correctly didn't find the text field");
Tcl
set code = {[waitForObject ":Credit Card.Account Name:_javax.swing.JTextField"]}
test exception $code "Correctly didn't find the text field"

If we expect an object to be hidden but nonetheless, present (for example, on a Tab page that isn't the current one), we can still access it, but this time we cannot use the waitForObject() function—which only works for visible objects—but instead must use the object.exists() function in conjunction with the findObject() function:

Python
if object.exists(":Credit Card.Account Name:_javax.swing.JTextField"):
    textField = findObject(":Credit Card.Account Name:_javax.swing.JTextField")
    if textField:
	test.passes("Correctly found the hidden object")
JavaScript
if (object.exists(":Credit Card.Account Name:_javax.swing.JTextField")) {
    var textField = findObject(":Credit Card.Account Name:_javax.swing.JTextField");
    if (textField)
	test.pass("Correctly found the hidden object");
}
Perl
if (object::exists(":Credit Card.Account Name:_javax.swing.JTextField")) {
    my $textField = findObject(":Credit Card.Account Name:_javax.swing.JTextField");
    if ($textField) {
	test::pass("Correctly found the hidden object");
    }
}
Tcl
if {[object exists ":Credit Card.Account Name:_javax.swing.JTextField"]} {
    set textField [findObject ":Credit Card.Account Name:_javax.swing.JTextField"]
    if {![isNull $textField]} {
	test pass "Correctly found the hidden object"
    }
}

Using these techniques it is possible to query and access every object in the AUT's object hierarchy, including both visible and hidden objects.

15.1.6.2. How to Call Functions on Java Objects

Squish makes it possible to call any public function on any Java object. (See How to Find and Query Java™ Objects (Section 15.1.6.1) for details about finding objects). The following example shows how you can create a Java™ object:

Python
s = java_lang_String("A string")
JavaScript
var s = new java_lang_String("A string");
Perl
my $s = java_lang_String->new("A string"); # "old"-style
my $s = new java_lang_String("A string");  # "new"-style
Tcl
set s [construct java_lang_String "A string"]
[Note]Note

When referring to Java™ objects which are qualified by package names in Squish scripts, the normal periods (.) are replaced with underscores (_). This is done because period is not allowed in identifier names (and in some cases has a special meaning) in most of the scripting languages that Squish supports.

The example below uses the calculator demo application as the AUT. The tiny JavaScript test script changes the multiply button's text from * to x:

Python
button = waitForObject(":frame0.*_javax.swing.JButton")
button.setText("x")
JavaScript
var button = waitForObject(":frame0.*_javax.swing.JButton");
button.setText("x");
Perl
my $button = waitForObject(":frame0.*_javax.swing.JButton");
$button->setText("x");
Tcl
set button [waitForObject ":frame0.*_javax.swing.JButton"]
invoke $button setText "x"

It is also possible to call static functions. Here is an example that uses Java™'s static Integer.parseInt(String s) function:

Python
i = java_lang_Integer.parseInt("12")
JavaScript
var i = java_lang_Integer.parseInt("12");
Perl
my $i = java_lang_Integer::parseInt("12");
Tcl
set i [invoke java_lang_Integer parseInt "12"]

15.1.6.3. How to Access Java™ Object Properties

Java™ objects can have fields (sometimes called properties). Public fields are accessible in Squish as the following example demonstrates:

Python
point = java_awt_Point(5, 8)
test.log(point.x)
JavaScript
var point = new java_awt_Point(5, 8);
test.log(point.x);
Perl
my $point = java_awt_Point->new(5, 8); # "old"-style
my $point = new java_awt_Point(5, 8);  # "new"-style
test::log($point->x);
JavaScript
set point [construct java_awt_Point 5 8]
test log [toString [property get $point x]]

In addition to public fields, Squish adds synthetic properties derived from method names with the SomeType getSomething(), boolean isSomething() and void setSomething(SomeType someValue) pattern (see How to Use the Java™ API (Section 15.1.6) for details). In the example where we changed the button text with setText("x"), we could have achieved the same thing using property syntax. Here's the example again:

Python
button = waitForObject(":frame0.*_javax.swing.JButton")
button.text = "New Text"
test.log(button.text)
JavaScript
var button = waitForObject(":frame0.*_javax.swing.JButton");
button.text = "New Text";
test.log(button.text);
Perl
my $button = waitForObject(":frame0.*_javax.swing.JButton");
$button->text = "New Text";
test::log($button->text);
Tcl
set button [waitForObject ":frame0.*_javax.swing.JButton"]
property set $button text "New Text"
test log [property get $button text]

When Squish encounters code that sets a property it automatically does the appropriate call. For example, using JavaScript, button.setText("x"). Similarly, if we try to read a value using property syntax, Squish will use the appropriate getter syntax, for example (and again using JavaScript), var text = button.text will be treated as var text = button.getText().

[Note]Note

These synthetic properties make it easier to add more verifications points to test scripts. (See How to Create and Use Verification Points (Section 15.3) for more details.)

Squish knows only about a limited set of classes. If you get errors accessing a class method referring to a super class, then it is likely that this class is not wrapped as standard. (See Wrapping custom Java™ classes (Section 16.4.8) how to extend the set of known classes.) Note that Squish 4 automatically wraps all the classes used by the AUT so errors of this kind should no longer occur.

15.1.6.4. How to Use the Java™ Convenience API

This section describes the script API Squish offers on top of Java™'s API to make it easy to perform common user actions such as clicking a button, entering text, etc. A complete list of the API is available in the Java™ Convenience API (Section 16.1.7) section in the Reference Manual (Chapter 16). Below are a few examples to give a flavor of how the API's functions are used.

Python
clickButton(":frame0_Notepad$1")
type(":frame0_javax.swing.JTextArea", "Some text")
activateItem(":frame0_javax.swing.JMenuBar", "File")
activateItem(":frame0.File_javax.swing.JMenu", "Exit")
JavaScript
clickButton(":frame0_Notepad$1");
type(":frame0_javax.swing.JTextArea", "Some text");
activateItem(":frame0_javax.swing.JMenuBar", "File");
activateItem(":frame0.File_javax.swing.JMenu", "Exit");
Perl
clickButton(":frame0_Notepad$1");
type(":frame0_javax.swing.JTextArea", "Some text");
activateItem(":frame0_javax.swing.JMenuBar", "File");
activateItem(":frame0.File_javax.swing.JMenu", "Exit");
Tcl
invoke clickButton ":frame0_Notepad$1"
invoke type ":frame0_javax.swing.JTextArea" "Some text"
invoke activateItem ":frame0_javax.swing.JMenuBar" "File"
invoke activateItem ":frame0.File_javax.swing.JMenu" "Exit"

Here, we have used a Python script to click a button, type in some text, and activate a menu and a menu item. We could use exactly the same code in JavaScript or Perl, except that we would need to add a semi-colon at the end of each line. Many more examples are given later on in the manual—they cover both AWT/Swing and SWT, including interactions with many different widgets such as line edits, spinners, lists, tables, and trees—see How to Test Java™ Applications (Section 15.1.16).

The complete API contains a lot more functions than the three we have shown here. Note also, that the same API works for both AWT/Swing applications and for SWT applications—the only difference is that they have different widgets and different object names.

15.1.6.5. How to Access Java™ Arrays

Some of the methods in the Java™ API return Arrays rather than single objects. The number of elements in such an array is accessible using the length property, and individual elements can be accessed using the at() method parameterized by the array index. Here is an example that lists the JPanel's in a JTabbedPane:

Python
tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane")
components = tabPane.getComponents()
for i in range(components.length):
    test.log("Component #%d: %s" % (i, components.at(i)))
JavaScript
var tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane");
var components = tabPane.getComponents();
for (var i = 0; i < components.length; ++i)
    test.log("Component #" + i + ": " + components.at(i));
Perl
my $tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane");
my $components = $tabPane->getComponents();
for (my $i = 0; $i < $components->length; ++$i) {
    test::log("Component #$i: ". $components->at($i) . "\n");
}
Tcl
set tabPane [waitForObject ":Payment Form_javax.swing.JTabbedPane"]
set components [invoke $tabPane getComponents]
for {set i 0} {$i < [property get $components length]} {incr i} {
    test log [concat "Component #$i: " [toString [invoke $components at $i]]]
}

Another example is shown in the Section 15.1.16.3.2.3, “How to Test Tree”'s tst_tree test script.

15.1.7. How to Use Test Statements

This section discusses the API Squish offers to perform tests that will create test results. Verification points also use this test API; more coverage of verification points is given in the How to Create and Use Verification Points (Section 15.3) section. Working with the test result log is discussed in the Processing Test Results (Section 16.2.3) section.

To compare two values and write the result of the comparison to the test log, use the test.compare() function. To simply check that something is true (i.e., to check a Boolean value), use the test.verify() function. To write some neutral information to the test log at a particular point in the test run, use the test.log() function, and to write a warning to the test log use the test.warning() function.

Here are a few examples that show how to use these functions.

Python
lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName")
test.verify(lineedit.enabled)
test.compare(lineedit.text, "Jane")
test.log("Important note", "This is an important note about the test")
test.warning("Suspicious warning", "This test is incomplete and should be extended!")
JavaScript
var lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
test.verify(lineedit.enabled);
test.compare(lineedit.text, "John");
test.log("Important note", "This is an important note about the test");
test.warning("Suspicious warning", "This test is incomplete and should be extended!");
Perl
my $lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
test::verify($lineedit->enabled);
test::compare($lineedit->text, "Jane");
test::log("Important note", "This is an important note about the test");
test::warning("Suspicious warning", "This test is incomplete and should be extended!");
Tcl
set lineedit [waitForObject "Addressbook.ABCentralWidget1.FirstName"]
test verify [property get $lineedit enabled]
test compare [property get $lineedit text] "John"
test log "Important note" "This is an important note about the test"
test warning "Suspicious warning" "This test is incomplete and should be extended!"

Both the test.log() and test.warning() functions can be given either one or two arguments, the first is the message text, and the optional second argument can be used to provide additional details.

Many other test functions are available, including ones for verifying expected failures and expected exceptions, and various functions for writing messages to the test log. The complete API is documented in the Verification Functions (Section 16.1.3.7) section in the Reference Manual (Chapter 16).

15.1.8. How to Use Event Handlers

In Squish test scripts it is possible to react to events that occur in the AUT. This can be useful, for example, to provide a test script response for when a dialog appears unexpectedly, such as an error message box. This can be done by registering an event handler function for a particular event and that should be called when that event occurs on a specified object, or on an object of a specified type, or for any object.

Event handler functions are registered by calling an installEventHandler() function. For a handler that should apply to all the AUT's objects—that is, a global event handler—just the event type and the handler function are passed as arguments. For a handler that should apply to a particular object or to all objects of a particular type, the object or type is passed as the first argument, followed by the event type and the handler function. In addition to standard toolkit events (such as Qt's QKeyEvent), some Squish- and toolkit-specific generic events are supported such as MessageBoxOpened and Crash.

[Note]Squish/Web-specific

For Squish/Web, event handler functions are always called with no argument, rather than passed an object (typically the object the event happened to). It is still possible to access objects inside Squish/Web event handlers, but we must obtain references to the objects ourselves, for example, using the waitForObject() function.

In the following subsections we will look at example event handlers for all three cases.

15.1.8.1. Global Event Handlers

When a message box pops up the MessageBoxOpened event occurs. (In fact, the MessageBoxOpened event only applies to the Squish/Java™, Squish/Qt, and Squish/Windows editions; however, there are similar events for the other toolkits.) Like all such events the test script will ignore the event, but we can register an event handler function to be called whenever such events occur. It doesn't really make sense to associate a global event like this with a particular object or type, so it is usually handled by a global event handler.

Here we will look at an example of creating and installing a handler for message boxes.

Python
def handleMessageBox(messageBox):
    test.log("MessageBox opened: '%s' - '%s'" % (messageBox.windowText, messageBox.text))
    messageBox.close()

def main():
    installEventHandler("MessageBoxOpened", "handleMessageBox")
    ...
JavaScript
function handleMessageBox(messageBox)
{
    test.log("MessageBox opened: '" + messageBox.windowText + "' - '" + messageBox.text + "'");
    messageBox.close();
}

function main()
{
    installEventHandler("MessageBoxOpened", "handleMessageBox");
    // ...
}
Perl
sub handleMessageBox
{
    my $messageBox = shift @_;
    test::log("MessageBox opened: '" . $messageBox->windowText . "' - '" . $messageBox->text + "'");
    $messageBox->close();
}

sub main
{
    installEventHandler("MessageBoxOpened", "handleMessageBox");
    # ...
}
Tcl
proc handleMessageBox {messageBox} {
    test log [concat "MessageBox opened: '" [property get $messageBox windowText] "' - '" [property get $messageBox text]  "'"]
    invoke $messageBox close
}

proc main {} {
    installEventHandler "MessageBoxOpened" "handleMessageBox"
    # ...
}

Note that if we were using a similar Squish/Web event (e.g., ModalDialogOpened), the dialog would not be passed as an argument, because Squish/Web event handlers have no arguments.

Another special event is Crash. This is useful when we want to install an event handler to be called when the AUT crashes—for example, to do cleanups or to restart the AUT. (The Crash event is supported by all Squish versions, except for Squish/Web.) Here's an example:

Python
def crashHandler():
    test.log("Deleting lock files after AUT crash")
    deleteLockFiles()

def main():
    installEventHandler("Crash", "crashHandler")
    ...
JavaScript
function crashHandler()
{
    test.log("Deleting lock files after AUT crash");
    deleteLockFiles();
}

function main()
{
    installEventHandler("Crash", "crashHandler");
    // ...
}
Perl
sub crashHandler
{
    test::log("Deleting lock files after AUT crash");
    deleteLockFiles();
}

sub main
{
    installEventHandler("Crash", "crashHandler");
    # ...
}
Tcl
proc crashHandler {} {
    test log "Deleting lock files after AUT crash"
    deleteLockFiles
}

proc main {} {
    installEventHandler "Crash" "crashHandler"
    # ...
}

A third kind of special event is the Timeout event. These events are triggered whenever the AUT fails to respond to some Squish command within five minutes. This can happen if the application got stuck in an endless loop, or if there is some other reason that keeps it from being able to respond. You can install an event handler for this event so that your tests can handle such situations gracefully.

15.1.8.2. Event Handlers for All Objects of a Specified Type

It is possible to set up an event handler that will respond to particular types of events for all objects of a specified type. For example, using Squish/Qt, we can install an event handler which is always called when a QMouseEvent occurs on a QCheckBox. This means that every time the event occurs, that is, whenever any of the AUT's checkboxes is clicked, the event handler is called. Here's an example:

Python
def handleCheckBox(obj):
    test.log("QCheckBox '%s' clicked" % objectName(obj))

def main():
    installEventHandler("QCheckBox", "QMouseEvent", "handleCheckBox")
    ...
JavaScript
function handleCheckBox(obj) {
    test.log("QCheckBox '" + objectName(obj) + "' clicked");
}

function main() {
    installEventHandler("QCheckBox", "QMouseEvent", "handleCheckBox");
    // ...
}
Perl
sub handleCheckBox
{
    my $obj = shift @_;
    test::log("QCheckBox '" . objectName($obj) . "' clicked");
}

sub main
{
    installEventHandler("QCheckBox", "QMouseEvent", "handleCheckBox");
    # ...
}
Tcl
proc handleCheckBox {obj} {
    test log [concat "QCheckBox '" [objectName $obj] "' clicked"]
}

proc main {} {
    installEventHandler "QCheckBox" "QMouseEvent" "handleCheckBox"
    # ...
}

Similar event handlers for similar events can be created using the other toolkits that Squish supports, but recall that for Squish/Web, no argument is passed to the event handler, so if we want to interact with an object we must first obtain a reference to it (e.g., using the waitForObject() function.)

15.1.8.3. Event Handlers for Specific Objects

The third kind of event handling that Squish supports is for events that occur to particular objects. For example, again using the Qt toolkit, we could install an event handler that was called every time a line editor received a QKeyEvent, so the event handler would be called every time the test typed some text into the line editor. Here's an example:

Python
def handleDescriptionLineEdit(obj):
    lineEdit = cast(obj, QLineEdit)
    test.log("QLineEdit '%s' text changed: %s" % (objectName(obj), lineEdit.text))

def main():
    lineEdit = waitForObject(":Description:_QLineEdit")
    installEventHandler(lineEdit, "QKeyEvent", "handleDescriptionLineEdit")
    ...
JavaScript
function handleDescriptionLineEdit(obj)
{
    var lineEdit = cast(obj, QLineEdit);
    test.log("QLineEdit '" + objectName(obj) + "' text changed: " + lineEdit.text)
}

function main()
{
    var lineEdit = waitForObject(":Description:_QLineEdit");
    installEventHandler(lineEdit, "QKeyEvent", "handleDescriptionLineEdit");
    // ...
}
Perl
sub handleDescriptionLineEdit
{
    my $obj = shift @_;
    my $lineEdit = cast($obj, QLineEdit);
    test::log("QLineEdit '" . objectName($obj) . "' text changed: " . $lineEdit->text);
}

sub main
{
    my $lineEdit = waitForObject(":Description:_QLineEdit");
    installEventHandler($lineEdit, "QKeyEvent", "handleDescriptionLineEdit");
    # ...
}
Tcl
proc handleDescriptionLineEdit {obj} {
    set lineEdit [cast $obj QLineEdit]
    test log [concat "QLineEdit '" [objectName $obj] "' text changed: " [toString [property get $lineEdit text]]]
}

proc main {} {
    set lineEdit [waitForObject ":Description:_QLineEdit"]
    installEventHandler $lineEdit "QKeyEvent" "handleDescriptionLineEdit"
    # ...
}

The object passed as obj is just a generic Squish object; we must cast it to an object of the correct type using the cast() function, to be able to access the object's methods and properties.

15.1.9. How to Create and Use Synchronization Points

When recording a script in Squish, the event recorder must ensure that the AUT and the test script are synchronized. One way of achieving this is for the recorder to automatically insert snooze() statements into the script. These statements force the script to wait for a specified number of seconds (which might be a fractional amount such as 2.5). This is necessary to ensure that a script is replayed at the same speed as it was recorded. For example, if the user waited for a window to pop up, the script will wait for the same amount of time. This is important to prevent Squish from running the AUT too fast for the AUT's toolkit to keep up.

Using snooze() statements is the simplest way to synchronize the AUT and a test script. But in many cases, simply waiting for a certain amount of time isn't sufficient. For example, if a script is recorded on a fast machine and later replayed on a slow machine the time waited by snooze() might not be long enough.

Another way of synchronizing is to use waitForObject() statements instead of snooze() statements. If the waitForObject() function is used, before every action that is recorded, a waitForObject() statement will be recorded so that the object can be accessed. So on replay, instead of waiting for a specific amount of time, Squish will wait for the given object to exist and be accessible (i.e., visible). Since using the waitForObject() function has proved much more reliable than using the snooze(), it is the default method used when recording new tests.

A third alternative is to use the waitFor() function. This function waits until a given condition becomes true, or optionally, until a specified time out expires. The condition can be anything from a property to a complex script statement. Here is an example that waits for a particular dialog to pop up, and logs a fatal error if the dialog doesn't appear within 5 seconds:

Python
ok = waitFor("object.exists('Addressbook.FileSave')", 5000)
if not ok:
    test.fatal("Addressbook.FileSave dialog didn't appear")
JavaScript
var ok = waitFor("object.exists('Addressbook.FileSave')", 5000);
if (!ok)
    test.fatal("Addressbook.FileSave dialog didn't appear");
Perl
my $ok = waitFor("object::exists('Addressbook.FileSave')", 5000);
if (!$ok) {
    test::fatal("Addressbook.FileSave dialog didn't appear");
}
Tcl
set ok [waitFor "object exists 'Addressbook.FileSave'" 5000]
if {!$ok} {
    test fatal "Addressbook.FileSave dialog didn't appear"
}

Here is another example, this time one that will wait forever since no timeout is specified. So if the expected file doesn't exist and isn't created, the test script will be stuck:

Python
waitFor("QFile.exists('addresses.tsv')")
JavaScript
waitFor("QFile.exists('addresses.tsv')");
Perl
waitFor("QFile::exists('addresses.tsv')");
Tcl
waitFor "invoke QFile exists 'addresses.tsv'"

This last example waits up to 2 seconds for an Add button to become disabled:

Python
button = waitForObject("Addressbook.ABCentralWidget1.AddButton")
ok = waitFor("button.enabled" 2000)
if ok == False:
    test.fatal("Add button still enabled after 2 seconds")
JavaScript
var button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
var ok = waitFor("button.enabled" 2000);
if (ok == false)
    test.fatal("Add button still enabled after 2 seconds");
Perl
my $button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
my $ok = waitFor("$button->enabled" 2000);
if ($ok == 0) {
    test::fatal("Add button still enabled after 2 seconds");
}
Tcl
set button [waitForObject "Addressbook.ABCentralWidget1.AddButton"]
set ok [waitFor {property get $button enabled} 2000]
if {$ok == false} {
    test fatal "Add button still enabled after 2 seconds"
}

These examples show different variations of synchronization points. As the condition which is passed to the waitFor() function can be any script code which can be evaluated, including function calls, there are no limits to creating synchronization points.

More on synchronization for Web applications and advanced AJAX synchronization can be found at How to Synchronize Web Page Loading for Testing (Section 15.1.5.7).

15.1.10. How to Test Multiple AUTs from a Single Test Script, Using ApplicationContext

Usually, a single application under test is specified for each test suite. This AUT is then executed and accessed by each test case. All the tutorials show this one test suite/one AUT approach, but in fact it is possible to start multiple applications and access and test all of them from within a single test suite. This makes it possible to test the interaction between different applications or between multiple instances of the same application. For example, being able to test multiple applications is essential for testing client/server systems.

Whenever an AUT is started a corresponding Application Context Functions (Section 16.1.3.10) object is created, and it is this object that is used by Squish to provide access to the AUT. Squish allows us to access the ApplicationContext object directly in our code, and this means that we can query the AUT for information such as the command line it was launched with, its current state, and so on. This information can also be accessed when a single AUT is used by making use of the context object returned by the defaultApplicationContext() function.

15.1.10.1. How to Start and Access Multiple Applications Under Test

When testing multiple applications from a single test script, the first step is to ensure that no application is set to be automatically started. Using the Squish IDE, click the Test Suite Settings toolbar button (in the Test Suites view) to make the test suite's Settings editor view visible. Now, in the editor's "Application Under Test (AUT)" section, make sure that the Automatically start the AUT checkbox is unchecked.

The function used to start an application is startApplication(). This function starts the given application (assuming it is located in an application path—see AUTs and Settings (Section 16.4)) using the given command line arguments and returns the corresponding ApplicationContext object. The application context object is a handle that refers to the application.

Optionally, as the second and third parameters, a host and port can be passed to the startApplication() function. This way, the startApplication() function will connect to the squishserver on the specified host and listen to the specified port, instead of using the default host and port (as specified in the Squish IDE's settings or on the squishrunner's command line). This allows us to control multiple applications on multiple computers from a single test script.

Special care must be taken if the application is using a different GUI toolkit than the test suite's default toolkit. The global testSettings Object (Section 16.1.3.13) object allows us to set the configuration of the toolkit wrapper on a per-AUT basis. See the testSettings.setWrappersForApplication() function for details on how to do this.

If we run two or more AUTs within a test script, which one should test code apply to? We can make one of the AUTs the active application by using the setApplicationContext() function, passing an ApplicationContext as the sole parameter. Once the call is made, all script code applies to the active application—unless another setApplicationContext() call is made to change the active application. Note that whenever we call the startApplication() function, not only is the application's ApplicationContext object returned, but the application is automatically set to be the active application.

We can obtain a list of all the currently running AUTs' ApplicationContext objects, by calling the applicationContextList() function. And we can retrieve the context object of the active application by calling the currentApplicationContext() function.

[Note]Note

If you want to record and access applications which are started by the AUT itself, and not by Squish, see the Recording the Sub-Processes started by the AUT (Section 16.7.1) section.

We will now look at some examples that show how to start multiple AUTs and how to use ApplicationContext objects to query them.

We will take as an example a client/server chat system. The system has a chat server called chatserver which must be running for communication to take place, and two chat clients, one written in Qt called chatclientqt, and the other written in Java called chatclientjava.

In the test we will first start the chat server. Then we start two clients; these automatically connect to the chat server at startup. We will then type something into the message editor of the first client and check that the second client received the message.

Python
startApplication("chatserver")
client1 = startApplication("chatclientqt", "Qt")
client2 = startApplication("chatclientjava", "Java")

setApplicationContext(client1)
editor = waitForObject("ChatWindow.messageEditor")
type(editor, "Message for client #2")

setApplicationContext(client2)
msgView = waitForObject("ChatWindow.messageView")
test.compare(msgView.text, "Message for client #2")
JavaScript
startApplication("chatserver");
var client1 = startApplication("chatclientqt", "Qt");
var client2 = startApplication("chatclientjava", "Java");

setApplicationContext(client1);
var editor = waitForObject("ChatWindow.messageEditor");
type(editor, "Message for client #2");

setApplicationContext(client2);
var msgView = waitForObject("ChatWindow.messageView");
test.compare(msgView.text, "Message for client #2");
Perl
startApplication("chatserver");
my $client1 = startApplication("chatclientqt", "Qt");
my $client2 = startApplication("chatclientjava", "Java");

setApplicationContext($client1);
my $editor = waitForObject("ChatWindow.messageEditor");
type($editor, "Message for client #2");

setApplicationContext($client2);
my $msgView = waitForObject("ChatWindow.messageView");
test::compare($msgView->text, "Message for client #2");
Tcl
startApplication "chatserver"
set client1 [startApplication "chatclientqt" "Qt"]
set client2 [startApplication "chatclientjava" "Java"]

setApplicationContext $client1
set editor [waitForObject "ChatWindow.messageEditor"]
invoke type $editor "Message for client #2"

setApplicationContext $client2
set msgView [waitForObject "ChatWindow.messageView"]
test verify [string equal [property get $msgView text] "Message for client #2"]

We begin by starting each of the applications in turn, although we only keep references to the client AUTs' ApplicationContext objects since we don't directly access the server in the test. Once the applications are running we make the first client the active AUT since the active AUT is currently client2 since that was the AUT started by the most recent startApplication() call. Then we get a reference to the client's chat editor and type some text into it. And at the end, we make the second client the active AUT, get a reference to its chat editor (a different widget this time since the toolkit is different—Java rather than Qt), and we compare the second client's editor's text with the text we sent from the first client. (For the Tcl version we prefer to use the test.verify() function rather than test.compare() because it is usually more convenient.)

15.1.10.2. How to Use ApplicationContext Objects

It is possible to use an ApplicationContext object to retieve information about the AUT it refers to. The application context of the AUT defined in the test suite settings can be retrieved using the defaultApplicationContext() function. When multiple AUTs are started there should not be any AUT defined in the test suite settings—each AUT's context object can be retrieved as the return value of the call to the startApplication() function which is used to start the AUT, or from the applicationContextList() function which returns all the AUTs' context objects.

The Application Context Functions (Section 16.1.3.10) section details the properties and functions that are accessible from ApplicationContext objects. Here are some examples.

Python
ctx = defaultApplicationContext()
test.log(ctx.commandLine)
test.log(ctx.cwd)
JavaScript
var ctx = defaultApplicationContext();
test.log(ctx.commandLine);
test.log(ctx.cwd);
Perl
my $ctx = defaultApplicationContext();
test::log($ctx->commandLine);
test::log($ctx->cwd);
Tcl
set ctx [defaultApplicationContext]
test log [property get $ctx commandLine]
test log [property get $ctx cwd]

Here we print the command line the AUT was invoked with and its current working directory—both are properties.

Python
ctx = startApplication("myapp")
peakMemory = 0
while ctx.isRunning:
    peakMemory = max(ctx.usedMemory, peakMemory)
    if not ctx.isFrozen(20):
	break
test.log(peakMemory)
JavaScript
ctx = startApplication("myapp");
var peakMemory = 0;
while (ctx.isRunning) {
    peakMemory = Math.max(ctx.usedMemory, peakMemory);
    if (!ctx.isFrozen(20))
	break;
}
test.log(peakMemory);
Perl
my $ctx = startApplication("myapp");
my $peakMemory = 0;
while ($ctx->isRunning) {
    if ($ctx->usedMemory > $peakMemory) {
	$peakMemory = $ctx->usedMemory;
    }
    if (!$ctx->isFrozen(20)) {
	last;
    }
}
test::log($peakMemory)

Here we start an application called myapp and so long as it is running we keep track of the maximum amount of memory it is using. (The memoryUsage property is only available in conjunction with the Squish memory module add-on.) We break out of the loop if the application stops running (in which case isRunning will be false), or if the application becomes unresponsive (frozen), after waiting 20 seconds.

Python
ctx = startApplication("myapp")
test.log("STDOUT", ctx.readStdout())
test.warning("STDERR", ctx.readStderr())
JavaScript
var ctx = startApplication("myapp");
test.log("STDOUT", ctx.readStdout());
test.warning("STDERR", ctx.readStderr());
Perl
my $ctx = startApplication("myapp");
test::log("STDOUT", $ctx->readStdout());
test::warning("STDERR", $ctx->readStderr());
Tcl
set ctx [startApplication "myapp"]
test log "STDOUT" [invoke $ctx readStdout]
test warning "STDERR" [invoke $ctx readStderr]

Here we have added everything that the AUT has written to stdout and stderr to the test log, classifying all stderr messages as warnings.

15.1.11. How to Test Qt Widgets

In this section we will see how the Squish API makes it straightforward to check the values and states of individual widgets so that we can test our application's business rules.

As we saw in the tutorial, we can use Squish's recording facility to create tests. However, it is often useful to modify such tests, or create tests entirely from scratch in code, particularly when we want to test business rules that involves multiple widgets.

In general there is no need to test a widget's standard behavior. For example, if an unchecked two-valued checkbox isn't checked after being clicked, that's a bug in the toolkit not in our code. If such a case arose we may need to write a workaround (and write tests for it), but normally we don't write tests just to check that a widget behaves as documented. On the other hand, what we do want to test is whether our application provides the business rules we intended to build into it. Some tests concern individual widgets in isolation—for example, testing that a combobox contains the appropriate items. Other tests concern inter-widget dependencies and interactions. For example, if we have a group of "payment method" radio buttons, we will want to test that if the "cash" radio button is chosen the check and credit card-relevant widgets are all hidden.

Whether we are testing individual widgets or inter-widget dependencies and interactions, we must first be able to identify the widgets we want to test. Once identified we can then verify that they have the values and are in the states that we expect. One way to identify a widget is to record a test that involves its use and see what name Squish uses for it. But the easiest way to identify a widget so that we can use it in our test code is to use the How to Use the Spy (Section 15.2.4) tool. (See also waitForObject().)

The purpose of this section is to explain and show how to access various Qt widgets and perform common operations using these widgets—such as getting and setting their properites—with the Perl, Python, JavaScript, and Tcl scripting languages.

After completing this section you should be able to access Qt widgets, gather data from those Qt widgets, and perform tests against expected values. The principles covered in this chapter apply to all Qt widgets, so even if you need to test a widget that isn't specifically mentioned here, you should have no problem doing so.

[Note]Note

The first time you use any of the script examples that are referenced in this guide, you will need to register the application with the squish server. The test suite will know which application you are using, but the squish server will not have an entry for it, until it is registered. The easiest way to accomplish this is to change the settings on an existing test suite and press the Manage... button next to the application text field. You will now be able to find the application and register it with the squish server.

15.1.11.1. How to Access Widgets

To test and verify a widget and its properties or contents, first we need access to the widget in the test script. To obtain a reference to the widget, the waitForObject() function is used. This function finds the widgets 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 How to Use the Spy (Section 15.2.4) tool.

The steps to find out the name are as follows:

  1. Start the Squish IDE and make the test suite we are working in active

  2. Start the Spy on the application under test

  3. Switch the Spy in Pause mode

  4. Switch to the AUT and work through the GUI until the widget we want to test is visible (e.g. open the dialog it is contained in)

  5. Switch back to the Squish IDE and switch the Spy into Pick mode

  6. Switch to the AUT and click on the widget you want to test

  7. Switch back to the Squish IDE. In the Spy object view the selected widget and its tree will be displayed. Right-click onto the object name and choose "Copy to clipboard".

  8. Exit the Spy

Now the object name we were looking for is saved in the clipboard and we can paste it into the script as the argument to waitForObject().

15.1.11.2. How to Test Widget States and Properties

Each Qt widget has a set of properties and states associated with it that you can query with Squish to perform checks in your test scripts. These properties can be things like, focus (does the widget have focus), enabled (is this widget enabled), visible (is the widget visible), height (what is the height of the widget), width (what is the width of the widget), etc. All of these properties are documented on the Qt/Trolltech web site. Just pick the version of Qt you are running (for example: Qt 4.3 / Qtopia Core 4.3), click on All Classes, and then search for the Qt class name you are looking for.

For example, lets imagine you have a button in your application and you used the Spy tool to discover that the Qt class name for this widget is QPushButton. In the All Classes section of the website, search for QPushButton and select it. You will see that this widget only has a few properties, however, there are additional properties inherited from the QAbstractButton class, and many more properties inherited from the QWidget class, and one property inherited from the QObject class. By visiting each of these parent classes, you will see all of the properties that you can query with Squish in your test scripts. We will see many examples of accessing and testing widget properties in the following setions.

15.1.11.3. How to Test Stateful and Single-Valued Widgets (Qt 4)

In this section we will see how to test the examples/qt4/paymentform example program. This program uses many basic Qt widgets including QCheckBox, QComboBox, QDateEdit, QLineEdit, QPushButton, QRadioButton, and QSpinBox. As part of our coverage of the example we will show how to check the values and state of individual widgets. We will also demonstrate how to test a form's business rules.

The paymentform example in "pay by check" mode.

The paymentform is invoked when an invoice is to be paid, either at a point of sale, or—for credit cards—by phone. The form's Pay button must only be enabled if the correct fields are filled in and have valid values. The business rules that we must test for are as follows:

  • In "cash" mode, i.e., when the Cash QRadioButton is checked:

    • No irrelevant widgets (e.g., account name, account number), must be visible. (Since the form uses a QStackedWidget we only have to check that the cash widget is visible and that the check and card widgets are hidden.)

    • The minimum payment is one dollar and the maximum is $2000 or the amount due, whichever is smaller.

  • In "check" mode, i.e., when the Check QRadioButton is checked:

    • No irrelevant widgets (e.g., issue date, expiry date), must be visible. (In practice we only have to check that the check widget is visible and that the cash and card widgets are hidden.)

    • The minimum payment is $10 and the maximum is $250 or the amount due, whichever is smaller.

    • The check date must be no earlier than 30 days ago and no later than tomorrow.

    • The bank name, bank number, account name, and account number line edits must all be nonempty.

    • The check signed checkbox must be checked.

  • In "card" mode, i.e., when the Card QRadioButton is checked:

    • No irrelevant widgets (e.g., check date, check signed), must be visible. (In practice we only have to check that the card widget is visible and that the check and card widgets are hidden.)

    • The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.

    • For non-Visa cards the issue date must be no earlier than three years ago.

    • The expiry date must be at least one month later than today.

    • The account name and account number line edits must be nonempty.

We will write three tests, one for each of the form's modes. And to make it slightly simpler to check the widgets in the QStackedWidget, we have explicitly given them object names (using QObject's setObjectName() method)—"CashWidget", "CheckWidget", and "CardWidget". In the same way we have also given the name "AmountDueLabel" to the QLabel that displays the amount due.

The source code for the payment form is in the directory SQUISHROOT/examples/qt4/paymentform, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHROOT/examples/qt4/paymentform/suite_py, and the JavaScript version of the tests is in SQUISHROOT/examples/qt4/paymentform/suite_js.

We will begin by reviewing the test script for testing the form's "cash" mode. First we will show the code, then we will explain it. (Don't worry that the code seems long—when we look at the next test script we will see how to break things down into managable pieces.)

Example 15.1. The tst_cash_mode Test Script

Python
def main():
    startApplication("paymentform")
    # Make sure the Cash radio button is checked so we start in the mode
    # we want to test
    cashRadioButtonName = ("{text='Cash' type='QRadioButton' visible='1'"
                           "window=':Make Payment_MainWindow'}")
    cashRadioButton = waitForObject(cashRadioButtonName)
    if not cashRadioButton.checked:
        clickButton(cashRadioButton)
    test.compare(cashRadioButton.checked, True)
    
    # Business rule #1: only the QStackedWidget's CashWidget must be
    # visible in cash mode
    # (The name "CashWidget" was set with QObject::setObjectName())
    cashWidget = waitForObject("{name='CashWidget' type='QLabel'}")
    test.compare(cashWidget.visible, True)
    
    checkWidgetName = "{name='CheckWidget' type='QWidget'}"
    # No waiting for a hidden object
    checkWidget = findObject(checkWidgetName)
    test.compare(checkWidget.visible, False)
    
    cardWidgetName = "{name='CardWidget' type='QWidget'}"
    # No waiting for a hidden object
    cardWidget = findObject(cardWidgetName)
    test.compare(cardWidget.visible, False)
    
    # Business rule #2: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}")
    chars = []
    for char in unicode(amountDueLabel.text):
        if char.isdigit():
            chars.append(char)
    amount_due = cast("".join(chars), int)
    maximum = min(2000, amount_due)
    
    paymentSpinBoxName = ("{buddy=':Make Payment.This Payment:_QLabel'"
                          "type='QSpinBox' unnamed='1' visible='1'}")
    paymentSpinBox = waitForObject(paymentSpinBoxName)
    test.verify(paymentSpinBox.minimum == 1)
    test.verify(paymentSpinBox.maximum == maximum)
    
    # Business rule #3: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ("{type='QPushButton' text='Pay' unnamed='1'"
                     "visible='1'}")
    payButton = waitForObject(payButtonName)
    test.compare(payButton.enabled, True)

JavaScript
function main()
{
    startApplication("paymentform");
    // Make sure the Cash radio button is checked so we start in the mode
    // we want to test
    var cashRadioButtonName = "{text='Cash' type='QRadioButton' visible='1'" +
                              "window=':Make Payment_MainWindow'}";
    var cashRadioButton = waitForObject(cashRadioButtonName);
    if (!cashRadioButton.checked) {
        clickButton(cashRadioButton);
    }
    test.compare(cashRadioButton.checked, true);
    
    // Business rule #1: only the QStackedWidget's CashWidget must be
    // visible in cash mode
    // (The name "CashWidget" was set with QObject::setObjectName())
    var cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");
    test.compare(cashWidget.visible, true);
    
    var checkWidgetName = "{name='CheckWidget' type='QWidget'}";
    // No waiting for a hidden object
    var checkWidget = findObject(checkWidgetName);
    test.compare(checkWidget.visible, false);
    
    var cardWidgetName = "{name='CardWidget' type='QWidget'}";
    // No waiting for a hidden object
    cardWidget = findObject(cardWidgetName);
    test.compare(cardWidget.visible, false);
    
    // Business rule #2: the minimum payment is $1 and the maximum is
    // $2000 or the amount due whichever is smaller
    var amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
    var chars = [];
    var amountDueText = new String(amountDueLabel.text);
    for (var i = 0; i < amountDueText.length; ++i) {
        var ch = amountDueText.charAt(i);
        if ("0123456789".indexOf(ch) > -1) {
            chars.push(ch);
        }
    }
            
    var amount_due = parseFloat(chars.join(""));
    var maximum = Math.min(2000, amount_due);
    
    var paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" +
                             "type='QSpinBox' unnamed='1' visible='1'}";
    var paymentSpinBox = waitForObject(paymentSpinBoxName);
    test.verify(paymentSpinBox.minimum == 1);
    test.verify(paymentSpinBox.maximum == maximum);
    
    // Business rule #3: the Pay button is enabled (since the above tests
    // ensure that the payment amount is in range)
    var payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" +
                        "visible='1'}";
    var payButton = waitForObject(payButtonName);
    test.compare(payButton.enabled, true);
}

Perl
sub main
{
    startApplication("paymentform");
    # Make sure the Cash radio button is checked so we start in the mode
    # we want to test
    my $cashRadioButtonName = "{text='Cash' type='QRadioButton' " .
                              "visible='1'window=':Make Payment_MainWindow'}";
    my $cashRadioButton = waitForObject($cashRadioButtonName);
    if (!$cashRadioButton->checked) {
        clickButton($cashRadioButton);
    }
    test::compare($cashRadioButton->checked, 1);
    
    # Business rule #1: only the QStackedWidget's CashWidget must be
    # visible in cash mode
    # (The name "CashWidget" was set with QObject::setObjectName())
    my $cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");
    test::compare($cashWidget->visible, 1);
    
    $checkWidgetName = "{name='CheckWidget' type='QWidget'}";
    # No waiting for a hidden object
    my $checkWidget = findObject($checkWidgetName);
    test::compare($checkWidget->visible, 0);
    
    my $cardWidgetName = "{name='CardWidget' type='QWidget'}";
    # No waiting for a hidden object
    my $cardWidget = findObject($cardWidgetName);
    test::compare($cardWidget->visible, 0);
    
    # Business rule #2: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    my $amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    my $maximum = 2000 < $amount_due ? 2000 : $amount_due;
        
    my $paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" .
                             "type='QSpinBox' unnamed='1' visible='1'}";
    my $paymentSpinBox = waitForObject($paymentSpinBoxName);
    test::verify($paymentSpinBox->minimum == 1);
    test::verify($paymentSpinBox->maximum == $maximum);
    
    # Business rule #3: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    my $payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" .
                        "visible='1'}";
    my $payButton = waitForObject($payButtonName);
    test::compare($payButton->enabled, 1);
}

Tcl
proc main {} {
    startApplication "paymentform"
    # Make sure the Cash radio button is checked so we start in the mode
    # we want to test
    set cashRadioButtonName {{text='Cash' type='QRadioButton' visible='1' 
            window=':Make Payment_MainWindow'}}
    
    set cashRadioButton [waitForObject $cashRadioButtonName]
    if {![property get $cashRadioButton checked]} {
        invoke clickButton $cashRadioButton
    }
    test compare [property get $cashRadioButton checked] true
    
    # Business rule #1: only the QStackedWidget's CashWidget must be
    # visible in cash mode
    # (The name "CashWidget" was set with QObject::setObjectName())
    set cashWidget [waitForObject {{name='CashWidget' type='QLabel'}}]
    test compare [property get $cashWidget visible] true
    
    set checkWidgetName {{name='CheckWidget' type='QWidget'}}
    # No waiting for a hidden object
    set checkWidget [findObject $checkWidgetName]
    test compare [property get $checkWidget visible] false
    
    set cardWidgetName {{name='CardWidget' type='QWidget'}}
    # No waiting for a hidden object
    set cardWidget [findObject $cardWidgetName]
    test compare [property get $cardWidget visible] false
    
    # Business rule #2: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    set amountDueLabel [waitForObject {{name='AmountDueLabel' type='QLabel'}}]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    set amount_due [expr $amountText]
    set maximum [expr $amount_due < 2000 ? $amount_due : 2000]
    
    set paymentSpinBoxName {{buddy=':Make Payment.This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}}
    set paymentSpinBox [waitForObject $paymentSpinBoxName]
    test compare [property get $paymentSpinBox minimum] 1
    test compare [property get $paymentSpinBox maximum] $maximum
    
    # Business rule #3: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    set payButtonName {{type='QPushButton' text='Pay' unnamed='1'
                        visible='1'}}
    set payButton [waitForObject $payButtonName]
    test compare [property get $payButton enabled] true
}



We must start by making sure that the form is in the mode we want to test. To access visible widgets the process is always the same: we create a variable holding the widget's property-based (real) name, then we call waitForObject() to get a reference to the widget. Once we have the reference we can use it to access the widget's properties and to call the widget's methods. We use this approach to see if the cash radio button is checked, and if it is not, we click it. In either case we then use the test.compare() method to confirm that the cash radio button is checked and ensure that we do the rest of the tests with the form in the correct mode.

Note that the clickButton() function can be used to click any button that inherits QAbstractButton, that is, QCheckBox, QPushButton, QRadioButton, and QToolButton.

The first business rule to be tested is that if the cash widget is visible, the check and card widgets must be hidden. Checking that a widget is visible is easily done by accessing the widget's visible property, and follows exactly the same pattern as we used to access the checked property. But for hidden widgets, the approach is slightly different—we do not (and must not) call waitForObject(); instead we call findObject() immediately. We can use a similar approach to checking that a particular tab page widget in a QTabWidget or particular item widget in a QToolBox is visible.

The second business rule concerns the minimum and maximum allowed payment amounts. As usual we begin by using waitForObject() to get references to the widgets we want—in this case starting with the amount due label. This label's text might contain a currency symbol and grouping markers (for example, $1,700 or €1.700), so to convert this into an integer we must strip away any non-digit characters first. We do this in different ways depending on the underlying scripting language, but in all cases we retrieve the label's text property's characters and convert them to an integer. (For example, in Python, we iterate over each character and join all those that are digits into a single string and use the cast() function which takes an object and the type the object should be converted to, and returns an object of the requested type—or 0 on failure. We use a similar approach in JavaScript, but for Perl and Tcl we simply replace non-digit characters using a regular expression.) The resulting integer is the amount due, so we can now trivially calculate the maximum amount that can be paid in cash.

With the minimum and maximum amounts known we next get a reference to the payment spinbox. (Notice how the spinbox has no name, but is uniquely identified by its buddy—the label beside it.) Once we have a reference to the spinbox we use the test.verify() method to ensure that it has the correct minimum and maximum amounts set. (For Tcl we have used the test.compare() method instead of test.verify() since it is more convenient to do so.)

Checking the last business rule is easy in this case since if the amount is in range (and it must be because we have just checked it), then payment is allowed so the Pay button should be enabled. Once again, we use the same approach to test this: first we call waitForObject() to get a reference to it, and then we conduct the test—in this case checking that the Pay button is enabled.

One interesting aspect of this last test is that if we use the Spy tool it does not give us the name of the Pay button but rather the name of the QDialogButtonBox that contains the button, so we must either give the button an object name or work out its identity for ourselves. We took the latter course, creating a property-name string giving values for the type, text (ignoring ampersands), unnamed, and visible properties. This is sufficient to uniquely identify the Pay button.

Although the "cash" mode test works well, there are a few places where we use essentially the same code. So before creating the test for "check" mode, we will create some common functions that we can use to refactor our tests with. (The process used to create shared code is described a little later in How to Create and Use Shared Data and Shared Scripts (Section 15.4)—essentially all we need to do is create a new script under the Test Suite's shared item's scripts item.) The Python common code is in common.py, the JavaScript common code is in common.js, and so on. We will also create some test-specific functions to make the main() function smaller and easier to understand—and we will put these functions in the test.py file (or test.js and so on) above the main() function.

Example 15.2. The Shared Code

Python
def clickRadioButton(text):
    radioButton = waitForObject("{text='%s' type='QRadioButton' visible='1'"
            "window=':Make Payment_MainWindow'}" % text)
    if not radioButton.checked:
        clickButton(radioButton)
    test.compare(radioButton.checked, True)
    

def getAmountDue():
    amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}")
    chars = []
    for char in unicode(amountDueLabel.text):
        if char.isdigit():
            chars.append(char)
    return cast("".join(chars), int)


def checkVisibleWidget(visible, hidden):
    widget = waitForObject("{name='%s' type='QWidget'}" % visible)
    test.compare(widget.visible, True)
    for name in hidden:
        widget = findObject("{name='%s' type='QWidget'}" % name)
        test.compare(widget.visible, False)


def checkPaymentRange(minimum, maximum):
    paymentSpinBox = waitForObject("{buddy=':Make Payment.This Payment:_QLabel' "
            "type='QSpinBox' unnamed='1' visible='1'}")
    test.verify(paymentSpinBox.minimum == minimum)
    test.verify(paymentSpinBox.maximum == maximum)

JavaScript
function clickRadioButton(text)
{
    var radioButton = waitForObject("{text='" + text + "' type='QRadioButton' " +
            "visible='1' window=':Make Payment_MainWindow'}");
    if (!radioButton.checked) {
        clickButton(radioButton);
    }
    test.compare(radioButton.checked, true);
}  


function getAmountDue()
{
    var amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
    var chars = [];
    var amountDueText = new String(amountDueLabel.text);
    for (var i = 0; i < amountDueText.length; ++i) {
        var ch = amountDueText.charAt(i);
        if ("0123456789".indexOf(ch) > -1) {
            chars.push(ch);
        }
    }
    return parseFloat(chars.join(""));
}


function checkVisibleWidget(visible, hidden)
{
    var widget = waitForObject("{name='" + visible + "' type='QWidget'}");
    test.compare(widget.visible, true);
    for (var i = 0; i < hidden.length; ++i) {
        var name = hidden[i];
        var widget = findObject("{name='" + name + "' type='QWidget'}");
        test.compare(widget.visible, false);
    }
}


function checkPaymentRange(minimum, maximum)
{
    var paymentSpinBox = waitForObject("{buddy=':Make Payment." +
        "This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}");
    test.verify(paymentSpinBox.minimum == minimum);
    test.verify(paymentSpinBox.maximum == maximum);
}

Perl
sub clickRadioButton
{
    my $text = shift(@_);
    my $radioButton = waitForObject("{text='$text' type='QRadioButton' " .
            "visible='1' window=':Make Payment_MainWindow'}");
    if (!$radioButton->checked) {
        clickButton($radioButton);
    }
    test::compare($radioButton->checked, 1);
}

    
sub getAmountDue
{
    my $amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    return $amount_due;
}


sub checkVisibleWidget
{
    my ($visible, @hidden) = @_;
    my $widget = waitForObject("{name='$visible' type='QWidget'}");
    test::compare($widget->visible, 1);
    foreach (@hidden) {
        my $widget = findObject("{name='$_' type='QWidget'}");
        test::compare($widget->visible, 0);
    }
}


sub checkPaymentRange
{
    my ($minimum, $maximum) = @_;
    my $paymentSpinBox = waitForObject("{buddy=':Make Payment." .
            "This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}");
    test::verify($paymentSpinBox->minimum == $minimum);
    test::verify($paymentSpinBox->maximum == $maximum);
}

Tcl
proc clickRadioButton {text} {
    set radioButton [waitForObject "{text='$text' type='QRadioButton' visible='1' window=':Make Payment_MainWindow'}"]
    if (![property get $radioButton checked]) {
        invoke clickButton $radioButton
    }
    test compare [property get $radioButton checked] true
}
    
proc getAmountDue {} {
    set amountDueLabel [waitForObject {{name='AmountDueLabel' type='QLabel'}}]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    return [expr $amountText]
}


proc checkVisibleWidget {visible hidden} {
    set widget [waitForObject "{name='$visible' type='QWidget'}"]
    test compare [property get $widget visible] true
    foreach name $hidden {
        set widget [findObject "{name='$name' type='QWidget'}"]
        test compare [property get $widget visible] false
    }
}


proc checkPaymentRange {minimum maximum} {
    set paymentSpinBox [waitForObject {{buddy=':Make Payment.This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}}]
    test compare [property get $paymentSpinBox minimum] $minimum
    test compare [property get $paymentSpinBox maximum] $maximum
}



The clickRadioButton function is used to click the radio button with the given text—this is used to set the correct page in the widget stack. The getAmoutDue() function reads the text from the amount due label, strips out formatting characters (e.g., commas), and converts the result to an integer. The checkVisibleWidget() function checks that the visible widget is visible and that the hidden widgets are not visible. One subtle point is that for visible widgets we must always use the waitForObject() function but for hidden widgets we must not use it but rather use the findObject() function instead. Finally, the checkPaymentRange() function checks that the payment spinbox's range matches the range we expect it to have.

Now we can write our test for "check" mode and put more of our effort into testing the business rules and less into some of the basic chores. The code we have put in the test.py (or test.js, and so on) file is broken down into several functions. The main() function is special for Squish—this function is the only function that Squish calls in a test, so we are free to add other functions, as we have done here, to make our main function clearer.

We will first show the main() function, and then we will show the functions it calls that are in the same test.py file (since we have already seen the functions that are called from common.py above). Note that in the actual files, the main() function is last but we prefer to show it first for ease of explanation.

Example 15.3. The tst_check_mode Test Script's main() function

Python
def main():
    startApplication("paymentform")
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.py"))

    # Make sure we start in the mode we want to test: check mode
    clickRadioButton("Check")
    
    # Business rule #1: only the CheckWidget must be visible in check mode
    checkVisibleWidget("CheckWidget", ("CashWidget", "CardWidget"))
    
    # Business rule #2: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(10, min(250, amount_due))
    
    # Business rule #3: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow
    today = QDate.currentDate()
    checkDateRange(today.addDays(-30), today.addDays(1))
    
    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'"
                           "visible='1'}")
    test.compare(payButton.enabled, False)
    
    # Business rule #5: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked()
    
    # Business rule #6: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields()
    payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'"
                              "visible='1'}")
    test.compare(payButton.enabled, True)

JavaScript
function main()
{
    startApplication("paymentform");
    // Import functionality needed by more than one test script
    source(findFile("scripts", "common.js"));

    // Make sure we start in the mode we want to test: check mode
    clickRadioButton("Check");
    
    // Business rule #1: only the CheckWidget must be visible in check mode
    checkVisibleWidget("CheckWidget", ["CashWidget", "CardWidget"]);
    
    // Business rule #2: the minimum payment is $10 and the maximum is
    // $250 or the amount due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(10, Math.min(250, amount_due));
    
    // Business rule #3: the check date must be no earlier than 30 days 
    // ago and no later than tomorrow
    var today = QDate.currentDate();
    checkDateRange(today.addDays(-30), today.addDays(1)); 

    // Business rule #4: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use findObject() without waiting
    var payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'" +
                               "visible='1'}");
    test.compare(payButton.enabled, false);
    
    // Business rule #5: the check must be signed (and if it isn't we
    // will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();

    // Business rule #6: the Pay button should be enabled since all the 
    // previous tests pass, the check is signed and now we have filled in
    // the account details
    populateCheckFields();
    payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'" +
                          "visible='1'}");
    test.compare(payButton.enabled, true);
}

Perl
sub main
{
    startApplication("paymentform");
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.pl"));

    # Make sure we start in the mode we want to test: check mode
    clickRadioButton("Check");
    
    # Business rule #1: only the CheckWidget must be visible in check mode
    checkVisibleWidget("CheckWidget", ("CashWidget", "CardWidget"));
    
    # Business rule #2: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    my $amount_due = getAmountDue();
    checkPaymentRange(10, 250 < $amount_due ? 250 : $amount_due);
    
    # Business rule #3: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow
    my $today = QDate::currentDate();
    checkDateRange($today->addDays(-30), $today->addDays(1));
    
    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    my $payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'" .
                               "visible='1'}");
    test::compare($payButton->enabled, 0);
    
    # Business rule #5: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();
    
    # Business rule #6: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields();
    my $payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'" .
                              "visible='1'}");
    test::compare($payButton->enabled, 1);
}

Tcl
proc main {} {
    startApplication "paymentform"
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Make sure we start in the mode we want to test: check mode
    clickRadioButton "Check"
    
    # Business rule #1: only the CheckWidget must be visible in check mode
    checkVisibleWidget "CheckWidget" {"CashWidget" "CardWidget"}
    
    # Business rule #2: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    set amount_due [getAmountDue]
    set maximum [expr 250 > $amount_due ? $amount_due : 250]
    checkPaymentRange 10 $maximum
    
    # Business rule #3: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow
    set today [invoke QDate currentDate]
    set thirtyDaysAgo [toString [invoke $today addDays -30]]
    set tomorrow [toString [invoke $today addDays 1]]
    checkDateRange $thirtyDaysAgo $tomorrow
    
    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    set payButton [findObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
    test compare [property get $payButton enabled] false
    
    # Business rule #5: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked
    
    # Business rule #6: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields
    set payButton [waitForObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
    test compare [property get $payButton enabled] true
}



The source() function is used to read in a script and execute it. Normally such a script is used purely to define things—for example, functions—and these then become available to the test script.

Getting the form into the right mode is now a one-liner thanks to our custom clickRadioButton() function.

All the business rules are similar to before, but in each case the code to test the rule has been reduced to one or two lines thanks to our use of common functions (clickRadioButton(), checkVisibleWidget(), getAmoutDue(), and checkPaymentRange()), and the use of test-specific functions (checkDateRange(), populateCheckFields(), and ensureSignedCheckBoxIsChecked()).

Example 15.4. The tst_check_mode Test Script's other functions

Python
def checkDateRange(minimum, maximum):
    checkDateEdit = waitForObject("{buddy=':Make Payment.Check Date:_QLabel' "
            "type='QDateEdit' unnamed='1' visible='1'}")
    test.verify(checkDateEdit.minimumDate == minimum)
    test.verify(checkDateEdit.maximumDate == maximum)

def ensureSignedCheckBoxIsChecked():
    checkSignedCheckBox = waitForObject("{text='Check Signed' type='QCheckBox' "
            "unnamed='1' visible='0' window=':Make Payment_MainWindow'}")
    if not checkSignedCheckBox.checked:
        clickButton(checkSignedCheckBox)
    test.compare(checkSignedCheckBox.checked, True)

def populateCheckFields():
    bankNameLineEdit = waitForObject("{buddy=':Make Payment.Bank Name:_QLabel' "
            "type='QLineEdit' unnamed='1' visible='1'}")
    type(bankNameLineEdit, "A Bank")
    bankNumberLineEdit = waitForObject(
        "{buddy=':Make Payment.Bank Number:_QLabel' type='QLineEdit' "
        "unnamed='1' visible='1'}")
    type(bankNumberLineEdit, "88-91-33X")
    accountNameLineEdit = waitForObject(
        "{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' "
        "unnamed='1' visible='1'}")
    type(accountNameLineEdit, "An Account")
    accountNumberLineEdit = waitForObject(
        "{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' "
        "unnamed='1' visible='1'}")
    type(accountNumberLineEdit, "932745395")

JavaScript
function checkDateRange(minimum, maximum)
{
    var checkDateEdit = waitForObject("{buddy=':Make Payment." +
        "Check Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}");
    test.verify(checkDateEdit.minimumDate == minimum);
    test.verify(checkDateEdit.maximumDate == maximum);
}

function ensureSignedCheckBoxIsChecked()
{
    var checkSignedCheckBox = waitForObject("{text='Check Signed' " +
        "type='QCheckBox' unnamed='1' visible='0' " +
        "window=':Make Payment_MainWindow'}");
    if (!checkSignedCheckBox.checked) {
        clickButton(checkSignedCheckBox);
    }
    test.compare(checkSignedCheckBox.checked, true);
}

function populateCheckFields()
{
    var bankNameLineEdit = waitForObject("{buddy=':Make Payment." +
        "Bank Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type(bankNameLineEdit, "A Bank");
    var bankNumberLineEdit = waitForObject("{buddy=':Make Payment." +
        "Bank Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type(bankNumberLineEdit, "88-91-33X");
    var accountNameLineEdit = waitForObject("{buddy=':Make Payment." +
        "Account Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type(accountNameLineEdit, "An Account");
    var accountNumberLineEdit = waitForObject("{buddy=':Make Payment." +
        "Account Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type(accountNumberLineEdit, "932745395");
}

Perl
sub checkDateRange
{
    my ($minimum, $maximum) = @_;
    $checkDateEdit = waitForObject("{buddy=':Make Payment.Check Date:_QLabel' " .
	    "type='QDateEdit' unnamed='1' visible='1'}");
    test::verify($checkDateEdit->minimumDate == $minimum);
    test::verify($checkDateEdit->maximumDate == $maximum);
}


sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedCheckBox = waitForObject("{text='Check Signed' " .
            "type='QCheckBox' unnamed='1' visible='0' " .
            "window=':Make Payment_MainWindow'}");
    if (!$checkSignedCheckBox->checked) {
        clickButton($checkSignedCheckBox);
    }
    test::compare($checkSignedCheckBox->checked, 1);
}

sub populateCheckFields
{
    my $bankNameLineEdit = waitForObject("{buddy=':Make Payment." .
            "Bank Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type($bankNameLineEdit, "A Bank");
    my $bankNumberLineEdit = waitForObject(
        "{buddy=':Make Payment.Bank Number:_QLabel' type='QLineEdit' " .
        "unnamed='1' visible='1'}");
    type($bankNumberLineEdit, "88-91-33X");
    my $accountNameLineEdit = waitForObject(
        "{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' " .
        "unnamed='1' visible='1'}");
    type($accountNameLineEdit, "An Account");
    my $accountNumberLineEdit = waitForObject(
        "{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' " .
        "unnamed='1' visible='1'}");
    type($accountNumberLineEdit, "932745395");
}

Tcl
proc checkDateRange {minimum maximum} {
    set checkDateEdit [waitForObject {{buddy=':Make Payment.Check Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}}]
    set minimumDate [toString [property get $checkDateEdit minimumDate]]
    set maximumDate [toString [property get $checkDateEdit maximumDate]]
    test verify [string equal $minimum $minimumDate]
    test verify [string equal $maximum $maximumDate]
}

proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedCheckBox [waitForObject {{text='Check Signed' type='QCheckBox' unnamed='1' visible='0' window=':Make Payment_MainWindow'}}]
    if (![property get $checkSignedCheckBox checked]) {
        invoke clickButton $checkSignedCheckBox
    }
    test compare [property get $checkSignedCheckBox checked] true
}

proc populateCheckFields {} {
    set bankNameLineEdit [waitForObject {{buddy=':Make Payment.Bank Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
    invoke type $bankNameLineEdit "A Bank"
    set bankNumberLineEdit [waitForObject {{buddy=':Make Payment.Bank Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
    invoke type $bankNumberLineEdit "88-91-33X"
    set accountNameLineEdit [waitForObject {{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
    invoke type $accountNameLineEdit "An Account"
    set accountNumberLineEdit [waitForObject {{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
    invoke type $accountNumberLineEdit "932745395"
}



The checkDateRange() function shows how we can test the properties of a QDateEdit. (Note for Tcl users: we have compared dates by converting them to strings.)

The ensureSignedCheckBoxIsChecked() function checks the checkbox if it isn't already checked—and then it checks that the checkbox is checked.

The populateCheckFields() function uses the type() function to simulate the user entering text. It is almost always better to simulate user interaction than to set widget properties directly—after all, it is the application's behavior as experienced by the user that we normally need to test. Once the fields are populated the Pay button should be enabled, and this is checked in the main() function's business rule six after calling the populateCheckFields() function.

Another point to note is that in this form we have two unnamed line edits both with the label "Account Name", and two other's with the label "Account Number". Squish is able to distinguish them because only one of each is visible at any one time. We could of course use setObjectName() to give them unique names if we wanted to.

We are now ready to look at the last test of the form's business logic—the test of "card" mode. Just as with "check" mode we have shortened and simplified the main() function by using functions defined in the common.py (or common.js, and so on) file and by using test-specific functions in the test.py file (or test.js and so on).

Example 15.5. The tst_card_mode Test Script's main() function

Python
def main():
    startApplication("paymentform")
    source(findFile("scripts", "common.py"))

    # Make sure we start in the mode we want to test: card mode
    clickRadioButton("Credit Card")
    
    # Business rule #1: only the CardWidget must be visible in check mode
    checkVisibleWidget("CardWidget", ("CashWidget", "CheckWidget"))
    
    # Business rule #2: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due 
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Business rule #3: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #4: the expiry date must be at least a month later
    # than today---we will make sure this is the case for the later tests
    checkCardDateEdits()
    
    # Business rule #5: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'}")
    test.compare(payButton.enabled, False)
    
    # Business rule #6: the Pay button should be enabled since all the 
    # previous tests pass, and now we have filled in the account details
    populateCardFields()
    payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'"
                              "visible='1'}")
    test.compare(payButton.enabled, True)

JavaScript
function main()
{
    startApplication("paymentform");
    source(findFile("scripts", "common.js"));

    // Make sure we start in the mode we want to test: card mode
    clickRadioButton("Credit Card");
    
    // Business rule #1: only the CardWidget must be visible in check mode
    checkVisibleWidget("CardWidget", ["CashWidget", "CheckWidget"]);
    
    // Business rule #2: the minimum payment is $10 or 5% of the amount due
    // whichever is larger and the maximum is $5000 or the amount due 
    // whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(Math.max(10, amount_due / 20.0), Math.min(5000, amount_due));

    // Business rule #3: for non-Visa cards the issue date must be no
    // earlier than 3 years ago
    // Business rule #4: the expiry date must be at least a month later
    // than today---we will make sure this is the case for the later tests
    checkCardDateEdits();

    // Business rule #5: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use findObject() without waiting
    var payButton = findObject("{type='QPushButton' text='Pay' " +
        "unnamed='1' visible='1'}");
    test.compare(payButton.enabled, false);
    
    // Business rule #6: the Pay button should be enabled since all the 
    // previous tests pass, and now we have filled in the account details
    populateCardFields();
    payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'" +
                              "visible='1'}");
    test.compare(payButton.enabled, true);
}

Perl
sub main
{
    startApplication("paymentform");
    source(findFile("scripts", "common.pl"));

    # Make sure we start in the mode we want to test: card mode
    clickRadioButton("Credit Card");
    
    # Business rule #1: only the CardWidget must be visible in check mode
    checkVisibleWidget("CardWidget", ("CashWidget", "CheckWidget"));
    
    # Business rule #2: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due 
    # whichever is smaller
    my $amount_due = getAmountDue();
    my $paymentSpinBox = waitForObject("{buddy=':Make Payment." .
            "This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}");
    my $fraction = $amount_due / 20.0;
    checkPaymentRange(10 < $fraction ? $fraction : 10,
                      5000 < $amount_due ? 5000 : $amount_due);

    # Business rule #3: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #4: the expiry date must be at least a month later
    # than today---we will make sure this is the case for the later tests
    checkCardDateEdits();
    
    # Business rule #5: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    my $payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'" .
                               "visible='1'}");
    test::compare($payButton->enabled, 0);
    
    # Business rule #6: the Pay button should be enabled since all the 
    # previous tests pass, and now we have filled in the account details
    populateCardFields();
    my $payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'" .
                              "visible='1'}");
    test::compare($payButton->enabled, 1);
}

Tcl
proc main {} {
    startApplication "paymentform"
    source [findFile "scripts" "common.tcl"]

    # Make sure we start in the mode we want to test: card mode
    clickRadioButton "Credit Card"
    
    # Business rule #1: only the CardWidget must be visible in check mode
    checkVisibleWidget "CardWidget" {"CashWidget" "CheckWidget"}
    
    # Business rule #2: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due 
    # whichever is smaller
    set amount_due [getAmountDue]
    set five_percent [expr $amount_due / 20.0]
    set minimum [expr 10 < $five_percent ? $five_percent : 10]
    set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
    checkPaymentRange $minimum $maximum

    # Business rule #3: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #4: the expiry date must be at least a month later
    # than today---we will make sure this is the case for the later tests
    checkCardDateEdits
    
    # Business rule #5: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    set payButton [findObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
    test compare [property get $payButton enabled] false
    
    # Business rule #6: the Pay button should be enabled since all the 
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    set payButton [waitForObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
    test compare [property get $payButton enabled] true
}



The testing of each business rule is very similar to what we did for "check" mode—for example, business rules one and two use the same functions but with different parameters. We have combined the test for business rules three and four into a single test-specific function, checkCardDateEdits(), that we will see in a moment. Business rules five and six work exactly the same way as before only this time we must populate different widgets to enable the Pay button and have created the test-specific populateCardFields() function to do this.

Example 15.6. The tst_card_mode Test Script's other functions

Python
def checkCardDateEdits():
    cardTypeComboBox = waitForObject("{buddy=':Make Payment.Card Type:_QLabel' "
            "type='QComboBox' unnamed='1' visible='1'}")
    for index in range(cardTypeComboBox.count):
        if cardTypeComboBox.itemText(index) != "Visa":
            cardTypeComboBox.setCurrentIndex(index)
            break
    today = QDate.currentDate()
    issueDateEdit = waitForObject("{buddy=':Make Payment.Issue Date:_QLabel' "
            "type='QDateEdit' unnamed='1' visible='1'}")
    test.verify(issueDateEdit.minimumDate == today.addYears(-3))

    expiryDateEdit = waitForObject("{buddy=':Make Payment.Expiry Date:_QLabel' "
            "type='QDateEdit' unnamed='1' visible='1'}")
    type(expiryDateEdit, unicode(today.addMonths(2).toString("MMM yyyy")))

def populateCardFields():
    cardAccountNameLineEdit = waitForObject(
            "{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' "
            "unnamed='1' visible='1'}")
    type(cardAccountNameLineEdit, "An Account")
    cardAccountNumberLineEdit = waitForObject(
            "{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' "
            "unnamed='1' visible='1'}")
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32")

JavaScript
function checkCardDateEdits()
{
    var cardTypeComboBox = waitForObject("{buddy=':Make Payment." +
        "Card Type:_QLabel' type='QComboBox' unnamed='1' visible='1'}");
    for (var index = 0; index < cardTypeComboBox.count; ++index) {
        if (cardTypeComboBox.itemText(index) != "Visa") {
            cardTypeComboBox.setCurrentIndex(index);
            break;
        }
    }
    var today = QDate.currentDate();
    var issueDateEdit = waitForObject("{buddy=':Make Payment." +
        "Issue Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}");
    test.verify(issueDateEdit.minimumDate == today.addYears(-3));

    var expiryDateEdit = waitForObject("{buddy=':Make Payment." +
        "Expiry Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}");
    type(expiryDateEdit, today.addMonths(2).toString("MMM yyyy"));
}

function populateCardFields()
{
    var cardAccountNameLineEdit = waitForObject("{buddy=':Make Payment." +
        "Account Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type(cardAccountNameLineEdit, "An Account");
    var cardAccountNumberLineEdit = waitForObject("{buddy=':Make Payment." +
        "Account Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32");
}

Perl
sub checkCardDateEdits
{
    my $cardTypeComboBox = waitForObject("{buddy=':Make Payment." .
            "Card Type:_QLabel' type='QComboBox' unnamed='1' visible='1'}");
    for (my $index = 0; $index < $cardTypeComboBox->count; $index++) {
        if ($cardTypeComboBox->itemText($index) != "Visa") {
            $cardTypeComboBox->setCurrentIndex($index);
            break;
        }
    }
    my $today = QDate::currentDate();
    my $issueDateEdit = waitForObject("{buddy=':Make Payment." .
            "Issue Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}");
    test::verify($issueDateEdit->minimumDate == $today->addYears(-3));

    my $expiryDateEdit = waitForObject("{buddy=':Make Payment." .
            "Expiry Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}");
    type($expiryDateEdit, $today->addMonths(2)->toString("MMM yyyy"));
}

sub populateCardFields
{
    my $cardAccountNameLineEdit = waitForObject(
            "{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' " .
            "unnamed='1' visible='1'}");
    type($cardAccountNameLineEdit, "An Account");
    my $cardAccountNumberLineEdit = waitForObject(
            "{buddy=':Make Payment.Account Number:_QLabel' " .
            "type='QLineEdit' unnamed='1' visible='1'}");
    type($cardAccountNumberLineEdit, "1343 876 326 1323 32");
}

Tcl
proc checkCardDateEdits {} {
    set cardTypeComboBox [waitForObject {{buddy=':Make Payment.Card Type:_QLabel' type='QComboBox' unnamed='1' visible='1'}}]
    set count [property get $cardTypeComboBox count]
    for {set index 0} {$index < $count} {incr index} {
        if {[invoke $cardTypeComboBox itemText $index] != "Visa"} {
            invoke $cardTypeComboBox setCurrentIndex $index
            break
	}
    }
    set today [invoke QDate currentDate]
    set issueDateEdit [waitForObject {{buddy=':Make Payment.Issue Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}}]
    set maximumIssueDate [toString [property get $issueDateEdit maximumDate]]
    set threeYearsAgo [toString [invoke $today addYears -3]]
    test verify [string equal $maximumIssueDate $threeYearsAgo]

    set expiryDateEdit [waitForObject {{buddy=':Make Payment.Expiry Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}}]
    set date [invoke $today addMonths 2]
    invoke type $expiryDateEdit [invoke $date toString "MMM yyyy"]
}

proc populateCardFields {} {
    set cardAccountNameLineEdit [waitForObject {{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
    invoke type $cardAccountNameLineEdit "An Account"
    set cardAccountNumberLineEdit [waitForObject {{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
    invoke type $cardAccountNumberLineEdit "1343 876 326 1323 32"
}



The checkCardDateEdits() function is used for business rules three and four. For rule three we need the card type combobox to be on any card type except Visa, so we iterate over the combobox's items and set the current item to be the first non-Visa item we find. We then check that the minimum issue date has been correctly set to three years ago. Businesss rule four specifies that the expiry date must be at least a month ahead. We explictly set the expiry to be a couple of months ahead so that the Pay button will be enabled later on. Initially though, the Pay button should be disabled, so the code for business rule five in the main() function checks for this.

For the last business rule we need some fake data for the card account name and number, and this is what the populateCardFields() function generates. After calling this function and having ensured that the dates are in range in the checkCardDateEdits() function, the Pay button should now be enabled. At the end of the main() function we check that this is the case.

We have now completed our review of testing business rules using stateful and single-valued widgets. Qt has other such widgets including QDateTimeEdit, QDial, QDoubleSpinBox, and QTimeEdit, but all of them are identified and tested using the same techniques we have seen here.

15.1.11.4. How to Test Items in Item Views, Item Widgets, and Models (Qt 4)

In this section we will see how to iterate over every item in Qt's item widgets (e.g., QListWidget, QTableWidget, and QTreeWidget), Qt's item views (e.g., QListView, QTableView, and QTreeView), and to extract each item's text and check its checked state and whether it is selected. In fact, for the Q*View classes, we access the underlying model, (e.g., QAbstractItemModel, QAbstractTableModel, or, QStandardItemModel), and iterate over the model's data, since the views themselves display but don't actually hold data.

Although the examples only output each item's text and checked and selected statuses to Squish's log, they are very easy to adapt to do more sophisticated testing, such as comparing actual values against expected values. (With one specified exception, all the code shown in this section is taken from the examples/qt4/itemviews example's test suites.)

15.1.11.4.1. How to Test Items in QListWidgets

It is very easy to iterate over all the items in a list widget and retrieve their texts and check their checked and selected statuses, as the following test example shows:

Example 15.7. The tst_listwidget Test Script

Python
def main():
    startApplication("itemviews")
    listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}"
    listWidget = waitForObject(listWidgetName)
    for row in range(listWidget.count):
        item = listWidget.item(row)
        checked = selected = ""
        if item.checkState() == Qt.Checked:
            checked = " +checked"
        if item.isSelected():
            selected = " +selected"
        test.log("(%d) '%s'%s%s" % (row, item.text(), checked, selected))
JavaScript
function main()
{
    startApplication("itemviews");
    var listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}";
    var listWidget = waitForObject(listWidgetName);
    for (var row = 0; row < listWidget.count; ++row) {
        var item = listWidget.item(row);
        var checked = "";
        var selected = "";
        if (item.checkState() == Qt.Checked) {
            checked = " +checked";
        }
        if (item.isSelected()) {
            selected = " +selected";
        }   
        test.log("(" + String(row) + ") '" + item.text() + "'" + checked + selected);
    }
}

Perl
sub main
{
    startApplication("itemviews");
    my $listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}";
    my $listWidget = waitForObject($listWidgetName);
    for (my $row = 0; $row < $listWidget->count; ++$row) {
        my $item = $listWidget->item($row);
        my $checked = "";
	my $selected = "";
        if ($item->checkState() == Qt::Checked) {
            $checked = " +checked";
	}
        if ($item->isSelected()) {
            $selected = " +selected";
	}
	test::log("($row) '" . $item->text() . "'$checked$selected");
    }
}

Tcl
proc main {} {
    startApplication "itemviews"
    set listWidgetName {{type='QListWidget' unnamed='1' visible='1'}}
    set listWidget [waitForObject $listWidgetName]
    for {set row 0} {$row < [property get $listWidget count]} {incr row} {
        set item [invoke $listWidget item $row]
        set checked ""
        set selected ""
        if {[invoke $item checkState] == [enum Qt Checked]} {
            set checked " +checked"
        }
        if [invoke $item isSelected] {
            set selected " +selected"
        }
        set text [toString [invoke $item text]]
        test log "($row) '$text'$checked$selected"
    }
}



All the output goes to Squish's log, but clearly it is easy to change the script to test against a list of specific values and so on.

15.1.11.4.2. How to Test Items in QListViews (QAbstractItemModels and QItemSelectionModels)

The view classes don't hold any data themselves; instead they visualize the data held in a model. So if we want to access all the items associated with a view we must first retrieve the view's model, and then iterate over the model's items. Furthermore, selections are held separately from the data model—in a selection model. This is because a selection is about visual interaction and does not affect the underlying data. (Of course a user might make a selection and then apply a change to the selection, but from the data model's point of view the change is simply applied to one or more items and the model doesn't know or care how those items were chosen.)

Example 15.8. The tst_listview Test Script

Python
def main():
    startApplication("itemviews")
    listViewName = "{type='QListView' unnamed='1' visible='1'}"
    listView = waitForObject(listViewName)
    model = listView.model()
    selectionModel = listView.selectionModel()
    for row in range(model.rowCount()):
        index = model.index(row, 0)
        text = model.data(index).toString()
        checked = selected = ""
        checkState = model.data(index, Qt.CheckStateRole).toInt()
        if checkState == Qt.Checked:
            checked = " +checked"
        if selectionModel.isSelected(index):
            selected = " +selected"
        test.log("(%d) '%s'%s%s" % (row, text, checked, selected))
JavaScript
function main()
{
    startApplication("itemviews");
    var listViewName = "{type='QListView' unnamed='1' visible='1'}";
    var listView = waitForObject(listViewName);
    var model = listView.model();
    var selectionModel = listView.selectionModel();
    for (var row = 0; row < model.rowCount(); ++row) {
        var index = model.index(row, 0);
        var text = model.data(index).toString();
        var checked = "";
        var selected = "";
        var checkState = model.data(index, Qt.CheckStateRole).toInt();
        if (checkState == Qt.Checked) {
            checked = " +checked";
        }
        if (selectionModel.isSelected(index)) {
            selected = " +selected";
        }
        test.log("(" + String(row) + ") '" + text + "'" + checked + selected);
    }
}

Perl
sub main
{
    startApplication("itemviews");
    my $listViewName = "{type='QListView' unnamed='1' visible='1'}";
    my $listView = waitForObject($listViewName);
    my $model = $listView->model();
    my $selectionModel = $listView->selectionModel();
    for (my $row = 0; $row < $model->rowCount(); ++$row) {
        my $index = $model->index($row, 0);
        my $text = $model->data($index)->toString();
        my $checked = "";
	my $selected = "";
        my $checkState = $model->data($index, Qt::CheckStateRole)->toInt();
        if ($checkState == Qt::Checked) {
            $checked = " +checked";
	}
        if ($selectionModel->isSelected($index)) {
            $selected = " +selected";
	}
	test::log("($row) '$text'$checked$selected");
    }
}

Tcl
proc main {} {
    startApplication "itemviews"
    set listViewName {{type='QListView' unnamed='1' visible='1'}}
    set listView [waitForObject $listViewName]
    set model [invoke $listView model]
    set selectionModel [invoke $listView selectionModel]
    for {set row 0} {$row < [invoke $model rowCount]} {incr row} {
        set index [invoke $model index $row 0]
        set text [toString [invoke [invoke $model data $index] toString]]
        set checked ""
        set selected ""
        set checkState [invoke [invoke $model data $index [enum Qt CheckStateRole]] toInt]
        if {$checkState == [enum Qt Checked]} {
            set checked " +checked"
        }
        if [invoke $selectionModel isSelected $index] {
            set selected " +selected"
        }
        test log "($row) '$text'$checked$selected"
    }
}



Notice that all data in a model is accessed using a QModelIndex. A model index has three attributes: a row, a column, and a parent. For lists only the row is used—the column is always 0; for tables the row and column are used; and for trees all three are used.

Notice also that the checked state is an attribute of the data, so we use the QAbstractItemModel.data() method to access it. (When we use this method without explicitly specifying a role, the role is taken to be Qt.DisplayRole which usually holds the item's text.) The QAbstractItemModel.data() method returns a QVariant, so we must always convert it to the correct type before using it.

In this subsection and the previous one we have seen how to iterate over list widgets and list views to check each item. In the next couple of subsections we will write similar tests for table widgets and table views. In addition we show how to populate a table widget with data—and the same approach can be used for populating list or tree widgets. Populating models is not shown since it is very similar to what we have seen above—we simply call QAbstractItemModel.setData() for each item whose value we want to set, giving an appropriate model index, role, and value.

15.1.11.4.3. How to Test Items in QTableWidgets

In this section we will look at two pieces of example code (in all the main scripting languages that Squish supports). The first example shows how to set the number of rows and columns a table has and how to populate a table with items—including making items checkable and selected—and also how to hide rows. The second example shows how to iterate over every item in a table (but skipping hidden rows), and printing the item's text and state information to Squish's log. (The code shown in this section is taken from the examples/qt4/csvtable example's tst_iterating test suites.)

Example 15.9. Setting up a Table Widget

Python
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}")
    tableWidget.setRowCount(4)
    tableWidget.setColumnCount(3)
    count = 0
    for row in range(tableWidget.rowCount):
        for column in range(tableWidget.columnCount):
            tableItem = QTableWidgetItem("Item %d" % count)
            count += 1
            if column == 2:
                tableItem.setCheckState(Qt.Unchecked)
                if row == 1 or row == 3:
                    tableItem.setCheckState(Qt.Checked)
            tableWidget.setItem(row, column, tableItem)
            if count in (6, 10):
                tableItem.setSelected(True)
    tableWidget.setRowHidden(2, True)

JavaScript
    var tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    tableWidget.setRowCount(4);
    tableWidget.setColumnCount(3);
    var count = 0;
    for (var row = 0; row < tableWidget.rowCount; ++row) {
        for (var column = 0; column < tableWidget.columnCount; ++column) {
            tableItem = new QTableWidgetItem("Item " + new String(count));
            ++count;
            if (column == 2) {
                tableItem.setCheckState(Qt.Unchecked);
                if (row == 1 || row == 3) {
                    tableItem.setCheckState(Qt.Checked);
                }
            }
            tableWidget.setItem(row, column, tableItem);
            if (count == 6 || count == 10) {
                tableItem.setSelected(true);
            }
        }
    }
    tableWidget.setRowHidden(2, true);

Perl
    my $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    $tableWidget->setRowCount(4);
    $tableWidget->setColumnCount(3);
    my $count = 0;
    for (my $row = 0; $row < $tableWidget->rowCount; ++$row) {
        for (my $column = 0; $column < $tableWidget->columnCount; ++$column) {
            my $tableItem = new QTableWidgetItem("Item $count");
            ++$count;
            if ($column == 2) {
                $tableItem->setCheckState(Qt::Unchecked);
                if ($row == 1 || $row == 3) {
                    $tableItem->setCheckState(Qt::Checked);
		}
	    }
            $tableWidget->setItem($row, $column, $tableItem);
            if ($count == 6 || $count == 10) {
                $tableItem->setSelected(1);
	    }
        }
    }
    $tableWidget->setRowHidden(2, 1);

Tcl
    set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
    invoke $tableWidget setRowCount 4
    invoke $tableWidget setColumnCount 3
    set count 0
    for {set row 0} {$row < [property get $tableWidget rowCount]} {incr row} {
	for {set column 0} {$column < [property get $tableWidget columnCount]} {incr column} {
	    set tableItem [construct QTableWidgetItem "Item $count"]
            incr count
            if {$column == 2} {
                invoke $tableItem setCheckState [enum Qt Unchecked]
                if {$row == 1 || $row == 3} {
                    invoke $tableItem setCheckState [enum Qt Checked]
                }
            }
            invoke $tableWidget setItem $row $column $tableItem
            if {$count == 6 || $count == 10} {
		invoke $tableItem setSelected 1
            }
	}
    }
    invoke $tableWidget setRowHidden 2 true



The table that the code produces is shown in the screenshot below:

Naturally, the approach shown in these examples can be used to set other aspects of table widget items, such as their font, background color, text alignment and so on.

Whether we have set up a table using our own test code as shown above, or have a table of data that was populated by some other means (for example, by the AUT loading a data file), we need to be able to iterate over the table's items, and check their text and other attributes. This is exactly what the next example shows.

Example 15.10. Testing a Table Widget's Items

Python
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}")
    for row in range(tableWidget.rowCount):
        if tableWidget.isRowHidden(row):
            test.log("Skipping hidden row %d" % row)
            continue
        for column in range(tableWidget.columnCount):
            tableItem = tableWidget.item(row, column)
            text = unicode(tableItem.text())
            checked = selected = ""
            if tableItem.checkState() == Qt.Checked:
                checked = " +checked"
            if tableItem.isSelected():
                selected = " +selected"
            test.log("(%d, %d) '%s'%s%s" % (row, column, text, checked, selected))    

JavaScript
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    for (var row = 0; row < tableWidget.rowCount; ++row) {
        if (tableWidget.isRowHidden(row)) {
            test.log("Skipping hidden row " + String(row));
            continue;
        }
        for (var column = 0; column < tableWidget.columnCount; ++column) {
            tableItem = tableWidget.item(row, column);
            var text = new String(tableItem.text());
            var checked = "";
            var selected = "";
            if (tableItem.checkState() == Qt.Checked) {
                checked = " +checked";
            }
            if (tableItem.isSelected()) {
                selected = " +selected";
            }
            test.log("(" + String(row) + ", " + String(column) + ") '" +
                     text + "' " + checked + selected);
        }
    }

Perl
    $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    for (my $row = 0; $row < $tableWidget->rowCount; ++$row) {
	if ($tableWidget->isRowHidden($row)) {
            test::log("Skipping hidden row $row");
	    next;
	}
        for (my $column = 0; $column < $tableWidget->columnCount; ++$column) {
            my $tableItem = $tableWidget->item($row, $column);
            my $text = $tableItem->text();
            my $checked = "";
	    my $selected = "";
            if ($tableItem->checkState() == Qt::Checked) {
                $checked = " +checked";
	    }
            if ($tableItem->isSelected()) {
                $selected = " +selected";
	    }
            test::log("($row, $column) '$text'$checked$selected");
	}
    }

Tcl
    set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
    for {set row 0} {$row < [property get $tableWidget rowCount]} {incr row} {
	if {[invoke $tableWidget isRowHidden $row]} {
            test log "Skipping hidden row $row"
	    continue
	}
	for {set column 0} {$column < [property get $tableWidget columnCount]} {incr column} {
	    set tableItem [invoke $tableWidget item $row $column]
            set text [toString [invoke $tableItem text]]
            set checked ""
            set selected ""
            if {[invoke $tableItem checkState] == [enum Qt Checked]} {
                set checked " +checked"
            }
            if {[invoke $tableItem isSelected]} {
                set selected " +selected"
            }
            test log "($row, $column) '$text'$checked$selected"
        }
    }



The log output produced by the above is:

(0, 0) 'Item 0'
(0, 1) 'Item 1'
(0, 2) 'Item 2'
(1, 0) 'Item 3'
(1, 1) 'Item 4'
(1, 2) 'Item 5' checked selected
Skipping hidden row 2
(3, 0) 'Item 9' selected
(3, 1) 'Item 10'
(3, 2) 'Item 11' checked

And as we noted earlier, the same techniques can be used to test other attributes, such as each table item's font, background color, text alignment, and so on.

Another useful way to test an entire table is to compare all its items to a data file in .tsv (tab-separated values format), .csv (comma-separated values format), or .xls (Microsoft® Excel™ spreadsheet format). An example of how to do this is given in How to Test Table Widgets and Use External Data Files (Qt 4) (Section 15.1.11.5).

15.1.11.4.4. How to Test Items in QTableViews (QAbstractItemModels and QItemSelectionModels)

Table views, like all the other view classes, presents the data held in a model rather than holding any data itself. So the key to performing tests on the data shown by a table view is to get the table view's model, and work on the model's data. The example below—which is very similar to the list view example shown earlier—shows how to do this.

Example 15.11. The tst_tableview Test Script

Python
def main():
    startApplication("itemviews")
    tableViewName = "{type='QTableView' unnamed='1' visible='1'}"
    tableView = waitForObject(tableViewName)
    model = tableView.model()
    selectionModel = tableView.selectionModel()
    for row in range(model.rowCount()):
        for column in range(model.columnCount()):
            index = model.index(row, column)
            text = model.data(index).toString()
            checked = selected = ""
            checkState = model.data(index, Qt.CheckStateRole).toInt()
            if checkState == Qt.Checked:
                checked = " +checked"
            if selectionModel.isSelected(index):
                selected = " +selected"
            test.log("(%d, %d) '%s'%s%s" % (row, column, text, checked, selected))

JavaScript
function main()
{
    startApplication("itemviews");
    var tableViewName = "{type='QTableView' unnamed='1' visible='1'}";
    var tableView = waitForObject(tableViewName);
    var model = tableView.model();
    var selectionModel = tableView.selectionModel();
    for (var row = 0; row < model.rowCount(); ++row) {
        for (var column = 0; column < model.columnCount(); ++column) {
            var index = model.index(row, column);
            var text = model.data(index).toString();
            var checked = "";
            var selected = "";
            var checkState = model.data(index, Qt.CheckStateRole).toInt();
            if (checkState == Qt.Checked) {
                checked = " +checked";
            }
            if (selectionModel.isSelected(index)) {
                selected = " +selected";
            }
            test.log("(" + String(row) + ", " + String(column) + ") '" +
                     text + "'" + checked + selected);
        }
    }
}

Perl
sub main
{
    startApplication("itemviews");
    my $tableViewName = "{type='QTableView' unnamed='1' visible='1'}";
    my $tableView = waitForObject($tableViewName);
    my $model = $tableView->model();
    my $selectionModel = $tableView->selectionModel();
    for (my $row = 0; $row < $model->rowCount(); ++$row) {
	for (my $column = 0; $column < $model->columnCount(); ++$column) {
	    my $index = $model->index($row, $column);
	    my $text = $model->data($index)->toString();
	    my $checked = "";
	    my $selected = "";
	    my $checkState = $model->data($index, Qt::CheckStateRole)->toInt();
	    if ($checkState == Qt::Checked) {
		$checked = " +checked";
	    }
	    if ($selectionModel->isSelected($index)) {
		$selected = " +selected";
	    }
	    test::log("($row, $column) '$text'$checked$selected");
	}
    }
}

Tcl
proc main {} {
    startApplication "itemviews"
    set tableViewName {{type='QTableView' unnamed='1' visible='1'}}
    set tableView [waitForObject $tableViewName]
    set model [invoke $tableView model]
    set selectionModel [invoke $tableView selectionModel]
    for {set row 0} {$row < [invoke $model rowCount]} {incr row} {
        for {set column 0} {$column < [invoke $model columnCount]} {incr column} {
            set index [invoke $model index $row $column]
            set text [toString [invoke [invoke $model data $index] toString]]
            set checked ""
            set selected ""
            set checkState [invoke [invoke $model data $index [enum Qt CheckStateRole]] toInt]
            if {$checkState == [enum Qt Checked]} {
                set checked " +checked"
            }
            if [invoke $selectionModel isSelected $index] {
                set selected " +selected"
            }
            test log "($row, $column) '$text'$checked$selected"
        }
    }
}



If we compare the above to the equivalent list view example shown earlier, it is clear that the only difference is that whereas list models only have a single column—column 0—to account for, table models have one or more columns that must be considered.

15.1.11.4.5. How to Test Items in QTreeWidgets

Tree widgets (and models shown in tree views) are rather different to test than list or table widgets and views. This is because trees have a more complex underlying structure. The structure is essentially this: a sequence of rows (top-level items), each of which can have one or more columns, and each of which can have its own row of child items. Each child item can have one or more columns, and can have its own row of child items, and so on.

The easiest way to iterate over a tree is to use a recursive procedure (that its, a procedure that calls itself), starting it off with the tree's "invisible root item", and then working on every item's child items, and their child items, and so on. An example is shown below. (Note that when more than one function is defined in a test, Squish always (and only) calls the one called main—this function can then call the other functions as required.)

Example 15.12. The tst_treewidget Test Script

Python
def checkAnItem(indent, item, root):
    if indent > -1:
        checked = selected = ""
        if item.checkState(0) == Qt.Checked:
            checked = " +checked"
        if item.isSelected():
            selected = " +selected"
        test.log("|%s'%s'%s%s" % (" " * indent, item.text(0), checked, selected))
    else:
        indent = -4
    # Only show visible child items
    if item != root and item.isExpanded() or item == root:
        for row in range(item.childCount()):
            checkAnItem(indent + 4, item.child(row), root)
       
def main():
    startApplication("itemviews")
    treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}"
    treeWidget = waitForObject(treeWidgetName)
    root = treeWidget.invisibleRootItem()
    checkAnItem(-1, root, root)

JavaScript
function checkAnItem(indent, item, root)
{
    if (indent > -1) {
        var checked = "";
        var selected = "";
        if (item.checkState(0) == Qt.Checked) {
            checked = " +checked";
        }
        if (item.isSelected()) {
            selected = " +selected";
        }
        var pad = "";
        for (var i = 0; i < indent; ++i) {
            pad += " ";
        }
        test.log("|" + pad + "'" + item.text(0) + "'" + checked + selected);
    }
    else {
        indent = -4;
    }
    // Only show visible child items
    if (item != root && item.isExpanded() || item == root) {
        for (var row = 0; row < item.childCount(); ++row) {
            checkAnItem(indent + 4, item.child(row), root);
        }
    }
}

function main()
{
    startApplication("itemviews");
    var treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}";
    var treeWidget = waitForObject(treeWidgetName);
    var root = treeWidget.invisibleRootItem();
    checkAnItem(-1, root, root);
}

Perl
sub checkAnItem
{
    my ($indent, $item, $root) = @_;
    if ($indent > -1) {
        my $checked = "";
        my $selected = "";
        if ($item->checkState(0) == Qt::Checked) {
            $checked = " +checked";
	}
        if ($item->isSelected()) {
            $selected = " +selected";
	}
        test::log("|" . " " x $indent . "'" . $item->text(0) . "'" . $checked . $selected);
    }
    else {
        $indent = -4
    }
    # Only show visible child items
    if ($item != $root && $item->isExpanded() || $item == $root) {
        for (my $row = 0; $row < $item->childCount(); ++$row) {
            checkAnItem($indent + 4, $item->child($row), $root);
        }
    }
}
        
sub main
{
    startApplication("itemviews");
    my $treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}";
    my $treeWidget = waitForObject($treeWidgetName);
    my $root = $treeWidget->invisibleRootItem();
    checkAnItem(-1, $root, $root);
}

Tcl
proc checkAnItem {indent item root} {
    if {$indent > -1} {
        set checked ""
        set selected ""
        if {[invoke $item checkState 0] == [enum Qt Checked]} {
            set checked " +checked"
        }
        if [invoke $item isSelected] {
            set selected " +selected"
        }
        set text [toString [invoke $item text 0]]
        set pad [string repeat " " $indent]
        test log "|$pad'$text'$checked$selected"
    } else {
        set indent [expr -4]
    }
    # Only show visible child items
    if {$item != $root && [invoke $item isExpanded] || $item == $root} {
        for {set row 0} {$row < [invoke $item childCount]} {incr row} {
            checkAnItem [expr $indent + 4] [invoke $item child $row] $root
        }
    }
}
       
proc main {} {
    startApplication "itemviews"
    set treeWidgetName {{type='QTreeWidget' unnamed='1' visible='1'}}
    set treeWidget [waitForObject $treeWidgetName]
    set root [invoke $treeWidget invisibleRootItem]
    checkAnItem -1 $root $root
}



The indent is used purely to show the tree's structure when printing out to Squish's log, and the leading |s are used because normally Squish strips whitespace from the ends of log messages and we don't want to do that here. For example:

|'Green algae'
|    'Chlorophytes'
|        'Chlorophyceae'
|        'Ulvophyceae'
|        'Trebouxiophyceae'
|    'Desmids & Charophytes'
|        'Closteriaceae' +checked
|        'Desmidiaceae'
|        'Gonaozygaceae' +selected
|        'Peniaceae'
|'Bryophytes'
|'Pteridophytes'
|    'Club Mosses'
|    'Ferns'
|'Seed plants'
|    'Cycads' +checked +selected
|    'Ginkgo'
|    'Conifers'
|    'Gnetophytes'
|    'Flowering Plants'

Notice that we only check items in the first column—if we need to check items in other columns, we must introduce a loop to iterate over the columns and use a column index rather than simply using the 0 (for the first column) that is shown in the example.

Another point to notice is that the 'Bryophytes' entry actually has three child items ('Liverworts', 'Hornworts', and, 'Mosses'), but these don't appear because the 'Bryophytes' item is collapsed (doesn't show its children and has a + to indicate it is expandable, whereas the others have - to indicate that they are expanded). In the code we ignore non-visible child items—we do this by only calling checkAnItem() if the current item is the root of the tree (i.e., the notional parent of all top-level items), or if the current item is not the root, but is expanded (meaning that its child items are visible in the tree). And we could of course, not skip the non-visible child items, by just removing the last if statement in checkAnItem().

Keep in mind that even if an item is visible, it might not be visible to the user—for example, if the item is not in the tree's visible area. However, it will be visible if the user scrolls to it.

15.1.11.4.6. How to Test Items in QTreeViews (QAbstractItemModels and QItemSelectionModels)

Tree views use a tree-structured model and so the easiest way to iterate over all their model's items is to use a recursive procedure, just as we did for tree widgets in the previous subsection. Here's an example:

Example 15.13. The tst_treeview Test Script

Python
def checkAnItem(indent, index, treeView, model, selectionModel):
    if indent > -1 and index.isValid():
        text = model.data(index).toString()
        checked = selected = ""
        checkState = model.data(index, Qt.CheckStateRole).toInt()
        if checkState == Qt.Checked:
            checked = " +checked"
        if selectionModel.isSelected(index):
            selected = " +selected"
        test.log("|%s'%s'%s%s" % (" " * indent, text, checked, selected))
    else:
        indent = -4
    # Only show visible child items
    if index.isValid() and treeView.isExpanded(index) or not index.isValid():
        for row in range(model.rowCount(index)):
            checkAnItem(indent + 4, model.index(row, 0, index), treeView, model, selectionModel)

        
def main():
    startApplication("itemviews")
    treeViewName = "{type='QTreeView' unnamed='1' visible='1'}"
    treeView = waitForObject(treeViewName)
    model = treeView.model()
    selectionModel = treeView.selectionModel()
    checkAnItem(-1, QModelIndex(), treeView, model, selectionModel)

JavaScript
function checkAnItem(indent, index, treeView, model, selectionModel)
{
    if (indent > -1 && index.isValid()) {
        var text = model.data(index).toString();
        var checked = "";
        var selected = "";
        var checkState = model.data(index, Qt.CheckStateRole).toInt();
        if (checkState == Qt.Checked) {
            checked = " +checked";
        }
        if (selectionModel.isSelected(index)) {
            selected = " +selected";
        }
        var pad = "";
        for (var i = 0; i < indent; ++i) {
            pad += " ";
        }
        test.log("|" + pad + "'" + text + "'" + checked + selected);
    }
    else {
        indent = -4;
    }
    // Only show visible child items
    if (index.isValid() && treeView.isExpanded(index) || !index.isValid()) {
        for (var row = 0; row < model.rowCount(index); ++row) {
            checkAnItem(indent + 4, model.index(row, 0, index), treeView, model,
                selectionModel);
        }
    }
}

function main()
{
    startApplication("itemviews");
    var treeViewName = "{type='QTreeView' unnamed='1' visible='1'}";
    var treeView = waitForObject(treeViewName);
    var model = treeView.model();
    var selectionModel = treeView.selectionModel();
    checkAnItem(-1, new QModelIndex(), treeView, model, selectionModel);
}

Perl
sub checkAnItem
{
    my ($indent, $index, $treeView, $model, $selectionModel) = @_;
    if ($indent > -1 && $index->isValid()) {
        my $text = $model->data($index)->toString();
        my $checked = "";
        my $selected = "";
        my $checkState = $model->data($index, Qt::CheckStateRole)->toInt();
        if ($checkState == Qt::Checked) {
            $checked = " +checked";
	}
        if ($selectionModel->isSelected($index)) {
            $selected = " +selected";
	}
        test::log("|" . " " x $indent . "'" . $text . "'" . $checked . $selected);
    }
    else {
        $indent = -4;
    }
    # Only show visible child items
    if ($index->isValid() && $treeView->isExpanded($index) || !$index->isValid()) {
        for (my $row = 0; $row < $model->rowCount($index); ++$row) {
            checkAnItem($indent + 4, $model->index($row, 0, $index),
                        $treeView, $model, $selectionModel);
        }
    }
}
     
sub main
{
    startApplication("itemviews");
    my $treeViewName = "{type='QTreeView' unnamed='1' visible='1'}";
    my $treeView = waitForObject($treeViewName);
    my $model = $treeView->model();
    my $selectionModel = $treeView->selectionModel();
    checkAnItem(-1, new QModelIndex(), $treeView, $model, $selectionModel);
}

Tcl
proc checkAnItem {indent index treeView model selectionModel} {
    if {$indent > -1 && [invoke $index isValid]} {
        set text [toString [invoke [invoke $model data $index] toString]]
        set checked ""
        set selected ""
        set checkState [invoke [invoke $model data $index [enum Qt CheckStateRole]] toInt]
        if {$checkState == [enum Qt Checked]} {
            set checked " +checked"
        }
        if [invoke $selectionModel isSelected $index] {
            set selected " +selected"
        }
        set pad [string repeat " " $indent]
        test log "|$pad'$text'$checked$selected"
    } else {
        set indent [expr -4]
    }
    # Only show visible child items
    if {[invoke $index isValid] && [invoke $treeView isExpanded $index] || ![invoke $index isValid]} {
        for {set row 0} {$row < [invoke $model rowCount $index]} {incr row} {
            checkAnItem [expr $indent + 4] [invoke $model index $row 0 $index] $treeView $model $selectionModel
        }
    }
}
        
proc main {} {
    startApplication "itemviews"
    set treeViewName {{type='QTreeView' unnamed='1' visible='1'}}
    set treeView [waitForObject $treeViewName]
    set model [invoke $treeView model]
    set selectionModel [invoke $treeView selectionModel]
    checkAnItem -1 [construct QModelIndex] $treeView $model $selectionModel
}



The code here is structurally almost the same as for iterating over the items in a tree widget, only here we use model indexes to identify items. In a model the "invisible root item" is represented by an invalid model index, that is, a model index created without any arguments. (The last statement in the main() functions shown above show how to create an invalid model index.) By using a recursive procedure we ensure that we can iterate over the entire tree, no matter how deep it is.

And just as we did for the QTreeWidget example shown before, for the QTreeView we skip collapsed (non-visible) child items. And we could easily not skip them by just removing the last if statement in checkAnItem().

15.1.11.5. How to Test Table Widgets and Use External Data Files (Qt 4)

In this section we will see how to test the csvtable program shown below. This program uses a QTableWidget to present the contents of a .csv (comma-separated values) file, and provides some basic functionality for manipulating the data—inserting and deleting rows, editing cells, and swapping columns. [5] As we review the tests we will learn how to import test data, manipulate the data, and compare what the QTableWidget shows with what we expect its contents to be. And since the csvtable program is a main-window-style application, we will also learn how to test that menu options and toolbar buttons behave as expected (and implicitly that their underlying actions get carried out). In addition, we will develop some generic functions that may be useful in several different tests.

The csvtable example.

The source code for this example is in the directory SQUISHROOT/examples/qt4/csvtable, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHROOT/examples/qt4/csvtable/suite_py, and the JavaScript version of the tests is in SQUISHROOT/examples/qt4/csvtable/suite_js.

The first test we will look at is deceptively simple and consists of just four executable statements. This simplicity is achieved by putting almost all the functionality into a shared script, to avoid code duplication. Here is the code:

Example 15.14. The tst_loading Test Script

Python
def main():
    startApplication("csvtable")
    source(findFile("scripts", "common.py"))
    doFileOpen("suite_py/shared/testdata/before.csv")
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}")
    compareTableWithDataFile(tableWidget, "before.csv")

JavaScript
function main()
{
    startApplication("csvtable");
    source(findFile("scripts", "common.js"));
    doFileOpen("suite_js/shared/testdata/before.csv");
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    compareTableWithDataFile(tableWidget, "before.csv");
}

Perl
sub main
{
    startApplication("csvtable");
    source(findFile("scripts", "common.pl"));
    doFileOpen("suite_pl/shared/testdata/before.csv");
    my $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    compareTableWithDataFile($tableWidget, "before.csv");
}

Tcl
proc main {} {
    startApplication "csvtable"
    source [findFile "scripts" "common.tcl"]
    doFileOpen "suite_tcl/shared/testdata/before.csv"
    set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
    compareTableWithDataFile $tableWidget "before.csv"
}



We begin by loading in the script that contains common functionality, just as we did in the previous section. Then we call a custom doFileOpen() function that tells the program to open the given file—and this is done through the user interface as we will see. Next we get a reference to the table widget using the waitForObject() function, and finally we check that the table widget's contents match the contents of the data file held amongst the test suite's test data. Note that both the csvtable program and Squish load and parse the data file using their own completely independent code. (See How to Create and Use Shared Data and Shared Scripts (Section 15.4) for how to import test data into Squish.)

Now we will look at the custom functions we have used in the above test.

Example 15.15. Extracts from the Shared Scripts

Python
def doFileOpen(path_and_filename):
    chooseMenuOptionByKey("File", "F", "o")
    waitForObject(":fileNameEdit_QLineEdit")
    components = path_and_filename.split("/")
    for component in components:
        type(":fileNameEdit_QLineEdit", component)
        waitForObject(":fileNameEdit_QLineEdit")
        type(":_QListView", "<Return>")
    

def chooseMenuOptionByKey(menuTitle, menuKey, optionKey):
    windowName = ("{type='MainWindow' unnamed='1' visible='1' "
                  "windowTitle?='CSV Table*'}")
    waitForObject(windowName)
    type(windowName, "<Alt+%s>" % menuKey)
    menuName = "{title='%s' type='QMenu' unnamed='1' visible='1'}" % menuTitle
    waitForObject(menuName)
    type(menuName, optionKey)

    
def compareTableWithDataFile(tableWidget, filename):
    for row, record in enumerate(testData.dataset(filename)):
        for column, name in enumerate(testData.fieldNames(record)):
            tableItem = tableWidget.item(row, column)
            test.compare(testData.field(record, name), tableItem.text())

JavaScript
function doFileOpen(path_and_filename)
{
    chooseMenuOptionByKey("File", "F", "o");
    waitForObject(":fileNameEdit_QLineEdit");
    components = path_and_filename.split("/");
    for (var i = 0; i < components.length; ++i) {
        type(":fileNameEdit_QLineEdit", components[i]);
        waitForObject(":fileNameEdit_QLineEdit");
        type(":fileNameEdit_QLineEdit", "<Return>");
    }
}
    

function chooseMenuOptionByKey(menuTitle, menuKey, optionKey)
{
    windowName = "{type='MainWindow' unnamed='1' visible='1' " +
                  "windowTitle?='CSV Table*'}";
    waitForObject(windowName);
    type(windowName, "<Alt+" + menuKey + ">");
    menuName = "{title='" + menuTitle + "' type='QMenu' unnamed='1' " +
               "visible='1'}";
    waitForObject(menuName);
    type(menuName, optionKey);
}

    
function compareTableWithDataFile(tableWidget, filename)
{
    records = testData.dataset(filename);
    for (var row = 0; row < records.length; ++row) {
        columnNames = testData.fieldNames(records[row]);
        for (var column = 0; column < columnNames.length; ++column) {
            tableItem = tableWidget.item(row, column);
            test.compare(testData.field(records[row], column),
                         tableItem.text());
        }
    }
}

Perl
sub doFileOpen
{
    my $path_and_filename = shift(@_);
    chooseMenuOptionByKey("File", "F", "o");
    waitForObject(":fileNameEdit_QLineEdit");
    my @components = split /\//, $path_and_filename;
    foreach (@components) {
        type(":fileNameEdit_QLineEdit", $_);
        waitForObject(":fileNameEdit_QLineEdit");
        type(":fileNameEdit_QLineEdit", "<Return>");
    }
}
    

sub chooseMenuOptionByKey
{
    my ($menuTitle, $menuKey, $optionKey) = @_;
    my $windowName = "{type='MainWindow' unnamed='1' visible='1' " .
                     "windowTitle?='CSV Table*'}";
    waitForObject($windowName);
    type($windowName, "<Alt+$menuKey>");
    my $menuName = "{title='$menuTitle' type='QMenu' unnamed='1' visible='1'}";
    waitForObject($menuName);
    type($menuName, $optionKey);
}

    
sub compareTableWithDataFile
{
    my ($tableWidget, $filename) = @_;
    my @records = testData::dataset($filename);
    for (my $row = 0; $row < scalar(@records); $row++) {
        my @columnNames = testData::fieldNames($records[$row]);
        for (my $column = 0; $column < scalar(@columnNames); $column++) {
            my $tableItem = $tableWidget->item($row, $column);
            test::compare($tableItem->text(),
                          testData::field($records[$row], $column));
        }
    }
}

Tcl
proc doFileOpen {path_and_filename} {
    chooseMenuOptionByKey "File" "F" "o"
    waitForObject ":fileNameEdit_QLineEdit"
    set components [split $path_and_filename "/"]
    foreach component $components {
        invoke type ":fileNameEdit_QLineEdit" $component
        waitForObject ":fileNameEdit_QLineEdit"
	invoke type ":fileNameEdit_QLineEdit" "<Return>"
    }
}
    

proc chooseMenuOptionByKey {menuTitle menuKey optionKey} {
    set windowName "{type='MainWindow' unnamed='1' visible='1' windowTitle?='CSV Table*'}"
    waitForObject $windowName
    invoke type $windowName "<Alt+$menuKey>"
    set menuName "{title='$menuTitle' type='QMenu' unnamed='1' visible='1'}"
    waitForObject $menuName
    invoke type $menuName $optionKey
}

    
proc compareTableWithDataFile {tableWidget filename} {
    set data [testData dataset $filename]
    for {set row 0} {$row < [llength $data]} {incr row} {
	set columnNames [testData fieldNames [lindex $data $row]]
	for {set column 0} {$column < [llength $columnNames]} {incr column} {
            set tableItem [invoke $tableWidget item $row $column]
            test compare [testData field [lindex $data $row] $column] [invoke $tableItem text]
	}
    }
}



The doFileOpen() function begins by opening a file through the user interface. This is done by using the custom chooseMenuOptionByKey() function. One point to note about the chooseMenuOptionByKey() function is that it uses wildcard matching for the windowTitle property (using ?= instead of equality testing with =; see Improving Object Identification (Section 16.8) for more details.). This is particularly useful for windows that show the current filename or other text that can vary. This function simulates the user clicking Alt+k (where k is a character, for example "F" for the file menu), and then the character that corresponds to the required action, (for example, "o" for "Open"). Once the file open dialog has popped up, for each component of the path and file we want, the doFileOpen() function types in a component followed by Return, and this leads to the file being opened.

When the file is opened, the program is expected to load the file's data. We check that the data has been loaded correctly by comparing the data shown in the table widget and the data file itself. This comparison is done by the custom compareTableWithDataFile() function. This function uses Squish's testData.dataset() function to load in the data so that it can be accessed through the Squish API. We expect every cell in the table to match the corresponding item in the data, and we check that this is the case using the test.compare() function.

Now that we know how to compare a table's data with the data in a file we can perform some more ambitious tests. We will load in the before.csv file, delete the first, last, and a middle row, insert a new row at the beginning and in the middle, and append a new row at the end. Then we will swap three pairs of columns. At the end the data should match the after.csv file.

Rather than writing code to do all these things we can simply record a test script that opens the file and performs all the deletions, insertions, and column swaps. Then we can edit the recorded test script to add a few lines of code to compare the actual results with the expected results. The added lines are shown below, in context:

Example 15.16. Extracts from the tst_editing Script

Python
    type(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit", "Regulatory Citation,Standard")
    waitForObject(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit")
    type(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit", "<Return>")
    # Added by hand
    source(findFile("scripts", "common.py"))
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}")
    compareTableWithDataFile(tableWidget, "after.csv")
    # End of added by hand
    waitForObject(":CSV Table - before.csv.File_QTableWidget")
    type(":CSV Table - before.csv.File_QTableWidget", "<Alt+F>")
    waitForObject(":CSV Table - before.csv.File_QMenu")
    type(":CSV Table - before.csv.File_QMenu", "q")
    waitForObject("{type='QPushButton' unnamed='1' text='No'}")
    clickButton("{type='QPushButton' unnamed='1' text='No'}")

JavaScript
    type(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit", "Regulatory Citation,Standard");
    waitForObject(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit");
    type(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit", "<Return>");
    // Added by hand
    source(findFile("scripts", "common.js"));
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    compareTableWithDataFile(tableWidget, "after.csv");
    // End of added by hand
    waitForObject(":CSV Table - before.csv.File_QTableWidget");
    type(":CSV Table - before.csv.File_QTableWidget", "<Alt+F>");
    waitForObject(":CSV Table - before.csv.File_QMenu");
    type(":CSV Table - before.csv.File_QMenu", "q");
    waitForObject("{type='QPushButton' unnamed='1' text='No'}");
    clickButton("{type='QPushButton' unnamed='1' text='No'}");

Perl
    type(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit", "Regulatory Citation,Standard");
    waitForObject(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit");
    type(":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit", "<Return>");
    # Added by hand
    source(findFile("scripts", "common.pl"));
    my $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    compareTableWithDataFile($tableWidget, "after.csv");
    # End of added by hand
    waitForObject(":CSV Table - before.csv.File_QTableWidget");
    type(":CSV Table - before.csv.File_QTableWidget", "<Alt+F>");
    waitForObject(":CSV Table - before.csv.File_QMenu");
    type(":CSV Table - before.csv.File_QMenu", "q");
    waitForObject("{type='QPushButton' unnamed='1' text='No'}");
    clickButton("{type='QPushButton' unnamed='1' text='No'}");

Tcl
    invoke type ":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit" "Regulatory Citation,Standard"
    waitForObject ":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit"
    invoke type ":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit" "<Return>"
    # Added by hand
    source [findFile "scripts" "common.tcl"]
    set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
    compareTableWithDataFile $tableWidget "after.csv"
    # End of added by hand
    waitForObject ":CSV Table - before.csv.File_QTableWidget"
    invoke type ":CSV Table - before.csv.File_QTableWidget" "<Alt+F>"
    waitForObject ":CSV Table - before.csv.File_QMenu"
    invoke type ":CSV Table - before.csv.File_QMenu" "q"
    waitForObject "{type='QPushButton' unnamed='1' text='No'}"
    invoke clickButton "{type='QPushButton' unnamed='1' text='No'}"



As the extract indictates, the added lines are not inserted at the end of the recorded test script, but rather just before the program is terminated.

We can do other tests of course, for example, checking some of the table's properties. Here is an example that checks that the row and column counts are what we expect them to be:

Example 15.17. Testing a Table Widget's Properties

Python
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}")
    test.verify(tableWidget.rowCount == 12)
    test.verify(tableWidget.columnCount == 5)

JavaScript
    tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    test.verify(tableWidget.rowCount == 12);
    test.verify(tableWidget.columnCount == 5);

Perl
    my $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
    test::verify($tableWidget->rowCount == 12);
    test::verify($tableWidget->columnCount == 5);

Tcl
    set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
    test compare [property get $tableWidget rowCount] 12
    test compare [property get $tableWidget columnCount] 5



This snippet assumes that we have used the source() function to make our custom functions available. (Tcl users note that although the test.verify() method is available it is usually more convenient to use test.compare() method is as we have done here.)

This example shows the power of combining recording with hand editing. If at a later date a new feature was added to the program we could incorporate tests for it in a number of ways. The simplest would be to just add another test script, do the recording, and then add in the three lines needed to compare the table with the expected data. Another approach would be to record the use of the new feature in a temporary test and then copy and paste the recording into the existing test at a suitable place and then change the file to be compared at the end to one that accounts for all the changes to the original data and also the changes that are a result of using the new feature.

15.1.11.6. How to Test QAction, QMenu, and QMenuBar (Qt 4)

If we want to check the properties of a menu's items, we can do so using the Squish IDE and inserting verification points, or directly in code. Here we will show how to use code.

QMenus (and also QWidgets) have a list of QAction objects. We can retrieve this list and iterate over its actions using the QList API, and for each action we can query or set its properties. First we will look at an example of accessing an action's properties, and then we will see the implementation of the custom getAction() function that the example depends on.

Python
editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu")
removeAction = getAction(editMenu, "&Remove Row")
test.verify(not removeAction.enabled)
test.verify(not removeAction.checked)
insertRowAction = getAction(editMenu, "&Insert Row")
test.verify(insertRowAction.enabled)
test.verify(not insertRowAction.checked)
JavaScript
var editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu");
var removeAction = getAction(editMenu, "&Remove Row");
test.verify(!removeAction.enabled);
test.verify(!removeAction.checked);
var insertRowAction = getAction(editMenu, "&Insert Row");
test.verify(insertRowAction.enabled);
test.verify(!insertRowAction.checked);
Perl
my $editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu");
my $removeAction = getAction($editMenu, "&Remove Row");
test::verify(!$removeAction->enabled);
test::verify(!$removeAction->checked);
my $insertRowAction = getAction($editMenu, "&Insert Row");
test::verify($insertRowAction->enabled);
test::verify(!$insertRowAction->checked);
Tcl
set menu [waitForObject ":CSV Table.Edit_QMenu"]
set removeAction [getAction $menu "Disabled"]
test compare [property get $removeAction enabled] 0
test compare [property get $removeAction checked] 0
set insertRowAction [getAction $menu "&Insert Row"]
test compare [property get $insertRowAction enabled] 1
test compare [property get $insertRowAction checked] 0

Here we get a reference to the application's Edit menu and check that the remove row action is disabled and unchecked and that the insert row action is enabled and unchecked. (As is often the case, we prefer to use the test.compare() function rather than the test.verify() function when using Tcl.)

Python
def getAction(widget, text):
    actions = widget.actions()
    for i in range(actions.count()):
        action = actions.at(i)
        if action.text == text:
            return action
JavaScript
function getAction(widget, text)
{
    var actions = widget.actions();
    for (var i = 0; i < actions.length; ++i) {
        var action = actions.at(i);
        if (action.text == text) {
            return action;
        }
    }
}
Perl
sub getAction
{
    my ($widget, $text) = @_;
    my $actions = $widget->actions();
    for (my $i = 0; $i < $actions->count(); ++$i) {
        my $action = $actions->at($i);
        if ($action->text eq $text) {
            return $action;
        }
    }
}
Tcl
proc getAction {widget text} {
    set actions [invoke $widget actions]
    for {set index 0} {$index < [invoke $actions count]} {incr index} {
        set action [invoke $actions at $index]
        set action_text [toString [property get $action text]]
        if {[string equal $action_text $text]} {
            return $action
        }
    }
}

This tiny function retrieves the list of actions for the given widget (or menu), and iterates over them until it finds one with the matching text. It then returns the corresponding action (or null if it doesn't find a match).

15.1.11.7. How to Test Graphics Views, Graphics Scenes and Graphics Items (Qt 4)

Qt 4.2 introduced the graphics/view architecture with the QGraphicsView, QGraphicsScene, and QGraphicsItem classes—and also many QGraphicsItem subclasses. A couple of additional classes were added in Qt 4.4 and another couple in Qt 4.6. Squish provides full support for testing applications that use this architecture.

In this section we will test a simple example application (examples/qt4/shapes) which uses a graphics view as its main window's central area. The scene includes standard widgets, and these provide the means to add additional QGraphicsItems. The Shapes application shown in the screenshot has had several graphics items added and moved.

The shapes example.

The Shapes application's buttons, labels, spinbox, and LCD number widgets are all standard QWidget subclasses, added to the view as QGraphicsProxyWidgets. The user can can add boxes (QGraphicsRectItems), polygons (these are application-specific custom RegularPolygonItem items—they always start out as triangles, but have a context menu for changing them to squares or back to triangles), and text items QGraphicsTextItems, by clicking the appropriate button. Rubber band selection has been switched on for the view to make it easier to select multiple items (but not the widgets of course). The user can move items by dragging them, delete them by selecting them and clicking the Delete button, and change their z order by selecting them and manipulating the spinbox.

In this section we will carry out the following simple test plan to test various features of the Shapes application, and to show how the testing of Qt's graphics/view architecture can be done.

  1. At startup verify that the Add Box, Add Polygon, Add Text, and Quit buttons are enabled and that the Delete button and Z spinbox are disabled.

  2. Add two boxes and verify that the second one's x and y coordinates are 5 pixels more than the first, and that the second one's z value is one more than the first one's.

  3. Add a polygon and confirm that it is a triangle, i.e., that its polygon has exactly three points.

  4. Right-click the triangle and choose the context menu's Square option; then confirm that it has changed to a square, i.e., that its polygon has exactly four points.

  5. Add a text item and confirm that the text entered in the input dialog matches that shown by the text item.

  6. Confirm that the Count LCD shows 4 items and that the Delete button and Z spinbox are enabled.

  7. Select all the items using rubber band selection, i.e., double-click on the background, then click and drag until all the items are selected, then drag them into the middle. Now select just the two boxes using rubber band selection, then click Delete, then click Yes to All. Verify that the Count now shows just 2 items and the Delete button and Z spinbox are disabled.

  8. Quit the application.

We can carry out the test plan using the new Squish IDE as follows. Create a new test suite and a new test case (e.g., a test suite of suite_py and a test case of tst_everything). Now follow all the steps in the test plan—but without worrying about the verifications! At the end you should have a complete recording of your interaction running to about 35 lines in Python and slightly more in other scripting languages.

The next step is to incorporate the verifications. We can either do this directly in code or we can use the Squish IDE. To use the Squish IDE, insert a breakpoint at each place you want a verification to be made and then run the script. The Squish IDE will stop at each breakpoint and you can then insert the verifications. It doesn't matter whether this is done using the Squish IDE or by hand, the results should be just the same.

We inserted lines of code in four different places to perform the verifications we needed. We began as soon as the application had started, verifying that all the buttons were enabled—except for the Delete button—and that the Z spinbox is disabled. Here's the code we inserted to achieve this:

Python
    test.verify(waitForObject(":Add Box_QPushButton").enabled)
    test.verify(waitForObject(":Add Polygon_QPushButton").enabled)
    test.verify(waitForObject(":Add Text..._QPushButton").enabled)
    test.verify(waitForObject(":Quit_QPushButton").enabled)
    test.verify(not findObject(":Delete..._QPushButton").enabled)
    test.verify(not findObject(":_QSpinBox").enabled)

JavaScript
    test.verify(waitForObject(":Add Box_QPushButton").enabled);
    test.verify(waitForObject(":Add Polygon_QPushButton").enabled);
    test.verify(waitForObject(":Add Text..._QPushButton").enabled);
    test.verify(waitForObject(":Quit_QPushButton").enabled);
    test.verify(!findObject(":Delete..._QPushButton").enabled);
    test.verify(!findObject(":_QSpinBox").enabled);

Perl
    test::verify(waitForObject(":Add Box_QPushButton")->enabled);
    test::verify(waitForObject(":Add Polygon_QPushButton")->enabled);
    test::verify(waitForObject(":Add Text..._QPushButton")->enabled);
    test::verify(waitForObject(":Quit_QPushButton")->enabled);
    test::verify(!findObject(":Delete..._QPushButton")->enabled);
    test::verify(!findObject(":_QSpinBox")->enabled);

Tcl
    test verify [property get [waitForObject ":Add Box_QPushButton"] enabled]
    test verify [property get [waitForObject ":Add Polygon_QPushButton"] enabled]
    test verify [property get [waitForObject ":Add Text..._QPushButton"] enabled]
    test verify [property get [waitForObject ":Quit_QPushButton"] enabled]
    test compare [property get [findObject ":Delete..._QPushButton"] enabled] 0
    test compare [property get [findObject ":_QSpinBox"] enabled] 0

For those objects we expect to be enabled we use the waitForObject() function, but for those we expect to be disabled we must use the findObject() function instead. In all cases, we retrieved the object and tested its enabled property.

After two boxes and a polygon are added, we inserted some additional code to check that the second box was properly offset from the first and that the polygon is a triangle (i.e., has three points).

Python
    rectItem1 = waitForObject(":_QGraphicsRectItem")
    rectItem2 = waitForObject(":_QGraphicsRectItem_2")
    test.verify(rectItem1.rect.x + 5 == rectItem2.rect.x)
    test.verify(rectItem1.rect.y + 5 == rectItem2.rect.y)
    test.verify(rectItem1.zValue < rectItem2.zValue)
    polygonItem = waitForObject(":_QGraphicsPolygonItem")
    test.verify(polygonItem.polygon.count() == 3)

JavaScript
    var rectItem1 = waitForObject(":_QGraphicsRectItem");
    var rectItem2 = waitForObject(":_QGraphicsRectItem_2");
    test.verify(rectItem1.rect.x + 5 == rectItem2.rect.x);
    test.verify(rectItem1.rect.y + 5 == rectItem2.rect.y);
    test.verify(rectItem1.zValue < rectItem2.zValue);
    var polygonItem = waitForObject(":_QGraphicsPolygonItem")
    test.verify(polygonItem.polygon.count() == 3);

Perl
    my $rectItem1 = waitForObject(":_QGraphicsRectItem");
    my $rectItem2 = waitForObject(":_QGraphicsRectItem_2");
    test::verify($rectItem1->rect->x + 5 eq $rectItem2->rect->x);
    test::verify($rectItem1->rect->y + 5 eq $rectItem2->rect->y);
    test::verify($rectItem1->zValue lt $rectItem2->zValue);
    my $polygonItem = waitForObject(":_QGraphicsPolygonItem");
    test::verify($polygonItem->polygon->count() == 3);

Tcl
    set rectItem1 [waitForObject ":_QGraphicsRectItem"]
    set rectItem2 [waitForObject ":_QGraphicsRectItem_2"]
    set rectItem1X [property get [property get $rectItem1 rect] x]
    set rectItem1Y [property get [property get $rectItem1 rect] y]
    set rectItem2X [property get [property get $rectItem2 rect] x]
    set rectItem2Y [property get [property get $rectItem2 rect] y]
    test compare $rectItem2X [expr $rectItem1X + 5]
    test compare $rectItem2Y [expr $rectItem1Y + 5]
    test verify [expr [property get $rectItem1 zValue] < [property get $rectItem2 zValue]]
    set polygonItem [waitForObject ":_QGraphicsPolygonItem"]
    test compare [invoke [property get $polygonItem polygon] count] 3 

Here we wait for each of the boxes to be created and then verify that the second box's x and y coordinates are 5 pixels greater than the first box's, and that the second box has a higher z value. We also check that the polygon item's polygon has three points.

The recorded code now right-clicks the polygon item and uses its context menu to change it into a square. It also adds a new text item with the text Some Text. So we have added a third block of code by hand to check that everything is as it should be.

Python
    test.verify(polygonItem.polygon.count() == 4)
    textItem = waitForObject(":_QGraphicsTextItem")
    test.verify(textItem.toPlainText() == "Some Text")
    countLCD = waitForObject(":_QLCDNumber")
    test.verify(countLCD.intValue == 4)
    test.verify(waitForObject(":Delete..._QPushButton").enabled)
    test.verify(waitForObject(":_QSpinBox").enabled)

JavaScript
    test.verify(polygonItem.polygon.count() == 4);
    var textItem = waitForObject(":_QGraphicsTextItem");
    test.verify(textItem.toPlainText() == "Some Text");
    var countLCD = waitForObject(":_QLCDNumber");
    test.verify(countLCD.intValue == 4);
    test.verify(waitForObject(":Delete..._QPushButton").enabled);
    test.verify(waitForObject(":_QSpinBox").enabled);

Perl
    test::verify($polygonItem->polygon->count() == 4);
    my $textItem = waitForObject(":_QGraphicsTextItem");
    test::verify($textItem->toPlainText() eq "Some Text");
    my $countLCD = waitForObject(":_QLCDNumber");
    test::verify($countLCD->intValue == 4);
    test::verify(waitForObject(":Delete..._QPushButton")->enabled);
    test::verify(waitForObject(":_QSpinBox")->enabled);

Tcl
    test compare [invoke [property get $polygonItem polygon] count] 4 
    set textItem [waitForObject ":_QGraphicsTextItem"]
    test compare [invoke $textItem toPlainText] "Some Text" 
    set countLCD [waitForObject ":_QLCDNumber"]
    test compare [invoke $countLCD intValue] 4 
    test verify [property get [waitForObject ":Delete..._QPushButton"] enabled]
    test verify [property get [waitForObject ":_QSpinBox"] enabled]

We begin by verifying that the polygon item now has four points (i.e., that it is now a square). Then we retrieve the text item and verify that its text is what we entered. The QLCDNumber is used to show how many items are present, so we check that it shows the correct number. And finally, we verify that the delete button and Z spinbox are both enabled.

After deleting a couple of items and clicking the view (so that no items are selected), we insert our final lines of verification code.

Python
    countLCD = waitForObject(":_QLCDNumber")
    test.verify(countLCD.intValue == 2)
    test.verify(not findObject(":Delete..._QPushButton").enabled)
    test.verify(not findObject(":_QSpinBox").enabled)

JavaScript
    var countLCD = waitForObject(":_QLCDNumber");
    test.verify(countLCD.intValue == 2);
    test.verify(!findObject(":Delete..._QPushButton").enabled);
    test.verify(!findObject(":_QSpinBox").enabled);

Perl
    my $countLCD = waitForObject(":_QLCDNumber");
    test::verify($countLCD->intValue == 2);
    test::verify(!findObject(":Delete..._QPushButton")->enabled);
    test::verify(!findObject(":_QSpinBox")->enabled);

Tcl
    set countLCD [waitForObject ":_QLCDNumber"]
    test compare [invoke $countLCD intValue] 2 
    test compare [property get [findObject ":Delete..._QPushButton"] enabled] 0
    test compare [property get [findObject ":_QSpinBox"] enabled] 0

Having deleted two items there should only be two left, and so we verify that the QLCDNumber correctly reflects this. Also, with no items selected both the Delete button and the Z spinbox should be disabled, so again we verify this.

These verifications are inserted just before the last line of the recorded script (which clicks the Quit button).

The entire script, containing the recorded and hand added parts is in examples/qt4/shapes/suite_py/tst_everything/test.py (or in suite_js/tst_everything/test.js for JavaScript, and so on for the other languages). Although we added our verifications by hand we could just as easily have added them by inserting breakpoints, navigating to the widgets or items of interest, clicking the properties we wanted to verify and then inserting a scriptified verification point. (It is usually best to use scriptified verifications since they are easiest to hand edit later on if we want to change them.)

Testing graphics/view scenes is no more difficult than testing any other Qt widgets or items. Squish gives sensible symbolic names to each graphics item, so it isn't difficult to identify them—and of course, we can always insert a breakpoint and use the Spy to identify any item we are interested in and to add it to the Object Map.

For some more information about testing graphics/view items, see also the castToQObject() function.

15.1.11.8. How to Test QListView (Qt 3)

This section describes how to test using script code whether the contents of a list view widget meets the expectations.

The first possibility is to iterate over the items in the list view and check the item text. Assuming we have a list view which has one item with the text "Apple" with two children called "Orange" and "Banana", we could use the following Python code to verify this:

listview = waitForObject("<name of list view>")
item = listview.firstChild()
test.compare(item.text(0), "Apple")
child = item.firstChild()
test.compare(child.text(0), "Orange")
sibling = item.nextSibling()
test.compare(sibling.text(0), "Banana")

In addition we want to check that there are no more toplevel items in the list view, meaning the first item has no siblings, so QListViewItem::nextSibling() returns 0 (in JavaScript):

var item = item.nextSibling();
test.verify(isNull(item));

So, we can use the QListViewItem::firstChild() and QListViewItem::nextSibling() functions to traverse the tree of list view items. To retrieve the item text of an item, we use the QListViewItem::text() function and pass the column whose text we want to get back into this function call.

Another possibility to retrieve an item is to use the QListView::findItem() function. This can be used to check whether an item is there or if we don't want to traverse the whole tree of items but we want to start at a specific one. To check that there exists an item with the text "Orange", we would write in Tcl:

set item [invoke $listview findItem "Orange" 0]
test compare [isNull $item] false

The second argument in QListView::findItem() is the column number we search in.

A list view can also contain more sophisticated items like QCheckListItems. Let's assume the "Orange" item is such a check item and we want to verify that this item is checked (in Python):

item = listview.findItem("Orange", 0)
checkitem = cast(item, QCheckListItem)
test.compare(checkitem.state(), QCheckListItem.On)

Since item is of the type QListViewItem, we need to cast it to a QCheckListItems using the cast command. If the cast fails, 0 would be returned. Then we can call the QCheckListItem::state() function on the item and test whether this returns QCheckListItem::On which means the item is checked.

15.1.11.9. How to Test QPopupMenu and QMenuBar (Qt 3)

If you want to check states, etc. of the menu using script code and not use Squish IDE's point & click interface to insert such a verification point, it is also possible to directly access menus from script code.

Similar to QListView, menus contain items which can be retrieved by name and which provide functions to verify the state.

On froglogic's Web site you can find the article Squish: Testing Menus which explains how to test menus using script code in great detail.

15.1.11.10. How to Test QTable (Qt 3)

Similar to QListView, often the contents of a QTable widget needs to be tested. QTable again consists of items which can be retrieved using the QTable::item() function.

To test whether the cell 5/4 contains the text "Kiwi", the following Python code can be used:

table = waitForObject("<name of table>")
cell = table.item(5, 4)
test.compare(cell.text(), "Kiwi")

Similarly to QListView, it is again possible to cast the cell items to more sophisticated items like e.g. QCheckTableItem (in case such items are used in the table) to query properties on those.

15.1.12. How to Test non-Qt Widgets in Qt Applications

Squish/Qt is designed to support automating tests on Qt widgets in Qt applications. However, on some platforms, Qt applications are built using a mixture of Qt and native widgets—for example, on Windows a Qt application may use native Windows dialogs and embedded ActiveX widgets, in addition to Qt widgets.

Fortunately, Squish supports recording and replaying keyboard and mouse operations on all native Windows controls. And in addition, it is possible to inspect the properties of standard Windows controls using Spy, and to insert verifications regarding these controls, and to access their properties inside test scripts. Note also, that there is a specific Squish/Windows edition that works with standard Windows applications such as those created using the MFC or .NET technologies.

15.1.13. How to Test Tk Widgets

This section illustrates how to test Tk applications using Tcl—and in particular, how to test some of the standard Tk widgets. Although only a few widgets are shown, the same principles and practices apply to all Tk widgets, so by the end of this section you should be able to test any of your AUT's widgets.

The most challenging aspect of implementing test scripts is usually when we want to create test verifications. As shown in the chapter Inserting Verification Points (Section 5.3) in the Tutorial: Starting to Test Tcl/Tk Applications (Chapter 13), 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.

15.1.13.1. How to Access Widgets

To test and verify a widget and its properties or contents, we must first get a reference to the widget in the test script. This can be done by calling the waitForObject() function with the object's symbolic or real (multi-property) name, since this function finds the widget with the given name and returns a reference to it.

So the key to verifying the state and properties of any widget is to be able to uniquely identify the widget we are interested in so that we can get a reference to it. Squish provides a number of different ways of finding the name of a widget. One approach is to record a dummy test where we interact with all the widgets we are interested in. This will result in the Object Map (Section 16.9) becoming populated with the names of the objects we interacted with, and we can then simply copy and paste the relevant names into our code.

An alternative to creating a dummy test is to use Squish's Spy tool. (See How to Use the Spy (Section 15.2.4)). Here are the steps to take:

  • Start the Squish IDE and open the AUT's test suite.

  • Start the Spy on the AUT.

  • Switch the Spy into Pause mode.

  • Switch to the AUT and navigate through the GUI until the widget we want to test is visible (e.g. open the dialog it is contained in).

  • Switch back to the Squish IDE and switch the Spy into Pick mode.

  • Switch back to the AUT and click on the widget you want to test.

  • Switch back to the Squish IDE. In the Spy object view the selected widget and its tree will be displayed. Right-click onto the object name and choose "Copy to clipboard".

  • Exit the Spy

In fact, when spying, the entire hierarchy of widgets for the AUT's current window is shown, and we can right-click any of the widgets and choose the Add to Object Map option.

Once we have the name of the widget (object) we are interested in, we can copy and paste it into our script so that it can be used as the argument to the waitForObject() function.

15.1.13.2. How to Test Widget States

One common requirement is to test the state of a widget, in particular whether it is enable or disabled. The widget's state property holds the information we want—here are a couple of examples that show it in use:

set entry1 [waitForObject ":myapp.entry1"]
test compare [property get $entry1 state] "normal"

set entry2 [waitForObject ":myapp.entry2"]
test compare [property get $entry2 state] "disabled"

This code verifies that the entry1 widget is enabled and that the entry2 widget is disabled.

15.1.13.3. Checkbuttons and Radiobuttons

Although the need to verify whether a standard Tk radiobutton or checkbutton is checked is a common requirement, neither of these widgets has a convenient property that we can use, so we must write a little bit more code than might have been expected.

We will start by verifying that a particular radiobutton is checked. First we must retrieve the radiobutton's variable and value properties, and then we must evaluate the variable to see if it is equal to the value—if it is, then the radiobutton is checked.

set radiobutton [waitForObject ":myapp.radiobutton"]
set variable [property get $radiobutton "variable"]
set value [property get $radiobutton "value"]
set actual_value [invoke tcleval "return \$$variable"]
test compare $actual_value $value

First we retrieve a reference to the radiobutton, then we retrieve the two properties we are interested in. Next we evaluate the variable to get its actual value, and finally we compare the actual value with the property value to see if they're the same.

We must use a similar approach for checkbuttons, only they have onvalue and offvalue properties that we must work with.

set checkbutton [waitForObject ":myapp.checkbutton"]
set variable [property get $checkbutton "variable"]
set onvalue [property get $checkbutton "onvalue"]
set actual_value [invoke tcleval "return \$$variable"]
test compare $actual_value $onvalue

Here, we retrieve a reference to the checkbutton, and then to the checkbutton's variable and onvalue properties. And just like we did for the radiobutton, we evaluate the variable to get its actual value, and compare this with the onvalue to see if they are the same.

If we wanted to verify that a checkbutton was not checked, we would simply retrieve the offvalue property and compare that with the actual value—if they are the same, then the checkbutton is not checked.

15.1.13.4. Text fields

A standard Tk entry widget's contents can be queried using the getvalue property.

set entry [waitForObject ":myapp.entry"]
test compare [property get $entry getvalue] "Houston"

Here we check that an entry contains the text Houston.

Querying the contents of Tk's multiline text widget is a bit more involved. For that we must call the widget's get method, giving it the start and end indexes for the text we want to check.

set text [invoke tcleval ".textfield get 1.0 end"]
test compare $text "line 1\nline 2"

Rather than retrieve a reference to the multiline text widget, instead we have used tcleval to execute the widget's get method with indexes that span the entire contents—this will result in all of the widget's text being returned. We then check that the text contains exactly two lines (with texts, line 1 and line 2).

Squish isn't limited to Tk's standard widgets—for example, we can test a BWidget Entry widget.

set bentry [waitForObject ":myapp.bentry"]
test compare [property get $entry text] "Apollo"

Here we retrieve the BWidget's text using its text property, and compare it to the text Apollo.

15.1.13.5. Listbox

One common requirement is to check the text of a Tk listbox's active item. This is easily done using the listbox's get method.

set active [invoke tcleval ".listbox get active"]
test compare $active "Gemini"

Similarly to what we did for the multiline text widget, rather than retrieve a reference to the listbox, instead we have used tcleval to execute the listbox's get method with an argument of active—this will result in the listbox's active item's text being returned. We then compare the text as usual, in this case with the literal text Gemini.

15.1.13.6. iwidget Radiobox

The iwidget Radiobox is different from the standard Tk radiobutton, in that it has a getvalue property that holds the text of its currently checked radiobutton.

set radiobox [waitForObject ":myapp.rbox"]
test compare [property get $radiobox getvalue] "Mercury"

If the Radiobox has radiobuttons, Mercury, Venus, and Mars, we can verify that the Mercury radiobutton is checked by retrieving a reference to the Radiobox, and then comparing the value of its getvalue property to see if it matches the text of the radiobutton that should be checked.

15.1.14. 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 Introducing Verification Points (Section 8.3) in Tutorial: Starting to Test Web Applications (Chapter 8), 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.

15.1.14.1. How to Access Elements

To test and verify an HTML element and its properties or contents, we must first get a reference to the element in the test script. This can be done by calling the waitForObject() function with the elements's symbolic or real (multi-property) name, since this function finds the element with the given name and returns a reference to it.

So the key to verifying the state and properties of any HTML element is to be able to uniquely identify the element we are interested in so that we can get a reference to it. Squish provides a number of different ways of finding the name of an element. One approach is to record a dummy test where we interact with all the elements we are interested in. This will result in the Object Map (Section 16.9) becoming populated with the names of the objects we interacted with, and we can then simply copy and paste the relevant names into our code.

An alternative to creating a dummy test is to use Squish's Spy tool. (See How to Use the Spy (Section 15.2.4)). Here are the steps to take:

  • Start the Squish IDE and open the Web application's test suite.

  • Set a breakpoint in the test script you are working with, at the location where you want to insert a verification.

  • Run the test until there Squish stops at the breakpoint.

  • When the Squish IDE pops up, start the Spy.

  • Switch the Spy into Pick mode.

  • Switch to the Web browser and click on the element you want to test. Since left clicks on form elements and links will trigger actions in the Web page, click with the right mouse button instead—this will allow you to pick the element you want but without avoid triggering any action in the Web page.

  • Switch back to the Squish IDE. In the Spy object view the selected element and its tree will be displayed. Right-click on the object name and choose "Copy to clipboard".

  • Stop the test run.

In fact, when spying, the entire hierarchy of elements for the Web page is shown, and we can right-click any of the elements and choose the Add to Object Map option.

Once we have the name of the element (object) we are interested in, we can copy and paste it into our script so that it can be used as the argument to the waitForObject() function.

15.1.14.2. 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.

Python
entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}")
test.compare(entry.disabled, False)
JavaScript
var entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
test.compare(entry.disabled, false);
Perl
my $entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
test::compare($entry->disabled, 0);
Tcl
set entry [waitForObject "{tagName='INPUT' id='input' form='myform' type='text'}"]
test compare [property get $entry disabled] false

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 compare with true instead.

15.1.14.3. Form Checkboxes and Radiobuttons

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

Python
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}")
test.compare(radiobutton.checked, True)
JavaScript
var radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
test.compare(radiobutton.checked, true);
Perl
my $radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
test::compare($radiobutton->checked, 1);
Tcl
set radiobutton [waitForObject ":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}"]
test compare [property get $radiobutton checked] true

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.

15.1.14.4. Form Text fields

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

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

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

15.1.14.5. 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 item or items are selected.

Python
selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
test.compare(selection.selectedIndex, 2)
test.compare(selection.selectedOption, "Cavalier")
JavaScript
var selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
test.compare(selection.selectedIndex, 2);
test.compare(selection.selectedOption, "Cavalier");
Perl
my $selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
test::compare($selection->selectedIndex, 2);
test::compare($selection->selectedOption, "Cavalier");
Tcl
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.

Python
selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select'}")
options = selection.options()
test.compare(options.optionAt(0).selected, True)     # item at index position 0 is selected
test.compare(options.optionAt(1).selected, False)    # item at index position 1 is not selected
test.compare(options.optionAt(2).selected, True)     # item at index position 2 is selected
test.compare(options.optionAt(1).text, "Round Head") # item at index position 1 has the text "Round Head"
JavaScript
var selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select'}");
var options = selection.options();
test.compare(options.optionAt(0).selected, true);     // item at index position 0 is selected
test.compare(options.optionAt(1).selected, false);    // item at index position 1 is not selected
test.compare(options.optionAt(2).selected, true);     // item at index position 2 is selected
test.compare(options.optionAt(1).text, "Round Head"); // item at index position 1 has the text "Round Head"
Perl
my $selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select'}");
my $options = $selection->options();
test::compare($options->optionAt(0)->selected, 1);        # item at index position 0 is selected
test::compare($options->optionAt(1)->selected, 0);        # item at index position 1 is not selected
test::compare($options->optionAt(2)->selected, 1);        # item at index position 2 is selected
test::compare($options->optionAt(1)->text, "Round Head"); # item at index position 1 has the text "Round Head"
Tcl
set selection [waitForObject ":{tagName='INPUT' id='sel' form='myform' type='select'}"]
set options [invoke $selection options]
# item at index position 0 is selected
test compare [property get [invoke options optionAt 0] selected] true
# item at index position 1 is not selected
test compare [property get [invoke options optionAt 1] selected] false
# item at index position 2 is selected
test compare [property get [invoke options optionAt 2] selected] true)   
# item at index position 1 has the text "Round Head"
test.compare [property get [invoke options 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 and third options are selected, and that the second is not selected. We also verify the second option's text.

15.1.14.6. 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 findObject() function and the waitForObject() function have an HTML_Object.evaluateXPath() 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:

Python
def getCellText(tableObject, row, column):
    return tableObject.evaluateXPath("TBODY/TR[%d]/TD[%d]" % (row + 1, column + 1)).stringValue
JavaScript
function getCellText(tableObject, row, column)
{
    return tableObject.evaluateXPath("TBODY/TR[" + (row + 1) + "]/TD[" + (column + 1) + "]").stringValue;
}
Perl
sub getCellText
{
    my ($tableObject, $row, $column) = @_;
    ++$row;
    ++$column;
    return $tableObject->evaluateXPath("TBODY/TR[$row]/TD[$column]")->stringValue;
}
JavaScript
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 (Section 16.1.9.21); 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:

Python
table = waitForObject(htmlTableName)
text = getCellText(table, 23, 11)
JavaScript
var table = waitForObject(htmlTableName);
var text = getCellText(table, 23, 11);
Perl
my $table = waitForObject($htmlTableName);
my $text = getCellText($table, 23, 11);
Tcl
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 (Section 15.1.5.2).

15.1.14.7. 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.

Python
table = waitForObject(":{tagName='TABLE' id='result_table]'}")
contents = table.innerText
test.verify(contents.find("Total: 387.92") != -1)
JavaScript
var table = waitForObject(":{tagName='TABLE' id='result_table]'}");
var contents = table.innerText;
test.verify(contents.indexOf("Total: 387.92") != -1);
Perl
my $table = waitForObject(":{tagName='TABLE' id='result_table]'}");
my $contents = $table->innerText;
test::verify(index($contents, "Total: 387.92") != -1);
Tcl
set table [waitForObject ":{tagName='TABLE' id='result_table]'}"]
set contents [property get $table innerText]
test verify [string first $contents "Total: 387.92"] != -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.

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

Notice that we must use the property() 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.

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

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

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

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

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

We can easily use the waitFor() 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.)

15.1.15. How to Automate Native Browser Dialogs, Java Applets, Flash/Flex, ActiveX, and more

Squish is primarily designed to support the automation of operations on web pages' DOM, DHTML, and HTML elements. But to completely test a web application, it is often necessary to automate operations on other kinds of component, and also on dialogs—this section shows the techniques used to perform such testing.

15.1.15.1. Automating native browser dialogs (login, certificates, etc.)

Many web applications require a login using the browser's native authentication dialog, or the acceptance of certificates as part of the startup process. Squish makes it is possible to automate logons and the acceptance of certificates as described below.

15.1.15.1.1. Automating a native login

Squish provides a custom function that you can call from your test scripts to automate a login with the browser's native authentication dialog. The key to using it is to start the login process (typically by clicking a button or link), then wait for the login dialog to appear, and then enter the username and password. Here's an example snippet that shows how it might be done:

Python
clickLink(":Login_A")
waitFor("isBrowserDialogOpen()")
automateLogin(tester_username, tester_password)
JavaScript
clickLink(":Login_A");
waitFor("isBrowserDialogOpen()");
automateLogin(tester_username, tester_password);
Perl
clickLink(":Login_A");
waitFor("isBrowserDialogOpen()");
automateLogin($tester_username, $tester_password);
Tcl
invoke clickLink ":Login_A"
invoke waitFor "invoke isBrowserDialogOpen"
invoke automateLogin $tester_username $tester_password

The snippet assumes that tester_username and tester_password are variables that hold the tester's username and password.

Squish's automateLogin() function automates the native browser authentication dialog for any of Squish's supported browsers, so you don't have to make any allowances for browser differences yourself.

[Note]Note

On Mac OS X you must turn on Universal Access in the System Preferences when you use the automateLogin() function.

15.1.15.1.2. Automating accepting certificates

Automating the acceptance of a certificate depends on which web browser is used. This section explains what needs to be done for each of Squish's supported web browsers to automate the acceptance of a certificate.

15.1.15.1.2.1. Internet Explorer 6 and later

The only step necessary to automate accepting a certificate when running a test in Internet Explorer is to accept it once, permanently. This must be done manually. After this, Squish will tell Internet Explorer to use the accepted certificate on each test run, and no further manual intervention is necessary.

15.1.15.1.2.2. Mozilla Firefox

To accept a certificate in Firefox, you must add some code to your script that will automate the browser dialogs for accepting the certificate. In addition it is necessary to workaround an issue in Firefox would make the test case hang. To do this, first a temporary site must be loaded, and then the real site can be loaded.

Here is an example that shows how to automate connecting to an HTTPS site and accepting the certificate.

Python
# Workaround: Load a temporary page first
loadUrl("http://www.froglogic.com")
# Now load the real page
loadUrl("https://the.real.site.you.want.to.load")
if Browser.type() == Browser.Firefox:
    # Accept the certificate
    waitFor("isBrowserDialogOpen()")
    nativeType("<Return>")
    snooze(1)
    # Accept the second certificate dialog
    nativeType("<Left>")
    nativeType("<Return>")
    waitFor("!isBrowserDialogOpen()")
    rehook()
JavaScript
// Workaround: Load a temporary page first
loadUrl("http://www.froglogic.com");
// Now load the real page
loadUrl("https://the.real.site.you.want.to.load");
if (Browser.type() == Browser.Firefox) {
    // Accept the certificate
    waitFor("isBrowserDialogOpen()");
    nativeType("<Return>");
    snooze(1);
    // Accept the second certificate dialog
    nativeType("<Left>");
    nativeType("<Return>");
    waitFor("!isBrowserDialogOpen()");
    rehook();
}
Perl
# Workaround: Load a temporary page first
loadUrl("http://www.froglogic.com");
# Now load the real page
loadUrl("https://the.real.site.you.want.to.load");
if (Browser.type() == Browser.Firefox) {
    # Accept the certificate
    waitFor("isBrowserDialogOpen()");
    nativeType("<Return>");
    snooze(1);
    # Accept the second certificate dialog
    nativeType("<Left>");
    nativeType("<Return>");
    waitFor("!isBrowserDialogOpen()");
    rehook();
}
Tcl
# Workaround: Load a temporary page first
loadUrl "http://www.froglogic.com"
# Now load the real page
loadUrl "https://the.real.site.you.want.to.load"
if {[[invoke Browser type] == Browser.Firefox]} {
    # Accept the certificate
    waitFor "isBrowserDialogOpen()"
    invoke nativeType "<Return>"
    snooze 1
    # Accept the second certificate dialog
    invoke nativeType "<Left>"
    invoke nativeType "<Return>"
    waitFor "![isBrowserDialogOpen]"
    rehook
}

Loading the temporary page is just an unfortunate—but hardly noticable—necessity. Once the page has loaded, Firefox uses two dialogs to complete the acceptance of a certificate, so we must interact with both of them to complete the acceptance. We use the nativeType() function to simulate the keyboard interaction, where normally we'd use the type() function. Also, after interacting with the dialogs we must call the rehook() function to make Squish do some reloading and reinitialization to account for the fact that the certificate has now been accepted and that as a result, the web page is in a different state.

15.1.15.1.2.3. Safari

The only step necessary to automate accepting a certificate when running a test in Safari is to accept it once, permanently. This must be done manually: First, open the page with the certificate in Safari, then choose to view the details of the certificate in the sheet that pops up, then check the checkbox that tells Safari to always trust the certificate on reconnects. After this, Squish will tell Safari to use the accepted certificate on each test run, and no further manual intervention is necessary.

15.1.15.2. Java Applets

Squish/Web now includes support for testing Java applets embedded in the web browser. This works because Squish/Web includes the necessary functionality from the Squish/Java edition, and means that Web testers can test web pages that include applets that have Java GUIs.

To test a Java applet that is embedded in a web page, simply record the test as normal—Squish/Web will handle all the web page interaction, and under the hood the functionality from the Squish/Java edition will be used when necessary for recording (and later playback) of any interactions with Java applets.

See Tutorial: Starting to Test Java™ AWT/Swing Applications (Chapter 6) for an introduction to Java GUI testing.

It is also possible to test java applets stand-alone as described in the Testing Java Applets (Section 16.4.6) section.

15.1.15.3. Flash, ActiveX, etc.

Squish supports automating interactions and testing non-HTML/DOM elements, that is, native objects, which are embedded in a web page. This is done at a fairly abstract level, which means that mouse and text input can be recorded and replayed. In addition it is possible to inspect embedded native objects with the Spy tool and to insert verifications for these native objects. All of a native object's public properties can be accessed in test scripts.

[Note]Windows and Internet Explorer-specific

ActiveX is a Windows-specific technology, so there is no support for it on other platforms. Squish's Qt edition supports ActiveX, and so does Squish's Web edition —but in the latter case, only in Internet Explorer.

Squish's Web edition supports flash—but only in Internet Explorer.

15.1.16. How to Test Java™ Applications

In this section we will see how the Squish API makes it straightforward to check the values and states of individual widgets so that we can test our application's business rules.

As we saw in the tutorial, we can use Squish's recording facility to create tests. However, it is often useful to modify such tests, or create tests entirely from scratch in code, particularly when we want to test business rules that involves multiple widgets.

In general there is no need to test a widget's standard behavior. For example, if an unchecked two-valued checkbox isn't checked after being clicked, that's a bug in the toolkit not in our code. If such a case arose we may need to write a workaround (and write tests for it), but normally we don't write tests just to check that a widget behaves as documented. On the other hand, what we do want to test is whether our application provides the business rules we intended to build into it. Some tests concern individual widgets in isolation—for example, testing that a combobox contains the appropriate items. Other tests concern inter-widget dependencies and interactions. For example, if we have a group of "payment method" radio buttons, we will want to test that if the "cash" radio button is chosen the check and credit card-relevant widgets are all hidden.

Whether we are testing individual widgets or inter-widget dependencies and interactions, we must first be able to get references to the widgets we want to test. Once we have a reference we can then verify that the widget it refers to has the value and is in the states that we expect.

There are a couple of approaches we can take to get a reference to a Java™ widget. One approach is based on identifying widgets by name. This can be done in two different ways. We can record a dummy test making sure we click the widgets we are interested in so that Squish adds the widgets' names to the object map. Or we can use the How to Use the Spy (Section 15.2.4) tool to pick the widgets we are interested in. In either of these cases we can then use the waitForObject() function to retrieve a reference to the widget of interest. Another approach is to obtain a reference to a container widget as just described, and then use Java™'s introspection facilities to obtain references to the widgets contained in the container. We will show both approaches in this section.

The purpose of this section is to explain and show how to access various Java™ widgets and perform common operations using these widgets—such as getting and setting their properites—with the Perl, Python, JavaScript, and Tcl scripting languages.

After completing this section you should be able to access Java™ widgets, gather data from those Java™ widgets, and perform tests against expected values. The principles covered in this chapter apply to all Java™ widgets, so even if you need to test a widget that isn't specifically mentioned here, you should have no problem doing so.

15.1.16.1. How to Access Widgets

To test and verify a widget and its properties or contents, first we need access to the widget in the test script. To obtain a reference to the widget, the waitForObject() 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. To find out the name of a widget the Spy tool comes in very handy (for more details about using Spy see How to Use the Spy (Section 15.2.4).

The steps to find out the name are the following:

  • Start the Squish IDE and make the test suite we are working in active

  • Start the Spy on the application under test

  • Switch to the AUT and work through the GUI until the widget we want to test is visible (e.g. open the dialog it is contained in)

  • Switch back to the Squish IDE and switch the Spy into Pick mode

  • Switch to the AUT and click on the widget you want to test

  • Switch back to the Squish IDE. In the Spy object view the selected widget and its tree will be displayed. Right-click onto the object name and choose "Copy to clipboard".

  • Exit the Spy

Now the object name we were looking for is saved in the clipboard and we can paste it into the script as the argument to waitForObject().

15.1.16.2. How to Test Java™ AWT/Swing Applications

In the subsections that follow we will focus on testing Java™ AWT/Swing widgets, both single-valued widgets like buttons and spinners, and multi-valued widgets such as lists, tables, and trees. We will also cover testing using external data files.

15.1.16.2.1. How to Test Stateful and Single-Valued Widgets (Java™—AWT/Swing)

In this section we will see how to test the examples/java/paymentform/PaymentForm.java example program. This program uses many basic Java™ AWT/Swing widgets including JButton, JCheckBox, JComboBox, JSpinner, and JTextField. As part of our coverage of the example we will show how to check the values and state of individual widgets. We will also demonstrate how to test a form's business rules.

The PaymentForm example in "pay by credit card" mode.

The PaymentForm is invoked when an invoice is to be paid, either at a point of sale, or—for credit cards—by phone. The form's Pay button must only be enabled if the correct fields are filled in and have valid values. The business rules that we must test for are as follows:

  • In "cash" mode, i.e., when the Cash tab is checked:

    • The minimum payment is one dollar and the maximum is $2000 or the amount due, whichever is smaller.

  • In "check" mode, i.e., when the Check tab is checked:

    • The minimum payment is $10 and the maximum is $250 or the amount due, whichever is smaller.

    • The check date must be no earlier than 30 days ago and no later than tomorrow.

    • The bank name, bank number, account name, and account number line edits must all be nonempty.

    • The check signed checkbox must be checked.

  • In "card" mode, i.e., when the Credit Card tab is checked:

    • The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.

    • For non-Visa cards the issue date must be no earlier than three years ago.

    • The expiry date must be at least one month later than today.

    • The account name and account number line edits must be nonempty.

We will write three tests, one for each of the form's modes.

The source code for the payment form is in the directory SQUISHROOT/examples/java/paymentform, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHROOT/examples/java/paymentform/suite_py, and the JavaScript version of the tests is in SQUISHROOT/examples/java/paymentform/suite_js.

We will begin by reviewing the test script for testing the form's "cash" mode. First we will show the code, then we will explain it.

Example 15.18. The tst_cash_mode Test Script

Python
def main():
    startApplication("PaymentForm.class")
    # Start with the correct tab
    tabWidgetName = ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy" 
    waitForObject(tabWidgetName)
    clickTab(tabWidgetName)

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabelName = ("{caption?='[$][0-9.,]*' type='javax.swing.JLabel' "
            "visible='true' window=':Payment Form_PaymentForm'}")
    amountDueLabel = waitForObject(amountDueLabelName)
    chars = []
    for char in unicode(amountDueLabel.getText()):
        if char.isdigit():
            chars.append(char)
    amount_due = cast("".join(chars), int)
    maximum = min(2000, amount_due)
    paymentSpinnerName = ("{type='javax.swing.JSpinner' visible='true' "
            "window=':Payment Form_PaymentForm'}")
    paymentSpinner = waitForObject(paymentSpinnerName)
    model = paymentSpinner.getModel()
    test.verify(model.minimum.intValue() == 1)
    test.verify(model.maximum.intValue() == maximum)
    
    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ":Payment Form.Pay_javax.swing.JButton"
    payButton = waitForObject(payButtonName)
    test.verify(payButton.enabled)

JavaScript
function main()
{
    startApplication("PaymentForm.class");
    // Start with the correct tab
    var tabWidgetName = ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy";
    waitForObject(tabWidgetName);
    clickTab(tabWidgetName);

    // Business rule #1: the minimum payment is $1 and the maximum is
    // $2000 or the amount due whichever is smaller
    var amountDueLabelName = "{caption?='[$][0-9.,]*' " +
	    "type='javax.swing.JLabel' visible='true' " +
	    "window=':Payment Form_PaymentForm'}";
    var amountDueLabel = waitForObject(amountDueLabelName);
    var chars = [];
    var amountDueText = new String(amountDueLabel.text);
    for (var i = 0; i < amountDueText.length; ++i) {
        var ch = amountDueText.charAt(i);
        if ("0123456789".indexOf(ch) > -1) {
            chars.push(ch);
        }
    }

    var amount_due = parseFloat(chars.join(""));
    var maximum = Math.min(2000, amount_due);

    var paymentSpinnerName = "{type='javax.swing.JSpinner' " +
	    "visible='true' window=':Payment Form_PaymentForm'}";
    var paymentSpinner = waitForObject(paymentSpinnerName);
    var model = paymentSpinner.getModel();
    test.verify(model.minimum.intValue() == 1);
    test.verify(model.maximum.intValue() == maximum);
    
    // Business rule #2: the Pay button is enabled (since the above tests
    // ensure that the payment amount is in range)
    var payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.enabled);
}

Perl
sub main
{
    startApplication("PaymentForm.class");
    # Start with the correct tab
    my $tabWidgetName = ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy";
    waitForObject($tabWidgetName);
    clickTab($tabWidgetName);

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    my $amountDueLabelName = "{caption?='[\$][0-9.,]*' " .
	    "type='javax.swing.JLabel' visible='true' " .
	    "window=':Payment Form_PaymentForm'}";
    my $amountDueLabel = waitForObject($amountDueLabelName);
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    my $maximum = 2000 < $amount_due ? 2000 : $amount_due;

    my $paymentSpinnerName = "{type='javax.swing.JSpinner' " .
	    "visible='true' window=':Payment Form_PaymentForm'}";
    my $paymentSpinner = waitForObject($paymentSpinnerName);
    my $model = $paymentSpinner->getModel();
    test::verify($model->minimum->intValue() == 1);
    test::verify($model->maximum->intValue() == $maximum);
   
    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    my $payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    my $payButton = waitForObject($payButtonName);
    test::verify($payButton->enabled);
}

Tcl
proc main {} {
    startApplication "PaymentForm.class"
    # Start with the correct tab
    set tabWidgetName ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy" 
    waitForObject $tabWidgetName
    invoke clickTab $tabWidgetName

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    set amountDueLabelName {{caption?='[$][0-9.,]*' type='javax.swing.JLabel' visible='true' window=':Payment Form_PaymentForm'}}
    set amountDueLabel [waitForObject $amountDueLabelName]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    set amount_due [expr $amountText]
    set maximum [expr $amount_due < 2000 ? $amount_due : 2000]

    set paymentSpinnerName {{type='javax.swing.JSpinner' visible='true' window=':Payment Form_PaymentForm'}}
    set paymentSpinner [waitForObject $paymentSpinnerName]
    set model [invoke $paymentSpinner getModel]
    set minimumAllowed [invoke [property get $model minimum] intValue]
    set maximumAllowed [invoke [property get $model maximum] intValue]
    test compare $minimumAllowed 1
    test compare $maximumAllowed $maximum
    
    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    set payButtonName ":Payment Form.Pay_javax.swing.JButton"
    waitForObject $payButtonName
    set payButton [findObject $payButtonName]
    test verify [property get $payButton enabled]
}



We must start by making sure that the form is in the mode we want to test. In general, the way we gain access to visible widgets is always the same: we create a variable holding the widget's name, then we call waitForObject() to get a reference to the widget. Once we have the reference we can use it to access the widget's properties and to call the widget's methods. In this case we don't need to callwaitForObject() on the tab's name since we don't need a reference to the tab; instead we just use the clickTab() function to click the tab we are interested in. How did we know the tab's name? We ran a dummy test and clicked each of the tabs—as a result Squish put the tab names in the object map and we copied them from there.

The first business rule to be tested concerns the minimum and maximum allowed payment amounts. As usual we begin by calling waitForObject() to get a reference to it—in this case starting with the amount due label. Because the amount due label's text varies depending on the amount due we cannot have a fixed name for it. So instead we identify it using a multiproperty name using wildcards. The wildcard of [$][0-9.,]* matches any text that starts with a dollar sign and is followed by zero or more digits, periods and commas. Squish can also do regular expression matching—see Improving Object Identification (Section 16.8) for more about matching.

Since the label's text might contain a currency symbol and grouping markers (for example, $1,700 or €1.700), to convert its text into an integer we must strip away any non-digit characters first. We do this in different ways depending on the underlying scripting language. (For example, in Python, we iterate over each character and join all those that are digits into a single string and use the cast() function which takes an object and the type the object should be converted to, and returns an object of the requested type—or 0 on failure. We use a similar approach in JavaScript, but for Perl and Tcl we simply replace non-digit characters using a regular expression.) The resulting integer is the amount due, so we can now trivially calculate the maximum amount that can be paid in cash.

With the minimum and maximum amounts known we next get a reference to the payment spinner. (In this case we didn't get the name from the object map, but guessed it. We could have used introspection, a technique we will use shortly.) Once we have a reference to the spinner, we retrieve its number model. Then we use the test.verify() method to ensure that the model has the correct minimum and maximum amounts set. (For Tcl we have used the test.compare() method instead of test.verify() since it is more convenient to do so.)

Checking the last business rule is easy in this case since if the amount is in range (and it must be because we have just checked it), then payment is allowed so the Pay button should be enabled. Once again, we use the same approach to test this: first we call waitForObject() to get a reference to it, and then we conduct the test—in this case checking that the Pay button is enabled.

Although the "cash" mode test works well, there are a few places where we use essentially the same code. So before creating the test for the "check" and "card" modes, we will create some common functions that we can use to refactor our tests with. (The process used to create shared code is described a little later in How to Create and Use Shared Data and Shared Scripts (Section 15.4)—essentially all we need to do is create a new script under the Test Suite's shared item's scripts item.) The Python common code is in common.py, the JavaScript common code is in common.js, and so on.

Example 15.19. The Shared Code

Python
def clickTabbedPane(text):
    waitForObject(":Payment Form.%s_com.froglogic.squish.awt.TabProxy" % text)
    clickTab(":Payment Form.%s_com.froglogic.squish.awt.TabProxy" % text)

    
def getAmountDue():
    amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' "
            "type='javax.swing.JLabel' visible='true' "
            "window=':Payment Form_PaymentForm'}")
    chars = []
    for char in unicode(amountDueLabel.getText()):
        if char.isdigit():
            chars.append(char)
    return cast("".join(chars), int)


def checkPaymentRange(minimum, maximum):
    paymentSpinner = waitForObject("{type='javax.swing.JSpinner' visible='true' "
            "window=':Payment Form_PaymentForm'}")
    model = paymentSpinner.getModel()
    test.verify(model.minimum.intValue() == minimum)
    test.verify(model.maximum.intValue() == maximum)

JavaScript
function clickTabbedPane(text)
{
    var tabbedPaneName = ":Payment Form." + text + "_com.froglogic.squish.awt.TabProxy";
    waitForObject(tabbedPaneName);
    clickTab(tabbedPaneName);
}

    
function getAmountDue()
{
    var amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' " +
            "type='javax.swing.JLabel' visible='true' " +
            "window=':Payment Form_PaymentForm'}")
    var chars = [];
    var amountDueText = new String(amountDueLabel.text);
    for (var i = 0; i < amountDueText.length; ++i) {
        var ch = amountDueText.charAt(i);
        if ("0123456789".indexOf(ch) > -1) {
            chars.push(ch);
        }
    }
    return parseFloat(chars.join(""));
}


function checkPaymentRange(minimum, maximum)
{
    var paymentSpinner = waitForObject("{type='javax.swing.JSpinner' " +
	    "visible='true' window=':Payment Form_PaymentForm'}");
    var model = paymentSpinner.getModel();
    test.verify(model.minimum.intValue() == minimum);
    test.verify(model.maximum.intValue() == maximum);
}

Perl
sub clickTabbedPane
{
    my $text = shift(@_);
    my $name = ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy";
    waitForObject($name);
    clickTab($name);
}

    
sub getAmountDue
{
    my $amountDueLabel = waitForObject("{caption?='[\$][0-9.,]*' " .
            "type='javax.swing.JLabel' visible='true' " .
            "window=':Payment Form_PaymentForm'}");
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    return $amount_due;
}


sub checkPaymentRange
{
    my ($minimum, $maximum) = @_;
    my $paymentSpinner = waitForObject("{type='javax.swing.JSpinner' visible='true' " .
            "window=':Payment Form_PaymentForm'}");
    my $model = $paymentSpinner->getModel();
    test::verify($model->minimum->intValue() == $minimum);
    test::verify($model->maximum->intValue() == $maximum);
}

Tcl
proc clickTabbedPane {text} {
    waitForObject ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy"
    invoke clickTab ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy"
}


proc getAmountDue {} {
    set amountDueLabel [waitForObject {{caption?='[$][0-9.,]*' type='javax.swing.JLabel' visible='true' window=':Payment Form_PaymentForm'}}]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    return [expr $amountText]
}


proc checkPaymentRange {minimum maximum} {
    set paymentSpinner [waitForObject {{type='javax.swing.JSpinner' visible='true' window=':Payment Form_PaymentForm'}}]
    set model [invoke $paymentSpinner getModel]
    set minimumAllowed [invoke [property get $model minimum] intValue]
    set maximumAllowed [invoke [property get $model maximum] intValue]
    test compare $minimumAllowed $minimum
    test compare $maximumAllowed $maximum
}



Now we can write our tests for "check" and "card" modes and put more of our effort into testing the business rules and less into some of the basic chores. We've broken the code for "check" mode into a main() function—this is special to Squish and the only function Squish will call—and some test-specific supporting functions, which combined with the shared functions shown above, make the code more managable. Although the main() function comes at the end of the test.py (or test.js and so on) file, we will show it first, and then show the test-specific supporting functions afterwards.

Example 15.20. The tst_check_mode Test Script's main() function

Python
def main():
    startApplication("PaymentForm.class")
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabbedPane("Check")

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(10, min(250, amount_due))
    
    # Business rule #2: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow
    checkDateRange(-30, 1)
    
    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButtonName = ":Payment Form.Pay_javax.swing.JButton"
    payButton = findObject(payButtonName)
    test.compare(payButton.enabled, False)
    
    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked()
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.enabled)

JavaScript
function main()
{
    startApplication("PaymentForm.class");
    // Import functionality needed by more than one test script
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabbedPane("Check");

    // Business rule #1: the minimum payment is $10 and the maximum is
    // $250 or the amount due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(10, Math.min(250, amount_due));
    
    // Business rule #2: the check date must be no earlier than 30 days 
    // ago and no later than tomorrow
    checkDateRange(-30, 1);
    
    // Business rule #3: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use findObject() without waiting
    var payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    var payButton = findObject(payButtonName);
    test.compare(payButton.enabled, false);
    
    // Business rule #4: the check must be signed (and if it isn't we
    // will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();
    
    // Business rule #5: the Pay button should be enabled since all the 
    // previous tests pass, the check is signed and now we have filled in
    // the account details
    populateCheckFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.enabled);
}

Perl
sub main
{
    startApplication("PaymentForm.class");
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabbedPane("Check");

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    my $amount_due = getAmountDue();
    checkPaymentRange(10, $amount_due < 250 ? $amount_due : 250);
    
    # Business rule #2: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow
    
    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    my $payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    my $payButton = findObject($payButtonName);
    test::compare($payButton->enabled, 0);
    
    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked;
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields;
    my $payButton = findObject($payButtonName);
    test::verify($payButton->enabled);
}

Tcl
proc main {} {
    startApplication "PaymentForm.class"
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabbedPane "Check"

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    set amount_due [getAmountDue]
    set maximum [expr 250 > $amount_due ? $amount_due : 250]
    checkPaymentRange 10 $maximum
    
    # Business rule #2: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow
    checkDateRange -30 1
    
    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    set payButtonName ":Payment Form.Pay_javax.swing.JButton"
    set payButton [findObject $payButtonName]
    test compare [property get $payButton enabled] false
    
    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields
    set payButton [waitForObject $payButtonName]
    test compare [property get $payButton enabled] true
}



The source() function is used to read in a script and execute it. Normally such a script is used purely to define things—for example, functions—and these then become available to the test script.

The first business rule is very similar to before, but the code is much shorter thanks to the shared checkPaymentRange() function. The test for the second rule is even simpler since we have put all the code in a separate checkDateRange() function that we will look at in a moment.

The third rule checks that the Pay button is disabled since at this stage the form's data isn't valid. (For example, the check hasn't been signed and there are no account details filled in.) The fourth rule is used to confirm that the check is signed—something that we explicitly make happen if it is necessary in the test-specific ensureSignedCheckBoxIsChecked() function.

For the fifth rule we populate the account line edits with fake data. At the end all the widgets have valid values so the Pay button should be enabled, and the last line tests that it is.

Example 15.21. The tst_check_mode Test Script's other functions

Python
def checkDateRange(daysBeforeToday, daysAfterToday):
    calendar = java_util_Calendar.getInstance()
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysBeforeToday)
    earliest = calendar.getTime()
    calendar.setTime(java_util_Date())
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysAfterToday)
    latest = calendar.getTime()
    checkDateSpinner = waitForObject(
            "{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' "
            "type='javax.swing.JSpinner' visible='true'}")
    model = checkDateSpinner.getModel()
    formatter = java_text_SimpleDateFormat("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) == formatter.format(earliest))
    test.verify(formatter.format(model.getEnd()) == formatter.format(latest))

def ensureSignedCheckBoxIsChecked():
    checkSignedCheckBox = waitForObject(
            ":Check.Check Signed_javax.swing.JCheckBox")
    if not checkSignedCheckBox.isSelected():
        clickButton(checkSignedCheckBox)
    test.verify(checkSignedCheckBox.isSelected())

def populateCheckFields():
    bankNameLineEdit = waitForObject(":Check.Bank Name:_javax.swing.JTextField")
    type(bankNameLineEdit, "A Bank")
    bankNumberLineEdit = waitForObject(":Check.Bank Number:_javax.swing.JTextField")
    type(bankNumberLineEdit, "88-91-33X")
    accountNameLineEdit = waitForObject(":Check.Account Name:_javax.swing.JTextField")
    type(accountNameLineEdit, "An Account")
    accountNumberLineEdit = waitForObject(":Check.Account Number:_javax.swing.JTextField")
    type(accountNumberLineEdit, "932745395")

JavaScript
function checkDateRange(daysBeforeToday, daysAfterToday)
{
    var checkDateSpinner = waitForObject("{container=':Payment Form." +
	    "Check_com.froglogic.squish.awt.TabProxy' " +
            "type='javax.swing.JSpinner' visible='true'}");
    var model = checkDateSpinner.getModel();
    var calendar = java_util_Calendar.getInstance();
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysBeforeToday);
    var earliest = calendar.getTime();
    calendar.setTime(new java_util_Date());
    calendar.add(java_util_Calendar.DAY_OF_MONTH, daysAfterToday);
    var latest = calendar.getTime();
    var formatter = new java_text_SimpleDateFormat("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
		formatter.format(earliest));
    test.verify(formatter.format(model.getEnd()) ==
		formatter.format(latest));
}

function ensureSignedCheckBoxIsChecked()
{
    var checkSignedCheckBox = waitForObject(
            ":Check.Check Signed_javax.swing.JCheckBox");
    if (!checkSignedCheckBox.isSelected()) {
        clickButton(checkSignedCheckBox);
	}
    test.verify(checkSignedCheckBox.isSelected());
}

function populateCheckFields()
{
    var bankNameLineEdit = waitForObject(":Check.Bank Name:_javax.swing.JTextField");
    type(bankNameLineEdit, "A Bank");
    var bankNumberLineEdit = waitForObject(":Check.Bank Number:_javax.swing.JTextField");
    type(bankNumberLineEdit, "88-91-33X");
    var accountNameLineEdit = waitForObject(":Check.Account Name:_javax.swing.JTextField");
    type(accountNameLineEdit, "An Account");
    var accountNumberLineEdit = waitForObject(":Check.Account Number:_javax.swing.JTextField");
    type(accountNumberLineEdit, "932745395");
}

Perl
sub checkDateRange
{
    my ($daysBeforeToday, $daysAfterToday) = @_;
    my $calendar = java_util_Calendar::getInstance();
    $calendar->add(java_util_Calendar::DAY_OF_MONTH, $daysBeforeToday);
    my $earliest = $calendar->getTime();
    $calendar->setTime(java_util_Date->new());
    $calendar->add(java_util_Calendar::DAY_OF_MONTH, $daysAfterToday);
    my $latest = $calendar->getTime();
    my $checkDateSpinner = waitForObject(
            "{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' " .
            "type='javax.swing.JSpinner' visible='true'}");
    my $model = $checkDateSpinner->getModel();
    my $formatter = java_text_SimpleDateFormat->new("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq $formatter->format($earliest));
    test::verify($formatter->format($model->getEnd()) eq $formatter->format($latest));
}

sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedCheckBox = waitForObject(
            ":Check.Check Signed_javax.swing.JCheckBox");
    if (!$checkSignedCheckBox->isSelected()) {
        clickButton($checkSignedCheckBox);
    }
    test::verify($checkSignedCheckBox->isSelected());
}


sub populateCheckFields
{
    my $bankNameLineEdit = waitForObject(":Check.Bank Name:_javax.swing.JTextField");
    type($bankNameLineEdit, "A Bank");
    my $bankNumberLineEdit = waitForObject(":Check.Bank Number:_javax.swing.JTextField");
    type($bankNumberLineEdit, "88-91-33X");
    my $accountNameLineEdit = waitForObject(":Check.Account Name:_javax.swing.JTextField");
    type($accountNameLineEdit, "An Account");
    my $accountNumberLineEdit = waitForObject(":Check.Account Number:_javax.swing.JTextField");
    type($accountNumberLineEdit, "932745395");
}

Tcl
proc checkDateRange {daysBeforeToday daysAfterToday} {
    set checkDateSpinner [waitForObject {{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' type='javax.swing.JSpinner' visible='true'}}]
    set formatter [construct java_text_SimpleDateFormat "yyyy-MM-dd"]
    set model [invoke $checkDateSpinner getModel]
    set minimumAllowed [invoke $formatter format [invoke $model getStart]]
    set maximumAllowed [invoke $formatter format [invoke $model getEnd]]
    set calendar [invoke java_util_Calendar getInstance]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] $daysBeforeToday
    set minimumDate [invoke $formatter format [invoke $calendar getTime]]
    invoke $calendar setTime [construct java_util_Date]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] $daysAfterToday
    set maximumDate [invoke $formatter format [invoke $calendar getTime]]
    test compare $minimumAllowed $minimumDate
    test compare $maximumAllowed $maximumDate
}

proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedCheckBox [waitForObject ":Check.Check Signed_javax.swing.JCheckBox"]
    if {![invoke $checkSignedCheckBox isSelected]} {
        invoke clickButton $checkSignedCheckBox
    }
    test compare [invoke $checkSignedCheckBox isSelected] true
}

proc populateCheckFields {} {
    set bankNameLineEdit [waitForObject ":Check.Bank Name:_javax.swing.JTextField"]
    invoke type $bankNameLineEdit "A Bank"
    set bankNumberLineEdit [waitForObject ":Check.Bank Number:_javax.swing.JTextField"]
    invoke type $bankNumberLineEdit "88-91-33X"
    set accountNameLineEdit [waitForObject ":Check.Account Name:_javax.swing.JTextField"]
    invoke type $accountNameLineEdit "An Account"
    set accountNumberLineEdit [waitForObject ":Check.Account Number:_javax.swing.JTextField"]
    invoke type $accountNumberLineEdit "932745395"
}



In the checkDateRange() function we test the properties of a JSpinner's SpinnerDateModel. Notice that we compare the dates as strings using a uniform format.

While Squish provides access to most of the Java™ API automatically, in some cases we need to access classes that are not available by default. In this function we need a couple of classes that are not available, java.util.Calendar and java.text.SimpleDateFormat. This isn't a problem in practice since we can always register additional classes (whether standard or our own custom classes) with the squishserver—see Wrapping custom Java™ classes (Section 16.4.8) for details. In this case we added several extra classes in java.ini which has just two lines:

[general]
AutClasses="java.util.Calendar","java.util.Date","java.text.DateFormat","java.text.SimpleDateFormat"

The ensureSignedCheckBoxIsChecked() function checks the state of a JCheckBox and if it is not checked, checks it by clicking it. The function then verifies that the checkbox is indeed checked.

The populateCheckFields() function fills in some JTextFields—this, along with setting the date and checking the checkbox earlier—should ensure that the Pay is enabled, something that is checked in the main() function after the populateCheckFields() function is called.

Notice that we used the type() function to simulate the user entering text. It is almost always better to simulate user interaction than to set widget properties directly—after all, it is the application's behavior as experienced by the user that we normally need to test.

We are now ready to look at the last test of the form's business logic—the test of "card" mode. The code is a bit longer because there are a few more things to test given the test specification, but everything works on the same principles as the tests we have already seen.

Example 15.22. The tst_card_mode Test Script's main() function

Python
def main():
    startApplication("PaymentForm.class")
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabbedPane("Credit Card")
    
    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due 
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))
    
    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits()

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButtonName = ":Payment Form.Pay_javax.swing.JButton"
    payButton = findObject(payButtonName)
    test.compare(payButton.enabled, False)
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, and now we have filled in the account details
    populateCardFields()
    payButton = findObject(payButtonName)
    test.verify(payButton.enabled)

JavaScript
function main()
{
    startApplication("PaymentForm.class");
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabbedPane("Credit Card");
    
    // Business rule #1: the minimum payment is $10 or 5% of the amount due
    // whichever is larger and the maximum is $5000 or the amount due 
    // whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(Math.max(10, amount_due / 20.0),
                      Math.min(5000, amount_due));

    // Business rule #2: for non-Visa cards the issue date must be no
    // earlier than 3 years ago
    // Business rule #3: the expiry date must be at least a month later
    // than today---we make sure that this is the case for the later tests
    checkCardDateEdits();

    // Business rule #4: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use findObject() without waiting
    var payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    var payButton = findObject(payButtonName);
    test.compare(payButton.enabled, false);
    
    // Business rule #5: the Pay button should be enabled since all the 
    // previous tests pass, and now we have filled in the account details
    populateCardFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.enabled);
}

Perl
sub main
{
    startApplication("PaymentForm.class");
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabbedPane("Credit Card");
    
    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due 
    # whichever is smaller
    my $amount_due = getAmountDue();
    my $minimum = $amount_due / 20.0 > 10 ? $amount_due / 20.0 : 10;
    my $maximum = $amount_due < 5000 ? $amount_due : 5000;
    checkPaymentRange($minimum, $maximum);

    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits;

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    my $payButtonName = ":Payment Form.Pay_javax.swing.JButton";
    my $payButton = findObject($payButtonName);
    test::compare($payButton->enabled, 0);
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, and now we have filled in the account details
    populateCardFields;
    my $payButton = findObject($payButtonName);
    test::verify($payButton->enabled);
}

Tcl
proc main {} {
    startApplication "PaymentForm.class"
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabbedPane "Credit Card"
    
    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due 
    # whichever is smaller
    set amount_due [getAmountDue]
    set five_percent [expr $amount_due / 20]
    set minimum [expr 10 > $five_percent ? 10 : $five_percent]
    set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
    checkPaymentRange $minimum $maximum

    # Business rule #2: for non-Visa cards the issue date must be no
    # earlier than 3 years ago
    # Business rule #3: the expiry date must be at least a month later
    # than today---we make sure that this is the case for the later tests
    checkCardDateEdits
    
    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    set payButtonName ":Payment Form.Pay_javax.swing.JButton"
    set payButton [findObject $payButtonName]
    test compare [property get $payButton enabled] false
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    set payButton [waitForObject $payButtonName]
    test compare [property get $payButton enabled] true
}



Just as we did for the "check" mode's main() function, we have encapsulated almost every test in a separate function. This makes the main() function simpler and clearer and makes it easier to develop and test tests individually.

Example 15.23. The tst_card_mode Test Script's other functions

Python
def checkCardDateEdits():
    # (1) set the card type to any non-Visa card
    cardTypeComboBox = waitForObject(":Credit Card.Card Type:_javax.swing.JComboBox")
    for index in range(cardTypeComboBox.getItemCount()):
        if cardTypeComboBox.getItemAt(index).toString() != "Visa":
            cardTypeComboBox.setSelectedIndex(index)
            break
    # (2) find the two date spinners
    creditCardTabPane = waitForObject(
            ":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy").component
    spinners = []
    for i in range(creditCardTabPane.getComponentCount()):
        component = creditCardTabPane.getComponent(i)
        if component.getClass().toString() == "class javax.swing.JSpinner":
            spinners.append(component)
    test.verify(len(spinners) == 2)
    # (3) check the issue date spinner's minimum date
    calendar = java_util_Calendar.getInstance()
    calendar.add(java_util_Calendar.YEAR, -3)
    date = calendar.getTime()
    issueDateSpinner = spinners[0]
    model = issueDateSpinner.getModel()
    formatter = java_text_SimpleDateFormat("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) == formatter.format(date))
    # (4) set the expiry date more than a month later than now for later tests
    calendar.setTime(java_util_Date())
    calendar.add(java_util_Calendar.DAY_OF_MONTH, 35)
    expiryDateSpinner = spinners[1]
    expiryDateSpinner.setValue(calendar.getTime())

def populateCardFields():
    cardAccountNameLineEdit = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")
    type(cardAccountNameLineEdit, "An Account")
    cardAccountNumberLineEdit = waitForObject(":Credit Card.Account Number:_javax.swing.JTextField")
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32")

JavaScript
function checkCardDateEdits()
{
    // (1) set the card type to any non-Visa card
    var cardTypeComboBox = waitForObject(":Credit Card.Card Type:_javax.swing.JComboBox");
    for (var index = 0; index < cardTypeComboBox.getItemCount(); ++index) {
        if (cardTypeComboBox.getItemAt(index).toString() != "Visa") {
            cardTypeComboBox.setSelectedIndex(index);
            break;
	}
    }
    // (2) find the two date spinners
    var creditCardTabPane = waitForObject(":Payment Form.Credit Card_com." +
	    "froglogic.squish.awt.TabProxy").component;
    var spinners = [];
    for (var i = 0; i < creditCardTabPane.getComponentCount(); ++i) {
        var component = creditCardTabPane.getComponent(i);
        if (component.getClass().toString() ==
	    "class javax.swing.JSpinner") {
            spinners.push(component);
	}
    }
    test.verify(spinners.length == 2);
    // (3) check the issue date spinner's minimum date
    var calendar = java_util_Calendar.getInstance();
    calendar.add(java_util_Calendar.YEAR, -3);
    var threeYearsAgo = calendar.getTime();
    var issueDateSpinner = spinners[0];
    var model = issueDateSpinner.getModel();
    var formatter = new java_text_SimpleDateFormat("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
		formatter.format(threeYearsAgo));
    // (4) set the expiry date more than a month later than now for later tests
    calendar.setTime(new java_util_Date());
    calendar.add(java_util_Calendar.DAY_OF_MONTH, 35);
    var expiryDateSpinner = spinners[1];
    expiryDateSpinner.setValue(calendar.getTime());
}

function populateCardFields()
{
    var cardAccountNameLineEdit = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField");
    type(cardAccountNameLineEdit, "An Account");
    var cardAccountNumberLineEdit = waitForObject(":Credit Card.Account Number:_javax.swing.JTextField");
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32");
}

Perl
sub checkCardDateEdits
{
    # (1) set the card type to any non-Visa card
    my $cardTypeComboBox = waitForObject(":Credit Card.Card Type:_javax.swing.JComboBox");
    for (my $index = 0; $index < $cardTypeComboBox->getItemCount(); ++$index) {
        if ($cardTypeComboBox->getItemAt($index)->toString() != "Visa") {
            $cardTypeComboBox->setSelectedIndex($index);
            break;
	}
    }
    # (2) find the two date spinners
    my $creditCardTabPane = waitForObject(
            ":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy")->component;
    my @spinners = ();
    for (my $i = 0; $i < $creditCardTabPane->getComponentCount(); ++$i) {
        my $component = $creditCardTabPane->getComponent($i);
        if ($component->getClass()->toString() eq "class javax.swing.JSpinner") {
            push @spinners, $component;
	}
    }
    test::verify(@spinners == 2);
    # (3) check the issue date spinner's minimum date
    my $calendar = java_util_Calendar::getInstance();
    $calendar->add(java_util_Calendar::YEAR, -3);
    my $date = $calendar->getTime();
    my $issueDateSpinner = $spinners[0];
    my $model = $issueDateSpinner->getModel();
    my $formatter = java_text_SimpleDateFormat->new("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq $formatter->format($date));
    # (4) set the expiry date more than a month later than now for later tests
    $calendar->setTime(java_util_Date->new());
    $calendar->add(java_util_Calendar::DAY_OF_MONTH, 35);
    my $expiryDateSpinner = $spinners[1];
    $expiryDateSpinner->setValue($calendar->getTime());
}


sub populateCardFields
{
    my $cardAccountNameLineEdit = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField");
    type($cardAccountNameLineEdit, "An Account");
    my $cardAccountNumberLineEdit = waitForObject(":Credit Card.Account Number:_javax.swing.JTextField");
    type($cardAccountNumberLineEdit, "1343 876 326 1323 32");
}

Tcl
proc checkCardDateEdits {} {
    # (1) set the card type to any non-Visa card
    set cardTypeComboBox [waitForObject ":Credit Card.Card Type:_javax.swing.JComboBox"]
    set count [invoke $cardTypeComboBox getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        if {[invoke $cardTypeComboBox getItemAt $index] != "Visa"} {
            invoke $cardTypeComboBox setSelectedIndex $index
            break
	}
    }
    # (2) find the two date spinners
    set creditCardTabPaneProxy [waitForObject ":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy"]
    set creditCardTabPane [property get $creditCardTabPaneProxy component]
    set spinners {}
    set count [invoke $creditCardTabPane getComponentCount]
    for {set index 0} {$index < $count} {incr index} {
        set component [invoke $creditCardTabPane getComponent $index]
	set classname [invoke [invoke $component getClass] toString]
	if {$classname == "class javax.swing.JSpinner"} {
            lappend spinners $component
	}
    }
    test compare [llength $spinners] 2
    # (3) check the issue date spinner's minimum date
    set calendar [invoke java_util_Calendar getInstance]
    invoke $calendar add [property get java_util_Calendar YEAR] -3
    set issueDateSpinner [lindex $spinners 0]
    set model [invoke $issueDateSpinner getModel]
    set formatter [construct java_text_SimpleDateFormat "yyyy-MM-dd"]
    set minimumAllowed [invoke $formatter format [invoke $model getStart]]
    set minimumDate [invoke $formatter format [invoke $calendar getTime]]
    test compare $minimumAllowed $minimumDate
    # (4) set the expiry date more than a month later than now for later tests
    invoke $calendar setTime [construct java_util_Date]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] 35
    set expiryDateSpinner [lindex $spinners 1]
    invoke $expiryDateSpinner setValue [invoke $calendar getTime]
}

proc populateCardFields {} {
    set cardAccountNameLineEdit [waitForObject ":Credit Card.Account Name:_javax.swing.JTextField"]
    invoke type $cardAccountNameLineEdit "An Account"
    set cardAccountNumberLineEdit [waitForObject ":Credit Card.Account Number:_javax.swing.JTextField"]
    invoke type $cardAccountNumberLineEdit "1343 876 326 1323 32"
}



The second and third business rules are handled by the test-specific checkCardDateEdits() function. For the second business rule we need the card type combobox to be on any card type except Visa, so we iterate over the combobox's items and set the current item to be the first non-Visa item we find. Now we must check that the card's issue date is not allowed to be too long ago.

This form has two JSpinners, one used for the card's issue date and the other for the card's expiry date. We can't use names to distingish between the spinners so we must obtain references to them by using introspection. To do this we begin by finding the innermost component that contains the spinners—in this case the JPane that is shown by the JTabbedPane's current tab. (Squish uses "proxy"s for some widgets, in this case a TabProxy; but we can always access the relevant component using the component property as we do here.) Once we have the JPane, we iterate over its components, making a list of those that are JSpinners. We then check that there are exactly two spinners as expected and then we are ready to check that the minimum issue date has been correctly set to three years ago.

The third business rule says that the expiry date must be at least a month ahead. We explictly set the expiry to be 35 days ahead so that the Pay button will be enabled later on.

Initially the Pay button should be disabled, and we check for this in the main() function. For the fifth business rule, we need some fake data for the card account name and number, and this is provided by the populateCardFields() function. After this has been called, and since the dates are now in range, the Pay button should now be enabled, and again we check this in the main() function.

We have now completed our review of testing business rules using stateful and single-valued widgets. Java™ has many other similar widgets but all of them are identified and tested using the same techniques we have used here.

15.1.16.2.2. How to Test JList, JTable, and JTree widgets (Java—AWT/Swing)

In this section we will see how to iterate over every item in Java™'s JList, JTable, and JTree widgets, and how to retrieve information from each item, such as their text and selected status. The actual data is held in models, so in each example we begin by retrieving a reference to the widget's underlying model, and then operate on the model itself.

Although the examples only output each item's text and selected status to Squish's log, they are very easy to adapt to do more sophisticated testing, such as comparing actual values against expected values.

All the code shown in this section is taken from the examples/java/itemviews example's test suites.

15.1.16.2.2.1. How to Test JList

It is very easy to iterate over all the items in a JList and retrieve their texts and selected status, as the following test example shows:

Example 15.24. The tst_jlist Test Script

Python
def main():
    startApplication("ItemViews.class")
    listWidgetName = ":Item Views_javax.swing.JList"
    listWidget = waitForObject(listWidgetName)
    model = listWidget.getModel()
    for row in range(model.getSize()):
        item = model.getElementAt(row)
        selected = ""
        if listWidget.isSelectedIndex(row):
            selected = " +selected"
        test.log("(%d) '%s'%s" % (row, item.toString(), selected))

JavaScript
function main()
{
    startApplication("ItemViews.class");
    var listWidgetName = ":Item Views_javax.swing.JList";
    var listWidget = waitForObject(listWidgetName);
    var model = listWidget.getModel();
    for (var row = 0; row < model.getSize(); ++row) {
        var item = model.getElementAt(row);
        var selected = "";
        if (listWidget.isSelectedIndex(row)) {
            selected = " +selected";
        }
        test.log("(" + String(row) + ") '" + item.toString() + "'" + selected);
    }
}

Perl
sub main
{
    startApplication("ItemViews.class");
    my $listWidgetName = ":Item Views_javax.swing.JList";
    my $listWidget = waitForObject($listWidgetName);
    my $model = $listWidget->getModel();
    for (my $row = 0; $row < $model->getSize(); ++$row) {
        my $item = $model->getElementAt($row);
        my $selected = "";
        if ($listWidget->isSelectedIndex($row)) {
            $selected = " +selected";
        }
        test::log("($row) '" . $item->toString() . "'$selected");
    }
}

Tcl
proc main {} {
    startApplication "ItemViews.class"
    set listWidgetName ":Item Views_javax.swing.JList"
    set listWidget [waitForObject $listWidgetName]
    set model [invoke $listWidget getModel]
    for {set row 0} {$row < [invoke $model getSize]} {incr row} {
        set item [invoke $model getElementAt $row]
        set selected ""
        if {[invoke $listWidget isSelectedIndex $row]} {
            set selected " +selected"
        }
        set text [invoke $item toString]
        test log "($row) '$text'$selected"
    }
}



All the output goes to Squish's log, but clearly it is easy to change the script to test against a list of specific values and so on.

15.1.16.2.2.2. How to Test JTable

It is also very easy to iterate over all the items in a JTable and retrieve their texts and selected status, as the following test example shows:

Example 15.25. The tst_jtable Test Script

Python
def main():
    startApplication("ItemViews.class")
    tableWidgetName = ":Item Views_javax.swing.JTable"
    tableWidget = waitForObject(tableWidgetName)
    model = tableWidget.getModel()
    for row in range(model.getRowCount()):
        for column in range(model.getColumnCount()):
            item = model.getValueAt(row, column)
            selected = ""
            if tableWidget.isCellSelected(row, column):
                selected = " +selected"
            test.log("(%d, %d) '%s'%s" % (row, column, item.toString(), selected))

JavaScript
function main()
{
    startApplication("ItemViews.class");
    var tableWidgetName = ":Item Views_javax.swing.JTable";
    var tableWidget = waitForObject(tableWidgetName);
    var model = tableWidget.getModel();
    for (var row = 0; row < model.getRowCount(); ++row) {
        for (var column = 0; column < model.getColumnCount(); ++column) {
            var item = model.getValueAt(row, column);
            var selected = "";
            if (tableWidget.isCellSelected(row, column)) {
                selected = " +selected";
            }
            test.log("(" + String(row) + ", " + String(column) + ") '" +
                     item.toString() + "'" + selected);
        }
    }
}

Perl
sub main
{
    startApplication("ItemViews.class");
    my $tableWidgetName = ":Item Views_javax.swing.JTable";
    my $tableWidget = waitForObject($tableWidgetName);
    my $model = $tableWidget->getModel();
    for (my $row = 0; $row < $model->getRowCount(); ++$row) {
        for (my $column = 0; $column < $model->getColumnCount(); ++$column) {
            my $item = $model->getValueAt($row, $column);
            my $selected = "";
            if ($tableWidget->isCellSelected($row, $column)) {
                $selected = " +selected";
            }
            test::log("($row, $column) '$item'$selected");
        }
    }
}

Tcl
proc main {} {
    startApplication "ItemViews.class"
    set tableWidgetName ":Item Views_javax.swing.JTable"
    set tableWidget [waitForObject $tableWidgetName]
    set model [invoke $tableWidget getModel]
    for {set row 0} {$row < [invoke $model getRowCount]} {incr row} {
        for {set column 0} {$column < [invoke $model getColumnCount]} {incr column} {
            set item [invoke $model getValueAt $row $column]
            set selected ""
            if {[invoke $tableWidget isCellSelected $row $column]} {
                set selected " +selected"
            }
            set text [invoke $item toString]
            test log "($row, $column) '$text'$selected"
        }
    }
}



Again, all the output goes to Squish's log, and clearly it is easy to change the script to test against a specific values and so on.

15.1.16.2.2.3. How to Test JTree

It is slightly more tricky to iterate over all the items in a JTree and retrieve their texts and selected status—since a tree is a recursive structure. Nonetheless, it is perfectly possible, as the following test example shows:

Example 15.26. The tst_jtree Test Script

Python
def checkAnItem(indent, model, item, selectionModel, treePath):
    if indent > -1:
        selected = ""
        if selectionModel.isPathSelected(treePath):
            selected = " +selected"
        test.log("|%s'%s'%s" % (" " * indent, item.toString(), selected))
    else:
        indent = -4
    for row in range(model.getChildCount(item)):
        child = model.getChild(item, row)
        childTreePath = treePath.pathByAddingChild(child)
        checkAnItem(indent + 4, model, child, selectionModel, childTreePath)
       
def main():
    startApplication("ItemViews.class")
    treeWidgetName = ":Item Views_javax.swing.JTree"
    treeWidget = waitForObject(treeWidgetName)
    model = treeWidget.getModel()
    selectionModel = treeWidget.getSelectionModel()
    treePath = javax_swing_tree_TreePath(model.getRoot())
    checkAnItem(-1, model, model.getRoot(), selectionModel, treePath)

JavaScript
function checkAnItem(indent, model, item, selectionModel, treePath)
{
    if (indent > -1) {
        var selected = "";
        if (selectionModel.isPathSelected(treePath)) {
            selected = " +selected";
        }
        var offset = "";
        for (var i = 0; i < indent; ++i) {
            offset = offset.concat(" ");
        }
        test.log("|" + offset + "'" + item.toString() + "'" + selected);
    }
    else {
        indent = -4;
    }
    for (var row = 0; row < model.getChildCount(item); ++row) {
        var child = model.getChild(item, row);
        var childTreePath = treePath.pathByAddingChild(child);
        checkAnItem(indent + 4, model, child, selectionModel, childTreePath)
    }
}
       
function main()
{
    startApplication("ItemViews.class");
    var treeWidgetName = ":Item Views_javax.swing.JTree";
    var treeWidget = waitForObject(treeWidgetName);
    var model = treeWidget.getModel();
    var selectionModel = treeWidget.getSelectionModel();
    var treePath = new javax_swing_tree_TreePath(model.getRoot());
    checkAnItem(-1, model, model.getRoot(), selectionModel, treePath);
}

Perl
sub checkAnItem
{
    my ($indent, $model, $item, $selectionModel, $treePath) = @_;
    if ($indent > -1) {
        my $selected = "";
        if ($selectionModel->isPathSelected($treePath)) {
            $selected = " +selected";
        }
        my $padding = " " x $indent;
        test::log("|" . $padding . "'" . $item->toString() . "'$selected");
    }
    else {
        $indent = -4;
    }
    for (my $row = 0; $row < $model->getChildCount($item); ++$row) {
        my $child = $model->getChild($item, $row);
        my $childTreePath = $treePath->pathByAddingChild($child);
        checkAnItem($indent + 4, $model, $child, $selectionModel, $childTreePath);
    }
}

sub main
{
    startApplication("ItemViews.class");
    my $treeWidgetName = ":Item Views_javax.swing.JTree";
    my $treeWidget = waitForObject($treeWidgetName);
    my $model = $treeWidget->getModel();
    my $selectionModel = $treeWidget->getSelectionModel();
    my $treePath = new javax_swing_tree_TreePath($model->getRoot());
    checkAnItem(-1, $model, $model->getRoot(), $selectionModel, $treePath);
}

Tcl
proc checkAnItem {indent model item selectionModel treePath} {
    if {$indent > -1} {
        set selected ""
        if {[invoke $selectionModel isPathSelected $treePath]} {
            set selected " +selected"
        }
        set offset [string repeat " " $indent]
        set text [invoke $item toString]
        test log "|$offset '$text'$selected"
    } else {
        set indent -4
    }
    for {set row 0} {$row < [invoke $model getChildCount $item]} {incr row} {
        set child [invoke $model getChild $item $row]
        set childTreePath [invoke $treePath pathByAddingChild $child]
        set offset [expr $indent + 4]
        checkAnItem $offset $model $child $selectionModel $childTreePath
    }
}
       
proc main {} {
    startApplication "ItemViews.class"
    set treeWidgetName ":Item Views_javax.swing.JTree"
    set treeWidget [waitForObject $treeWidgetName]
    set model [invoke $treeWidget getModel]
    set selectionModel [invoke $treeWidget getSelectionModel]
    set root [invoke $model getRoot]
    set treePath [construct javax_swing_tree_TreePath $root]
    checkAnItem -1 $model $root $selectionModel $treePath
}



The key difference from JList and JTable is that since JTrees are recursive it is easiest if we ourselves use recursion to iterate over all the items. We have also kept track of the "path" of each item since we need this to determine which items are selected—there is no need to do this if we only want to retrieve attributes of the items themselves. And just as with the previous examples, all the output goes to Squish's log, although it is easy to adapt the script to perform other tests.

15.1.16.2.3. How to Test JTable and Use External Data Files (Java—AWT/Swing)

In this section we will see how to test the CsvTable.java program shown below. This program uses a JTable to present the contents of a .csv (comma-separated values) file, and provides some basic functionality for manipulating the data—inserting and deleting rows and swapping columns. [5] As we review the tests we will learn how to import test data, manipulate the data, and compare what the JTable shows with what we expect its contents to be. And since the CSV Table program is a main-window-style application, we will also learn how to test that menu options behave as expected.

The CSV Table program.

The source code for this example is in the directory SQUISHROOT/examples/java/csvtable, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHROOT/examples/java/csvtable/suite_py, and the JavaScript version of the tests is in SQUISHROOT/examples/java/csvtable/suite_js.

The first test we will look at is deceptively simple and consists of just four executable statements. This simplicity is achieved by putting almost all the functionality into a shared script, to avoid code duplication. Here is the code:

Example 15.27. The tst_loading Test Script

Python
def main():
    startApplication("CsvTable.class")
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename)
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    compareTableWithDataFile(jtable, filename)

JavaScript
function main()
{
    startApplication("CsvTable.class");
    source(findFile("scripts", "common.js"));
    var filename = "before.csv";
    doFileOpen(filename);
    var jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    compareTableWithDataFile(jtable, filename);
}

Perl
sub main
{
    startApplication("CsvTable.class");
    source(findFile("scripts", "common.pl"));
    my $filename = "before.csv";
    doFileOpen($filename);
    my $jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    compareTableWithDataFile($jtable, $filename);
}

Tcl
proc main {} {
    startApplication "CsvTable.class"
    source [findFile "scripts" "common.tcl"]
    set filename "before.csv"
    doFileOpen $filename
    set jtable [waitForObject {{type='javax.swing.JTable' visible='true'}}]
    compareTableWithDataFile $jtable $filename
}



We begin by loading in the script that contains common functionality, just as we did in an earlier section. Then we call a custom doFileOpen() function that tells the program to open the given file—and this is done through the user interface as we will see. Next we get a reference to the JTable using the waitForObject() function, and finally we check that the JTable's contents match the contents of the data file held amongst the test suite's test data. Note that both the CSV Table program and Squish load and parse the data file using their own completely independent code. (See How to Create and Use Shared Data and Shared Scripts (Section 15.4) for how to import test data into Squish.)

Now we will look at the custom functions we have used in the above test.

Example 15.28. Extracts from the Shared Scripts

Python
def doFileOpen(filename):
    chooseMenuOptionByKey("F", "o")
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    # Platform-specific name
    fileDialogEntryName = ("{leftWidget=':Open.File Name:"
        "_javax.swing.plaf.metal.MetalFileChooserUI$AlignedLabel' "
        "type='javax.swing.plaf.metal.MetalFileChooserUI$3' visible='true' "
        "window=':Open_javax.swing.JDialog'}")
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, filename)
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, "<Return>")
    

def chooseMenuOptionByKey(menuKey, optionKey):
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    type(paneName, "<Alt+%s>" % menuKey)
    waitForObject(paneName)
    type(paneName, optionKey)

    
def compareTableWithDataFile(jtable, filename):
    tableModel = jtable.getModel()
    for row, record in enumerate(testData.dataset(filename)):
        for column, name in enumerate(testData.fieldNames(record)):
            text = tableModel.getValueAt(row, column).toString()
            test.compare(testData.field(record, name), text)

JavaScript
function doFileOpen(filename)
{
    chooseMenuOptionByKey("F", "o");
    var paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject(paneName);
    // Platform-specific name
    var fileDialogEntryName = ("{leftWidget=':Open.File Name:" +
        "_javax.swing.plaf.metal.MetalFileChooserUI$AlignedLabel' " +
        "type='javax.swing.plaf.metal.MetalFileChooserUI$3' visible='true' " +
        "window=':Open_javax.swing.JDialog'}");
    waitForObject(fileDialogEntryName);
    type(fileDialogEntryName, filename);
    waitForObject(fileDialogEntryName);
    type(fileDialogEntryName, "<Return>");
}
    

function chooseMenuOptionByKey(menuKey, optionKey)
{
    var paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject(paneName);
    type(paneName, "<Alt+" + menuKey + ">")
    waitForObject(paneName);
    type(paneName, optionKey);
}

    
function compareTableWithDataFile(jtable, filename)
{
    var tableModel = jtable.getModel();
    var records = testData.dataset(filename);
    for (var row = 0; row < records.length; ++row) {
        columnNames = testData.fieldNames(records[row]);
        for (var column = 0; column < columnNames.length; ++column) {
            text = tableModel.getValueAt(row, column).toString();
            test.compare(testData.field(records[row], column), text);
        }
    }
}

Perl
sub doFileOpen
{
    my $filename = shift(@_);
    chooseMenuOptionByKey("F", "o");
    my $paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject($paneName);
    # Platform-specific name
    my $fileDialogEntryName = ("{leftWidget=':Open.File Name:" .
        "_javax.swing.plaf.metal.MetalFileChooserUI\$AlignedLabel' " .
        "type='javax.swing.plaf.metal.MetalFileChooserUI\$3' visible='true' " .
        "window=':Open_javax.swing.JDialog'}");
    waitForObject($fileDialogEntryName);
    type($fileDialogEntryName, $filename);
    waitForObject($fileDialogEntryName);
    type($fileDialogEntryName, "<Return>");
}
    

sub chooseMenuOptionByKey
{
    my($menuKey, $optionKey) = @_;
    my $paneName = "{type='javax.swing.JRootPane' visible='true'}";
    waitForObject($paneName);
    type($paneName, "<Alt+$menuKey>");
    waitForObject($paneName);
    type($paneName, $optionKey);
}

    
sub compareTableWithDataFile
{
    my ($jtable, $filename) = @_;
    my $tableModel = $jtable->getModel();
    my @records = testData::dataset($filename);
    for (my $row = 0; $row < scalar(@records); $row++) {
        my @columnNames = testData::fieldNames($records[$row]);
        for (my $column = 0; $column < scalar(@columnNames); $column++) {
            my $text = $tableModel->getValueAt($row, $column)->toString();
            test::compare($text, testData::field($records[$row], $column));
        }
    }
}

Tcl
proc doFileOpen {filename} {
    chooseMenuOptionByKey "F" "o"
    set paneName {{type='javax.swing.JRootPane' visible='true'}}
    waitForObject $paneName
    # Platform-specific name
    set fileDialogEntryName {{leftWidget=':Open.File Name:_javax.swing.plaf.metal.MetalFileChooserUI$AlignedLabel' type='javax.swing.plaf.metal.MetalFileChooserUI$3' visible='true' window=':Open_javax.swing.JDialog'}}
    waitForObject $fileDialogEntryName
    invoke type $fileDialogEntryName $filename
    waitForObject $fileDialogEntryName
    invoke type $fileDialogEntryName "<Return>"
}
    

proc chooseMenuOptionByKey {menuKey optionKey} {
    set paneName {{type='javax.swing.JRootPane' visible='true'}}
    waitForObject $paneName
    invoke type $paneName "<Alt+$menuKey>"
    waitForObject $paneName
    invoke type $paneName $optionKey
}

    
proc compareTableWithDataFile {jtable filename} {
    set data [testData dataset $filename]
    set tableModel [invoke $jtable getModel]
    for {set row 0} {$row < [llength $data]} {incr row} {
	set columnNames [testData fieldNames [lindex $data $row]]
	for {set column 0} {$column < [llength $columnNames]} {incr column} {
            set item [invoke $tableModel getValueAt $row $column]
            test compare [testData field [lindex $data $row] $column] [invoke $item toString]
	}
    }
}



The doFileOpen() function begins by opening a file through the user interface. This is done by using the custom chooseMenuOptionByKey() function. The file dialog used may not be the same on all platforms so the name of the text entry (in this case of type AlignedLabel) may vary, so we have added a note that the name is platform-specific. Apart from that using the dialog itself is straightforward—we simply type in the filename into the text entry and the type Return to confirm the choice.

The chooseMenuOptionByKey() function simulates the user clicking Alt+k (where k is a character, for example "F" for the file menu), and then the character that corresponds to the required action, (for example, "o" for "Open").

When the file is opened, the program is expected to load the file's data. We check that the data has been loaded correctly by comparing the data shown in the JTable and the data file itself. This comparison is done by the custom compareTableWithDataFile() function. This function uses Squish's testData.dataset() function to load in the data so that it can be accessed through the Squish API. We expect every cell in the table to match the corresponding item in the data, and we check that this is the case using the test.compare() function.

Now that we know how to compare a table's data with the data in a file we can perform some more ambitious tests. We will load in the before.csv file, delete a few rows, insert a new row in the middle, and append a new row at the end. Then we will swap a few pairs of columns. At the end the data should match the after.csv file.

Rather than writing code to do all these things we can simply record a test script that opens the file and performs all the deletions, insertions, and column swaps. Then we can edit the recorded test script to add a few lines of code near the end to compare the actual results with the expected results. Shown below is an extract from the test script starting one line above the hand written code and continuing to the end of the script:

Example 15.29. Extracts from the tst_editing Script

Python
    waitForObject(":CSV Table - before.csv_javax.swing.JRootPane")
# Added by Hand
    source(findFile("scripts", "common.py"))
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    tableModel = jtable.getModel()
    test.verify(tableModel.getColumnCount() == 5)
    test.verify(tableModel.getRowCount() == 11)
    compareTableWithDataFile(jtable, "after.csv")
# End of Added by Hand
    type(":CSV Table - before.csv_javax.swing.JRootPane", "<Alt+F>")
    waitForObject(":CSV Table - before.csv_javax.swing.JRootPane")
    type(":CSV Table - before.csv_javax.swing.JRootPane", "q")
    waitForObject(":CSV Table - before.csv.Yes_javax.swing.JButton")
    type(":CSV Table - before.csv.Yes_javax.swing.JButton", "<Alt+N>")

JavaScript
    waitForObject(":CSV Table - before.csv_javax.swing.JRootPane");
// Added by Hand
    source(findFile("scripts", "common.js"))
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    tableModel = jtable.getModel()
    test.verify(tableModel.getColumnCount() == 5)
    test.verify(tableModel.getRowCount() == 10)
    compareTableWithDataFile(jtable, "after.csv")
// End of Added by Hand
    type(":CSV Table - before.csv_javax.swing.JRootPane", "<Alt+F>");
    waitForObject(":CSV Table - before.csv_javax.swing.JRootPane");
    type(":CSV Table - before.csv_javax.swing.JRootPane", "q");
    waitForObject(":CSV Table - before.csv.Yes_javax.swing.JButton");
    type(":CSV Table - before.csv.Yes_javax.swing.JButton", "<Alt+N>");
}

Perl
    waitForObject(":CSV Table - before.csv_javax.swing.JRootPane");
# Added by Hand
    source(findFile("scripts", "common.pl"));
    my $jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    my $tableModel = $jtable->getModel();
    test::verify($tableModel->getColumnCount() == 5);
    test::verify($tableModel->getRowCount() == 11);
    compareTableWithDataFile($jtable, "after.csv");
# End of Added by Hand
    type(":CSV Table - before.csv_javax.swing.JRootPane", "<Alt+F>");
    waitForObject(":CSV Table - before.csv_javax.swing.JRootPane");
    type(":CSV Table - before.csv_javax.swing.JRootPane", "q");
    waitForObject(":CSV Table - before.csv.Yes_javax.swing.JButton");
    type(":CSV Table - before.csv.Yes_javax.swing.JButton", "<Alt+N>");
}

Tcl
    waitForObject ":CSV Table - before.csv_javax.swing.JRootPane"
# Added by Hand
    source [findFile "scripts" "common.tcl"]
    set jtable [waitForObject {{type='javax.swing.JTable' visible='true'}}]
    set tableModel [invoke $jtable getModel]
    test compare [invoke $tableModel getColumnCount] 5
    test compare [invoke $tableModel getRowCount] 11
    compareTableWithDataFile $jtable "after.csv"
# End of Added by Hand
    invoke type ":CSV Table - before.csv_javax.swing.JRootPane" "<Alt+F>" 
    waitForObject ":CSV Table - before.csv_javax.swing.JRootPane"
    invoke type ":CSV Table - before.csv_javax.swing.JRootPane" "q" 
    waitForObject ":CSV Table - before.csv.Yes_javax.swing.JButton"
    invoke type ":CSV Table - before.csv.Yes_javax.swing.JButton" "<Alt+N>" 
}



As the extract indictates, the added lines are not inserted at the end of the recorded test script, but rather just before the program is terminated—after all, we need the program to be running to query its JTable. (The reason that the row counts differ is that slightly different interactions were recorded for each scripting language.)

This example shows the power of combining recording with hand editing. If at a later date a new feature was added to the program we could incorporate tests for it in a number of ways. The simplest would be to just add another test script, do the recording, and then add in the lines needed to compare the table with the expected data. Another approach would be to record the use of the new feature in a temporary test and then copy and paste the recording into the existing test at a suitable place and then change the file to be compared at the end to one that accounts for all the changes to the original data and also the changes that are a result of using the new feature.

15.1.16.3. How to Test Java™ SWT Applications

In the subsections that follow we will focus on testing Java™ SWT widgets, both single-valued widgets like buttons and date/time edits, and multi-valued widgets such as lists, tables, and trees. We will also cover testing using external data files.

15.1.16.3.1. How to Test Stateful and Single-Valued Widgets (Java™/SWT)

In this section we will see how to test the examples/java/paymentform_swt/PaymentFormSWT.java example program. This program uses many basic Java™/SWT widgets including Button, Combo, DateTime, TabFolder, and Text. As part of our coverage of the example we will show how to check the values and state of individual widgets. We will also demonstrate how to test a form's business rules.

The PaymentFormSWT example in "pay by credit card" mode.

The PaymentFormSWT is invoked when an invoice is to be paid, either at a point of sale, or—for credit cards—by phone. The form's Pay button must only be enabled if the correct fields are filled in and have valid values. The business rules that we must test for are as follows:

  • In "cash" mode, i.e., when the Cash tab is checked:

    • The minimum payment is one dollar and the maximum is $2000 or the amount due, whichever is smaller.

  • In "check" mode, i.e., when the Check tab is checked:

    • The minimum payment is $10 and the maximum is $250 or the amount due, whichever is smaller.

    • The check date must be no earlier than 30 days ago and no later than tomorrow, and must initially be set to today's date.

    • The bank name, bank number, account name, and account number line edits must all be nonempty.

    • The check signed checkbox must be checked.

  • In "card" mode, i.e., when the Credit Card tab is checked:

    • The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.

    • The issue date must be no earlier than three years ago, and must be initially set to the earliest possible date.

    • The expiry date must be at least one month later than today, and must be initially set to the earliest possible date.

    • The account name and account number line edits must be nonempty.

[Note]Note

Java™/SWT's DateTime control for Eclipse 3.4 does not support the setting of date ranges, so although the application does constrain the date ranges using listeners, we cannot easily test this in hand written code. One simple solution is to record a script where an attempt is made to change to an out of range date, and either run that as a separate test or copy and paste the relevant lines into a hand written script.

We will write three tests, one for each of the form's modes.

The source code for the payment form is in the directory SQUISHROOT/examples/java/paymentform_swt, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHROOT/examples/java/paymentform_swt/suite_py, and the JavaScript version of the tests is in SQUISHROOT/examples/java/paymentform_swt/suite_js.

We will begin by reviewing the test script for testing the form's "cash" mode. First we will show the code, then we will explain it.

Example 15.30. The tst_cash_mode Test Script

Python
def main():
    startApplication("PaymentFormSWT.class")
    # Start with the correct tab
    tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    tabFolder = waitForObject(tabFolderName)
    clickTab(tabFolder, "C&ash")

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    amountDueLabelName = ("{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' "
            "visible='true' window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    amountDueLabel = waitForObject(amountDueLabelName)
    chars = []
    for char in unicode(amountDueLabel.getText()):
        if char.isdigit():
            chars.append(char)
    amount_due = cast("".join(chars), int)
    maximum = min(2000, amount_due)
    paymentSpinnerName = ("{isvisible='true' type='org.eclipse.swt.widgets.Spinner' "
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    paymentSpinner = waitForObject(paymentSpinnerName)
    test.verify(paymentSpinner.getMinimum() == 1)
    test.verify(paymentSpinner.getMaximum() == maximum)
    
    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    payButton = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())

JavaScript
function main()
{
    startApplication("PaymentFormSWT.class");
    // Start with the correct tab
    var tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
    var tabFolder = waitForObject(tabFolderName);
    clickTab(tabFolder, "C&ash");

    // Business rule #1: the minimum payment is $1 and the maximum is
    // $2000 or the amount due whichever is smaller
    var amountDueLabelName = "{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    var amountDueLabel = waitForObject(amountDueLabelName);
    var chars = [];
    var amountDueText = new String(amountDueLabel.text);
    for (var i = 0; i < amountDueText.length; ++i) {
        var ch = amountDueText.charAt(i);
        if ("0123456789".indexOf(ch) > -1) {
            chars.push(ch);
        }
    }

    var amount_due = parseFloat(chars.join(""));
    var maximum = Math.min(2000, amount_due);

    var paymentSpinnerName = "{isvisible='true' type='org.eclipse.swt.widgets.Spinner' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    var paymentSpinner = waitForObject(paymentSpinnerName);
    test.verify(paymentSpinner.getMinimum() == 1);
    test.verify(paymentSpinner.getMaximum() == maximum);
    
    // Business rule #2: the Pay button is enabled (since the above tests
    // ensure that the payment amount is in range)
    var payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.isEnabled());
}

Perl
sub main
{
    startApplication("PaymentFormSWT.class");
    # Start with the correct tab
    my $tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
    my $tabFolder = waitForObject($tabFolderName);
    clickTab($tabFolder, "C&ash");

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    my $amountDueLabelName = "{caption?='[\$][0-9.,]*' type='org.eclipse.swt.widgets.Label' " .
            "visible='true' window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    my $amountDueLabel = waitForObject($amountDueLabelName);
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    my $maximum = 2000 < $amount_due ? 2000 : $amount_due;
    my $paymentSpinnerName = "{isvisible='true' type='org.eclipse.swt.widgets.Spinner' " .
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    my $paymentSpinner = waitForObject($paymentSpinnerName);
    test::verify($paymentSpinner->getMinimum() == 1);
    test::verify($paymentSpinner->getMaximum() == $maximum);
    
    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    my $payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
    my $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}

Tcl
proc main {} {
    startApplication "PaymentFormSWT.class"
    # Start with the correct tab
    set tabFolderName ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    set tabFolder [waitForObject $tabFolderName]
    invoke clickTab $tabFolder "C&ash"

    # Business rule #1: the minimum payment is $1 and the maximum is
    # $2000 or the amount due whichever is smaller
    set amountDueLabelName {{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
    set amountDueLabel [waitForObject $amountDueLabelName]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    set amount_due [expr $amountText]
    set maximum [expr $amount_due < 2000 ? $amount_due : 2000]
    set paymentSpinnerName {{isvisible='true' type='org.eclipse.swt.widgets.Spinner' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
    set paymentSpinner [waitForObject $paymentSpinnerName]
    test compare [invoke $paymentSpinner getMinimum] 1
    test compare [invoke $paymentSpinner getMaximum] $maximum
    
    # Business rule #2: the Pay button is enabled (since the above tests
    # ensure that the payment amount is in range)
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}



We must start by making sure that the form is in the mode we want to test. In general, the way we gain access to visible widgets is always the same: we create a variable holding the widget's name, then we call waitForObject() to get a reference to the widget. Once we have the reference we can use it to access the widget's properties and to call the widget's methods. In this case we use waitForObject() to get a reference to the TabFolder widget and then use the clickTab() function to click the tab we are interested in. How did we know the tab folder's name? We used the How to Use the Spy (Section 15.2.4) facility.

The first business rule to be tested concerns the minimum and maximum allowed payment amounts. As usual we begin calling waitForObject() to get references to the widgets we are interested in—in this case starting with the amount due label. Because the amount due label's text varies depending on the amount due we cannot have a fixed name for it. So instead we identify it using a multiproperty name using wildcards. The wildcard of [$][0-9.,]* matches any text that starts with a dollar sign and is followed by zero or more digits, periods and commas. Squish can also do regular expression matching—see Improving Object Identification (Section 16.8) for more about matching.

Since the label's text might contain a currency symbol and grouping markers (for example, $1,700 or €1.700), to convert its text into an integer we must strip away any non-digit characters first. We do this in different ways depending on the underlying scripting language. (For example, in Python, we iterate over each character and join all those that are digits into a single string and use the cast() function which takes an object and the type the object should be converted to, and returns an object of the requested type—or 0 on failure. We use a similar approach in JavaScript, but for Perl and Tcl we simply replace non-digit characters using a regular expression.) The resulting integer is the amount due, so we can now trivially calculate the maximum amount that can be paid in cash.

With the minimum and maximum amounts known we next get a reference to the payment Spinner, again using Spy to find out the spinner's name. Once we have a reference to the spinner, we use the test.verify() method to ensure that is has the correct minimum and maximum amounts set. (For Tcl we have used the test.compare() method instead of test.verify() since it is more convenient to do so.)

Checking the last business rule is easy in this case since if the amount is in range (and it must be because we have just checked it), then payment is allowed so the Pay button should be enabled. Once again, we use the same approach to test this: first we call waitForObject() to get a reference to it, and then we conduct the test—in this case checking that the Pay button is enabled.

Although the "cash" mode test works well, there are a few places where we use essentially the same code. So before creating the test for the "check" and "card" modes, we will create some common functions that we can use to refactor our tests with. (The process used to create shared code is described a little later in How to Create and Use Shared Data and Shared Scripts (Section 15.4)—essentially all we need to do is create a new script under the Test Suite's shared item's scripts item.) The Python common code is in common.py, the JavaScript common code is in common.js, and so on.

Example 15.31. The Shared Code

Python
def clickTabItem(name):
    tabFolderName = ("{isvisible='true' type='org.eclipse.swt.widgets.TabFolder' "
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    tabFolder = waitForObject(tabFolderName)
    clickTab(tabFolder, name)


def dateTimeEqualsDate(dateTime, date):
    return (dateTime.getYear() == date.get(java_util_Calendar.YEAR) and
            dateTime.getMonth() == date.get(java_util_Calendar.MONTH) and
            dateTime.getDay() == date.get(java_util_Calendar.DAY_OF_MONTH))

    
def getAmountDue():
    amountDueLabelName = ("{caption?='[$][0-9.,]*' "
            "type='org.eclipse.swt.widgets.Label' visible='true' "
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    amountDueLabel = waitForObject(amountDueLabelName)
    chars = []
    for char in unicode(amountDueLabel.getText()):
        if char.isdigit():
            chars.append(char)
    return cast("".join(chars), int)


def checkPaymentRange(minimum, maximum):
    paymentSpinner = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Spinner' "
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
    test.verify(paymentSpinner.getMinimum() == minimum)
    test.verify(paymentSpinner.getMaximum() == maximum)

JavaScript
function clickTabItem(name)
{
    var tabFolderName = "{isvisible='true' type='org.eclipse.swt.widgets.TabFolder' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    var tabFolder = waitForObject(tabFolderName);
    clickTab(tabFolder, name);
}


function dateTimeEqualsDate(dateTime, aDate)
{
    return (dateTime.getYear() == aDate.get(java_util_Calendar.YEAR) &&
            dateTime.getMonth() == aDate.get(java_util_Calendar.MONTH) &&
            dateTime.getDay() == aDate.get(java_util_Calendar.DAY_OF_MONTH));
}

    
function getAmountDue()
{
    var amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' " +
            "type='org.eclipse.swt.widgets.Label' visible='true' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
    var chars = [];
    var amountDueText = new String(amountDueLabel.text);
    for (var i = 0; i < amountDueText.length; ++i) {
        var ch = amountDueText.charAt(i);
        if ("0123456789".indexOf(ch) > -1) {
            chars.push(ch);
        }
    }
    return parseFloat(chars.join(""));
}


function checkPaymentRange(minimum, maximum)
{
    var paymentSpinner = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Spinner' " +
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
    test.verify(paymentSpinner.getMinimum() == minimum);
    test.verify(paymentSpinner.getMaximum() == maximum);
}

Perl
sub clickTabItem
{
    my $name = shift(@_);
    my $tabFolderName = "{isvisible='true' type='org.eclipse.swt.widgets.TabFolder' " .
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
    my $tabFolder = waitForObject($tabFolderName);
    clickTab($tabFolder, $name);
}


sub dateTimeEqualsDate
{
    my ($dateTime, $date) = @_;
    return ($dateTime->getYear() == $date->get(java_util_Calendar::YEAR) &&
            $dateTime->getMonth() == $date->get(java_util_Calendar::MONTH) &&
            $dateTime->getDay() == $date->get(java_util_Calendar::DAY_OF_MONTH));
}

    
sub getAmountDue
{
    my $amountDueLabel = waitForObject("{caption?='[\$][0-9.,]*' " .
            "type='org.eclipse.swt.widgets.Label' visible='true' " .
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
    my $amount_due = $amountDueLabel->text;
    $amount_due =~ s/\D//g; # remove non-digits
    return $amount_due;
}


sub checkPaymentRange
{
    my ($minimum, $maximum) = @_;
    my $paymentSpinner = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Spinner' " .
            "window=':Payment Form_org.eclipse.swt.widgets.Shell'}");
    test::verify($paymentSpinner->getMinimum() == $minimum);
    test::verify($paymentSpinner->getMaximum() == $maximum);
}

Tcl
proc clickTabItem {name} {
    set tabFolderName {{isvisible='true' type='org.eclipse.swt.widgets.TabFolder' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
    set tabFolder [waitForObject $tabFolderName]
    invoke clickTab $tabFolder $name
}


proc getAmountDue {} {
    set amountDueLabelName {{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
    set amountDueLabel [waitForObject $amountDueLabelName]
    set amountText [toString [property get $amountDueLabel text]]
    regsub -all {\D} $amountText "" amountText
    return [expr $amountText]
}


proc dateTimeEqualsDate {dateTime date} {
    set yearsMatch [expr [invoke $dateTime getYear] == [invoke $date get [property get java_util_Calendar YEAR]]]
    set monthsMatch [expr [invoke $dateTime getMonth] == [invoke $date get [property get java_util_Calendar MONTH]]]
    set daysMatch [expr [invoke $dateTime getDay] == [invoke $date get [property get java_util_Calendar DAY_OF_MONTH]]]
    if {$yearsMatch && $monthsMatch && $daysMatch} {
        return true
    }
    return false
}


proc checkPaymentRange {minimum maximum} {
    set paymentSpinner [waitForObject {{isvisible='true' type='org.eclipse.swt.widgets.Spinner' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}]
    test compare [invoke $paymentSpinner getMinimum] $minimum
    test compare [invoke $paymentSpinner getMaximum] $maximum
    
}



Now we can write our tests for "check" and "card" modes and put more of our effort into testing the business rules and less into some of the basic chores. The code for "check" mode is quite long, but we have broken it down into a main() function—the only function that Squish will call—and a couple of test-specific supporting functions that help keep the main() function short and clear, in addition to making use of the common functions we saw above.

Example 15.32. The tst_check_mode Test Script's main() function

Python
def main():
    startApplication("PaymentFormSWT.class")
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabItem("Chec&k")

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(10, min(250, amount_due))
    
    # Business rule #2: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    checkDateTime = waitForObject(":Check.Check Date:_DateTime")
    today = java_util_Calendar.getInstance()
    test.verify(dateTimeEqualsDate(checkDateTime, today))
    
    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    payButton = findObject(payButtonName)
    test.verify(not payButton.isEnabled())
    
    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked()
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())

JavaScript
function main()
{
    startApplication("PaymentFormSWT.class");
    // Import functionality needed by more than one test script
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabItem("Chec&k");

    // Business rule #1: the minimum payment is $10 and the maximum is
    // $250 or the amount due whichever is smaller
    var amount_due = getAmountDue();
    checkPaymentRange(10, Math.min(250, amount_due));
    
    // Business rule #2: the check date must be no earlier than 30 days 
    // ago and no later than tomorrow, and must initially be set to
    // today. Here we just check its initial value.
    var checkDateTime = waitForObject(":Check.Check Date:_DateTime");
    var today = java_util_Calendar.getInstance();
    test.verify(dateTimeEqualsDate(checkDateTime, today));
    
    // Business rule #3: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use findObject() without waiting
    var payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
    var payButton = findObject(payButtonName);
    test.verify(!payButton.isEnabled());
    
    // Business rule #4: the check must be signed (and if it isn't we
    // will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();
    
    // Business rule #5: the Pay button should be enabled since all the 
    // previous tests pass, the check is signed and now we have filled in
    // the account details
    populateCheckFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.isEnabled());
}

Perl
sub main
{
    startApplication("PaymentFormSWT.class");
    # Import functionality needed by more than one test script
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabItem("Chec&k");

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    my $amount_due = getAmountDue();
    checkPaymentRange(10, $amount_due < 250 ? $amount_due : 250);
    
    # Business rule #2: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    my $checkDateTime = waitForObject(":Check.Check Date:_DateTime");
    my $today = java_util_Calendar::getInstance();
    test::verify(dateTimeEqualsDate($checkDateTime, $today));
    
    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    my $payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
    my $payButton = findObject($payButtonName);
    test::verify(!$payButton->isEnabled());
    
    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked();
    
    # Business rule #5: the Pay button should be enabled since all the 
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields();
    my $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}

Tcl
proc main {} {
    startApplication "PaymentFormSWT.class"
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabItem "Chec&k"

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    set amount_due [getAmountDue]
    checkPaymentRange 10 [expr 250 > $amount_due ? $amount_due : 250]
    
    # Business rule #2: the check date must be no earlier than 30 days 
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    set checkDateTime [waitForObject ":Check.Check Date:_DateTime"]
    set today [invoke java_util_Calendar getInstance]
    test verify [dateTimeEqualsDate $checkDateTime $today]
    
    # Business rule #3: the Pay button is disabled (since the form's data