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
15.1.22. How to Modify Squish Functions

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. And many examples are presented to show how to do things in practice. (For a complete reference to the Squish APIs see the Tools Reference Manual (Chapter 16).)

[Important]Important

The Squish IDE loads and saves test scripts (e.g., files with names matching test.*), as Unicode text using the UTF-8 encoding. All the Squish tools assume that UTF-8 is used for all the scripts they execute. (See the Editor view (Section 17.2.5).) If you don't edit your test scripts using the Squish IDE, make sure that the editor you use loads and saves the scripts using UTF-8; or, if your 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". (This is a requirement shared by all the scripting languages that Squish supports.)

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 one or more space-separated property items (each having the form, propertyname='value'), and finally a closing brace. For most toolkits at least two properties must be specified with one being 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. Furthermore, most toolkits don't even allow objects to be named. 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.

See also, the Application Objects view (Section 17.2.1) and the Properties view (Section 17.2.10).

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 a 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.10).

If there is no obvious way of identifying an object, either use Squish's Spy tool (How to Use the Spy (Section 15.2.3)) 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.10) 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.9).

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 in the Test Results view (Section 17.2.14). 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:

Python
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:

Python
try:
    moreOptionsButtonName = "{type='QPushButton' name='More Options'}"
    moreOptionsButton = waitForObject(moreOptionsButtonName)
except LookupError:
    pass # button doesn't exist so don't try to click it
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. It is for this reason that it is almost always better to use symbolic names rather than real names whenever possible. (See Object Map (Section 16.10) and the Object Map view (Section 17.2.8).)

(Incidentally, Squish distinguishes between the two kinds of names 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 powerful features is its ability to access the complete Qt API (and the AUT's 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 convenience API (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 How to Test Qt Widgets (Section 15.1.11) section later in this manual presents many 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.verify(cashRadioButton.checked)
    
    # 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)

JavaScript
    var cashRadioButtonName = "{text='Cash' type='QRadioButton' " +
            "visible='1' window=':Make Payment_MainWindow'}";
    var cashRadioButton = waitForObject(cashRadioButtonName);
    if (!cashRadioButton.checked) {
        clickButton(cashRadioButton);
    }
    test.verify(cashRadioButton.checked);
    
    // 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);

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 verify [property get $cashRadioButton checked]
    
    # 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

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 (i.e., to the Test Results view (Section 17.2.14)).

Python
lineedit = waitForObject(":Forename:_LineEdit")
lineedit.text = "A new text"
text = lineedit.text
test.log(str(text))
JavaScript
var lineedit = waitForObject(":Forename:_LineEdit");
lineedit.text = "A new text";
var text = lineedit.text;
test.log(String(text));
Perl
my $lineedit = waitForObject(":Forename:_LineEdit");
$lineedit->text = "A new text";
my $text = $lineedit->text;
test::log("$text");
Tcl
set lineedit [waitForObject ":Forename:_LineEdit"]
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. Symbolic names are what Squish records, and it is almost always better to use them rather than real names. When we copy and paste or modify code from a recorded script we will often use symbolic names like these.

[Tip]Converting QStrings to Native Strings

In the examples above it is notable that the queried text from QLineEdit::text can't be directly passed to the test.log function (or to native print functions such as print or puts). This is because the property returns a QString object and the script functions to print a string expect a native string—i.e., a Python str, or JavaScript String, and so on. This conversion must be done explicitly as shown in the examples (although in the Perl case we did it indirectly using string interpolation).

The conversion in the other direction (i.e., passing a native string to a Qt API function that expects a QString) is done automatically by Squish, so no explicit conversion is necessary in such cases.

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 the QButton::setText function.

Python
button = waitForObject(":Address Book - Add.OK_QPushButton")
button.setText("Changed Button Text")
JavaScript
var button = waitForObject(":Address Book - Add.OK_QPushButton");
button.setText("Changed Button Text");
Perl
my $button = waitForObject(":Address Book - Add.OK_QPushButton");
$button->setText("Changed Button Text");
Tcl
set button [waitForObject ":Address Book - Add.OK_QPushButton"]
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 (i.e., the Test Results view (Section 17.2.14)). 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(":Address Book - Add.Forename:_QLabel")
label.setAlignment(Qt.AlignLeft)
JavaScript
var label = waitForObject(":Address Book - Add.Forename:_QLabel");
label.setAlignment(Qt.AlignLeft);
Perl
my $label = waitForObject(":Address Book - Add.Forename:_QLabel");
$label->setAlignment(Qt::AlignLeft);
Tcl
set label [waitForObject ":Address Book - Add.Forename:_QLabel"]
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 Tools 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—although here we click an item in a table), and the last example shows how to activate a menu option (in this case, File|Open).

Python
clickButton(":Address Book - Add.OK_QPushButton")
doubleClickItem(":CSV Table - before.csv.File_QTableWidget",
        "10/0", 22, 20, 0, Qt.LeftButton)
activateItem(waitForObjectItem(":Address Book_QMenuBar", "File"))
activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."))
JavaScript
clickButton(":Address Book - Add.OK_QPushButton");
doubleClickItem(":CSV Table - before.csv.File_QTableWidget",
        "10/0", 22, 20, 0, Qt.LeftButton);
activateItem(waitForObjectItem(":Address Book_QMenuBar", "File"));
activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."));
Perl
clickButton(":Address Book - Add.OK_QPushButton");
doubleClickItem(":CSV Table - before.csv.File_QTableWidget",
        "10/0", 22, 20, 0, Qt.LeftButton);
activateItem(waitForObjectItem(":Address Book_QMenuBar", "File"));
activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."));
Tcl
invoke clickButton ":Address Book - Add.OK_QPushButton"
invoke doubleClickItem ":CSV Table - before.csv.File_QTableWidget" \
        "10/0" 22 20 0 [enum Qt LeftButton]
invoke activateItem [waitForObjectItem ":Address Book_QMenuBar" "File"]
invoke activateItem [waitForObjectItem ":Address Book.File_QMenu" "Open..."]

See the How to Test Qt Widgets (Section 15.1.11) section for a wide range of examples of how to test various Qt widgets.

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 (see 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 How to Test Tk Widgets (Section 15.1.13) section later in this manual presents various examples that show how to use the scripting Tk API to access and test complex Tk widgets.

[Note]Tk Object Names

Squish uses a completely different object naming scheme for Tk applications than for other toolkits. Tk identifies objects using qualified names, e.g., myapp.myframe.mylabel. Squish takes advantage of Tk's existing naming scheme and uses it for identifying objects in Tk tests.

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 frame1, which in turn is a child of myapp (the applicaton's main window).

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.

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.3) section for details. Alternatively, record a quick throw-away test in which you interact with all the AUT objects you are interested in: this will populate the Object Map (Section 16.10) with the objects' names.

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:

Tcl
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:

Tcl
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:

Tcl
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 to the Test Results (Test Results view (Section 17.2.14)).

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:

Tcl
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 Tools 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.

Tcl
invoke clickButton [waitForObject \
    ":addressbook\\.tcl.dialog.buttonarea.ok"]
invoke type [waitForObject ":addressbook\\.tcl.dialog.email"] "com"
waitForObjectItem ":addressbook\\.tcl.#menuBar" "File"
invoke activateItem ":addressbook\\.tcl.#menuBar" "File"
waitForObjectItem ":addressbook\\.tcl.#menuBar.#file" "Open..."
invoke activateItem ":addressbook\\.tcl.#menuBar.#file" "Open..."

Here, we click a button, type some text into an entry widget, and invoke the File|Open menu option. These are the most commonly used Tk convenience functions, although there are additional ones in the API. For more examples of testing a variety of Tk widgets in AUTs see How to Test Tk Widgets (Section 15.1.13).

15.1.4. How to Use the XView API

[Under Construction  .]

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 a 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.10) 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.3) 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 catchable exception if the object isn't available. (An object might not be available because it is an AJAX object that only appears under certain conditions, or it might only appear as the result of some JavaScript code executing, and so on.) Here's a code snippet that shows how to use 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);
Tcl
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—which is returned as an object of type HTML_Object Class (Section 16.1.10.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.10.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.10) 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.10).

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 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:

# This will print out the name of the browser:
test.log("We are running in " + Browser.name())
if Browser.id() == InternetExplorer:
    ...
elif Browser.id() == Mozilla:
    ...
elif Browser.id() == Firefox:
    ...
elif Browser.id() == Safari:
    ...
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 : '' so if the document contains an element with ID busyDIV, style_display will be set to that element's style.display property's value—otherwise it will be set to an empty string.

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.10) section in the Tools 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 (clickLink, selectOption, and setText), although all of them are just as easy to use.

15.1.5.7. How to Synchronize Web Page Loading for Testing

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

However, in some cases we need to ensure that the page has loaded before we attempt to access its objects. The special 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(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"))
else:
    test.fatal("Page loading failed")
JavaScript
var loaded = waitFor("isPageLoaded()", 5000);
if (loaded)
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"));
else
    test.fatal("Page loading failed");
Perl
my $loaded = waitFor("isPageLoaded()", 5000);
if ($loaded) {
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"));
}
else {
    test::fatal("Page loading failed");
}
Tcl
set loaded [waitFor "[invoke isPageLoaded]" 5000]
if {$loaded} {
    invoke clickButton [invoke waitForObject \
        ":{tagName='INPUT' type='button' value='Login'}"]
} else {
    test fatal "Page loading failed"
}

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

In advanced AJAX applications, waiting for a page to be loaded and using the waitForObject function is often insufficient, since parts of the page will be loaded using asynchronous AJAX requests that occur in the background. In such cases we must take more sophisticated approaches to synchronization where we 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 syncBackbase():
    loading = waitFor("isBackbaseLoading()", 2000)
    if not loading:
        return
    waitFor("not isBackbaseLoading()")
JavaScript
function syncBackbase()
{
    var loading = waitFor("isBackbaseLoading()", 2000);
    if (!loading)
        return;
    waitFor("!isBackbaseLoading()");
}
Perl
sub syncBackbase
{
    my $loading = waitFor("isBackbaseLoading()", 2000);
    if (!$loading) {
        return;
    }
    waitFor("!isBackbaseLoading()");
}
Tcl
proc syncBackbase {} {
    set loading [waitFor "[invoke 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 syncBackbase 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 syncBackbase—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):
    syncBackbase()
JavaScript
function waitUntilObjectReady(obj)
{
    syncBackbase();
}
Perl
sub waitUntilObjectReady
{
    syncBackbase();
}
Tcl
proc waitUntilObjectReady {obj} {
    invoke syncBackbase
}

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.5.8. How to Handle Redirects Involving a Change of Protocol or Hostname

[Note]Squish for Web—Windows and Microsoft Internet Explorer-specific

This section describes a limitation and workaround that only applies to Squish for Web with Microsoft Internet Explorer.

When using Squish for Web with Microsoft Internet Explorer you might encounter a problem with web sites that automatically redirect their users to a URL that has a different protocol or hostname to the original URL. For example, if you were recording on the site http://www.example.com and completed a logon form that redirected to either https://www.example.com (i.e., a site with a different protocol), or to http://www.example-secured.com (i.e., a site with a different hostname), recording and subsequent replaying would fail.

The reason for this limitation is that some parts of Squish run inside the browser itself and for Squish to communicate amongst its components it uses a technique called cross-site-scripting. As a security measure, Internet Explorer disallows this kind of communication—unless the source URL (i.e., the web site being interacted with) is in Microsoft Internet Explorer's list of trusted sites.

In the case of a change of protocol the easiest solution is to change the URL that the loadUrl function uses so that it doesn't mention the protocol. For example, change "http://www.example.com" to "www.example.com". This should work in most cases since Internet Explorer uses wildcard matching for its trusted sites, so if it has an entry for *.example.com, any URL whose site name ends in that text will match—for example both http://www.example.com and https://secure.example.com will match.

In the case of a hostname that changes the easiest solution is to add the other hostname to Microsoft Internet Explorer's list of trusted sites. Alternatively, manually add a suitable call to the loadUrl function with the relevant hostname when it is needed.

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.10) 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.10.2.2) for more about handling object hierarchy changes.)

  • Multiple property (real) 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 for most GUI toolkits a multi-property name (also called a real name) must include a type property. However, Squish for Web and Squish for Windows don't need a type property specified—but will use it if it is present of course. 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.9) 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.3) 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 (Section 16.10) 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:

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:

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"

The test.exception function executes the given code and expects the code to throw an exception.

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, regardless of whether they are visible.

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]Package Names

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. See also, Wrapping custom Java™ classes (Section 16.4.7).

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) 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]Synthetic Properties

Synthetic properties—that is, properties created by Squish based on function signatures—make it easier to add more verification 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.7) for 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 Tools 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 clicked a button, typed in some text, and activated a menu and a menu item. 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 Create and Access Java™ Arrays

15.1.6.5.1. Accessing Java Arrays

Some of the methods in the Java™ API return Arrays (called JavaArrays in Squish) rather than single objects. The number of elements in such an array is accessible using the JavaArray.length property, and individual elements can be accessed using the JavaArray.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.6.5.2. Creating and Using JavaArrays

In addition to accessing the Java™ arrays returned by Squish functions as shown above, it is also possible to create your own Native Java Arrays (Section 16.1.7.1). Here are some examples to give a flavor of how JavaArrays are used. Notice that if we store items as java.lang.Objects, then we can store pretty well any kind of data we like.

Python
    variousObjects = JavaArray(5) # creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(":Payment Form.Cancel_javax.swing.JButton"))
    variousObjects.set(1, java_lang_Object())
    variousObjects.set(2, 4)           # converted to java.lang.Integer
    variousObjects.set(3, "some text") # converted to java.lang.String
    test.compare(variousObjects.at(0).text, "Cancel")
    test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object")
    test.compare(variousObjects.at(2), 4)
    test.compare(variousObjects.at(3), "some text")
    test.verify(variousObjects.length == 5)
    
    integers = JavaArray(42, "int") # creates an int[42] array
    integers.set(23, -71)
    test.compare(integers.at(23), -71)
    test.verify(integers.length == 42)
    
    strings = JavaArray(10, "java.lang.String") # java.lang.String[10]
    strings.set(4, "more text")
    test.compare(strings.at(4), "more text")
    test.verify(strings.length == 10)

JavaScript
    var variousObjects = new JavaArray(5); // creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(":Payment Form.Cancel_javax.swing.JButton"));
    variousObjects.set(1, new java_lang_Object());
    variousObjects.set(2, 4);           // converted to java.lang.Integer
    variousObjects.set(3, "some text"); // converted to java.lang.String
    test.compare(variousObjects.at(0).text, "Cancel");
    test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object");
    test.compare(variousObjects.at(2), 4);
    test.compare(variousObjects.at(3), "some text");
    test.verify(variousObjects.length == 5);
    
    var integers = new JavaArray(42, "int"); // creates an int[42] array
    integers.set(23, -71);
    test.compare(integers.at(23), -71);
    test.verify(integers.length == 42);
    
    var strings = new JavaArray(10, "java.lang.String"); // java.lang.String[10]
    strings.set(4, "more text");
    test.compare(strings.at(4), "more text");
    test.verify(strings.length == 10);

Perl
    my $variousObjects = JavaArray(5); # creates a java.lang.Object[5] array
    $variousObjects->set(0, waitForObject(":Payment Form.Cancel_javax.swing.JButton"));
    $variousObjects->set(1, java_lang_Object());
    $variousObjects->set(2, 4);           # converted to java.lang.Integer
    $variousObjects->set(3, "some text"); # converted to java.lang.String
    test::compare($variousObjects->at(0)->text, "Cancel");
    test::compare($variousObjects->at(1)->getClass()->getName(), "java.lang.Object");
    test::compare($variousObjects->at(2), 4);
    test::compare($variousObjects->at(3), "some text");
    test::verify($variousObjects->length == 5);
    
    my $integers = JavaArray(42, "int"); # creates an int[42] array
    $integers->set(23, -71);
    test::compare($integers->at(23), -71);
    test::verify($integers->length == 42);
    
    my $strings = JavaArray(10, "java.lang.String"); # java.lang.String[10]
    $strings->set(4, "more text");
    test::compare($strings->at(4), "more text");
    test::verify($strings->length == 10);

Tcl
    set variousObjects [construct JavaArray 5]
    invoke $variousObjects set 0 [waitForObject ":Payment Form.Cancel_javax.swing.JButton"]
    invoke $variousObjects set 1 [construct java_lang_Object]
    # converted to java.lang.Integer
    invoke $variousObjects set 2 4
    # converted to java.lang.String
    invoke $variousObjects set 3 "some text"
    test compare [property get [invoke $variousObjects at 0] text] "Cancel"
    test compare [invoke [invoke [invoke $variousObjects at 1] getClass] getName] "java.lang.Object"
    test compare [invoke $variousObjects at 2] 4
    test compare [invoke $variousObjects at 3] "some text"
    test compare [property get $variousObjects length] 5
    
    # creates an int[42] array
    set integers [construct JavaArray 42 "int"]
    invoke $integers set 23 [expr -71]
    test compare [invoke $integers at 23] -71
    test compare [property get $integers length] 42
    
    # java.lang.String[10]
    set strings [construct JavaArray 10 "java.lang.String"]
    invoke $strings set 4 "more text"
    test compare [invoke $strings at 4] "more text"
    test compare [property get $strings length] 10

The JavaArray API is documented in Native Java Arrays (Section 16.1.7.1).

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(":Address Book - Add.Forename:_QLabel")
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(":Address Book - Add.Forename:_QLabel");
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(":Address Book - Add.Forename:_QLabel");
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 ":Address Book - Add.Forename:_QLabel"]
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 Tools 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 inside 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 for Web-specific

For Squish for 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 for 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 for Java™, Squish for Qt, and Squish for 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 for Web event (e.g., ModalDialogOpened), the dialog would not be passed as an argument, because Squish for Web event handlers receive 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 for 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. The timeout time can be changed by using the squishrunner's or squishserver's setResponseTimeout option (see Configuring squishrunner (Section 16.5.1.7) or Configuring squishserver (Section 16.5.2.3)), or using the Squish IDE (see Squish pane's child panes (Section 17.3.11.7.1).)

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 for 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 for 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 test cases.

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(':Address Book - Save As_QFileDialog')",
    5000)
if not ok:
    test.fatal("AddressBook Save As dialog didn't appear")
JavaScript
var ok = waitFor("object.exists(':Address Book - Save As_QFileDialog')",
    5000);
if (!ok)
    test.fatal("AddressBook Save As dialog didn't appear");
Perl
my $ok = waitFor("object::exists(':Address Book - Save As_QFileDialog')",
    5000);
if (!$ok) {
    test::fatal("AddressBook Save As dialog didn't appear");
}
Tcl
set ok [waitFor "object exists ':Address Book - Save As_QFileDialog'" \
    5000]
if {!$ok} {
    test fatal "AddressBook Save As 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 next example waits up to 2 seconds for an OK button to become enabled. The waitFor function repeatedly evaluates the code it has been given as its first argument and returns true as soon as the code evaluates to true; or returns false if the code doesn't evaluate to true within the number of milliseconds specified by its second argument.

Python
button = waitForObject(":Address Book - Add.OK_QPushButton")
enabled = waitFor("button.enabled", 2000)
if not enabled:
    test.fatal("OK button has not been enabled")
JavaScript
var button = waitForObject(":Address Book - Add.OK_QPushButton");
var enabled = waitFor("button.enabled", 2000);
if (!enabled)
    test.fatal("OK button has not been enabled");
Perl
my $button = waitForObject(":Address Book - Add.OK_QPushButton");
my $enabled = waitFor("$button->enabled", 2000);
if (!$enabled) {
    test::fatal("OK button has not been enabled");
}
Tcl
set button [waitForObject ":Address Book - Add.OK_QPushButton"]
set enabled [waitFor {property get $button enabled} 2000]
if {!$enabled} {
    test fatal "OK button has not been enabled"
}

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 (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 by making use of the context object returned by the currentApplicationContext 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 (Section 17.2.15)) to make the test suite's Settings view (Section 17.2.13) 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 a 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]AUT Sub-processes

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.8.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 compare [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.

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, and of the currently running AUT by the currentApplicationContext 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 (Section 16.1.3.10) section details the properties and functions that are accessible from ApplicationContext objects. Here are some examples.

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

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

Python
ctx = currentApplicationContext()
peakMemory = 0
while ctx.isRunning:
    peakMemory = max(ctx.usedMemory, peakMemory)
    if not ctx.isFrozen(20):
	break
test.log(peakMemory)
JavaScript
var ctx = currentApplicationContext();
var peakMemory = 0;
while (ctx.isRunning) {
    peakMemory = Math.max(ctx.usedMemory, peakMemory);
    if (!ctx.isFrozen(20))
	break;
}
test.log(peakMemory);
Perl
my $ctx = currentApplicationContext();
my $peakMemory = 0;
while ($ctx->isRunning) {
    if ($ctx->usedMemory > $peakMemory) {
	$peakMemory = $ctx->usedMemory;
    }
    if (!$ctx->isFrozen(20)) {
	last;
    }
}
test::log($peakMemory)
Tcl
set ctx [currentApplicationContext]
set peakMemory 0
while {[applicationContext $ctx isRunning] == 1} {
    if {[applicationContext $ctx usedMemory] > $peakMemory} {
	set peakMemory [applicationContext $ctx usedMemory]
    }
    if {![applicationContext $ctx isFrozen 20]} {
	break
    }
}
test log $peakMemory

Here we access the currently running AUT and keep track of the maximum amount of memory it is using. 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 = currentApplicationContext()
test.log("STDOUT", ctx.readStdout())
test.warning("STDERR", ctx.readStderr())
JavaScript
var ctx = currentApplicationContext();
test.log("STDOUT", ctx.readStdout());
test.warning("STDERR", ctx.readStderr());
Perl
my $ctx = currentApplicationContext();
test::log("STDOUT", $ctx->readStdout());
test::warning("STDERR", $ctx->readStderr());
Tcl
set ctx [currentApplicationContext]
test log "STDOUT" [applicationContext $ctx readStdout]
test warning "STDERR" [applicationContext $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 involve 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 Spy tool (see How to Use the Spy (Section 15.2.3); and see also the waitForObject function.)

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 all of the scripting languages the Squish supports.

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]Registering Example AUTs

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 squishserver. The test suite will know which application you are using, but the squishserver will not have an entry for it, until it is registered. The easiest way to accomplish this is to use the "Manage AUTs" dialog (Section 17.3.4). Note that when recording tests for your own applications the registration is done automatically, so this is only relevant for testing the example AUTs supplied with Squish.

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 Spy tool (see How to Use the Spy (Section 15.2.3)) and adding the object to the Object Map (Section 16.10) (so that Squish will remember it) and then copying the object's name (preferably its symbolic name) to the clipboard ready to be pasted into our test.

15.1.11.1. How to Test Widget States and Properties

Each Qt widget has a set of properties and states associated with it that we can query with Squish to perform checks in our test scripts. These properties can be things like, focus (does the widget have the keyboard 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 Nokia/Qt web site. Just pick the version of Qt you are running (for example: Qt 4.7), and search for the Qt class of the object whose properties you want to verify.

For example, lets imagine we have a button in our 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 click it. You will see that this widget has only 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 sections.

Reading the toolkit's documentation is useful for seeing what properties a widget has and for learning about them. However, if we use the Squish Spy we can see all of the AUT's objects and for the selected object all of its properties and their values. Since most properties have sensible names this is often sufficient to see what properties a particular widget has and which of them we wish to verify. (For details see the Squish Spy Perspective (Section 17.1.2.1) and the views it cross-references.)

15.1.11.2. 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, and so on.

We will begin by reviewing the test script for testing the form's "cash" mode. The code is all in one single large main function. (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.) We will show the function in pieces, with each piece followed by an explanation.

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.verify(cashRadioButton.checked)
    
    # 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)

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.verify(cashRadioButton.checked);
    
    // 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);

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);

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 verify [property get $cashRadioButton checked]
    
    # 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

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 name, then we call waitForObject to get a reference to the widget. Generally it is best to use symbolic names, but multi-property (real) names make sense for widgets that have been uniquely named with the QObject::setObjectName function, and are also useful when we need to do wildcard matching.

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.

Python
    # 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)

JavaScript
    // 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);

Perl
    # 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);

Tcl
    # 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

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.

Python
    # 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)

JavaScript
    // 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);

Perl
    # 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);

Tcl
    # 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

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 this is more convenient.)

Python
<xi:include></xi:include>
JavaScript
    // 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.verify(payButton.enabled);
}

Perl
    # 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
    # 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 verify [property get $payButton enabled]
}

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.1. 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.verify(radioButton.checked)
    

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.verify(radioButton.checked);
}  


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::verify($radioButton->checked);
}

    
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 verify [property get $radioButton checked]
}
    
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.2. 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.verify(payButton.enabled)

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.verify(payButton.enabled);
}

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 verify [property get $payButton enabled]
}



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). These supporting functions are shown below, each followed by a brief explanation.

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)

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);
}

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);
}

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]
}

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

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

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

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

Tcl
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 verify [property get $checkSignedCheckBox checked]
}

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

Python
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 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 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 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 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 want 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 the QObject::setObjectName method in the AUT's source code to give them unique names if we wanted to.

Example 15.3. 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.verify(checkSignedCheckBox.checked)

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.verify(checkSignedCheckBox.checked);
}

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::verify($checkSignedCheckBox->checked);
}

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 verify [property get $checkSignedCheckBox checked]
}

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"
}



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.4. 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.verify(payButton.enabled)

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.verify(payButton.enabled);
}

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 verify [property get $payButton enabled]
}



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.5. 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.3. 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.3.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.6. 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.3.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.7. 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 the QAbstractItemModel.setData method for each item whose value we want to set, giving an appropriate model index, role, and value.

15.1.11.3.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.8. 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.9. 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.4).

15.1.11.3.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.10. 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.3.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.11. 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 the checkAnItem function 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 the checkAnItem function.

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.3.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.12. 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 the checkAnItem function.

15.1.11.4. 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. [12] 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.13. 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.14. 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.9) 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.15. 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.16. 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.5. 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.6. 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.7. 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:

Python
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 that the first item has no siblings, so the QListViewItem::nextSibling method returns 0. For example:

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 an item's text, we use the QListViewItem::text function, passing it the number of the column whose text we want.

Another way of retrieving an item is to use the QListView::findItem function. This can be used to check whether an item is present or if we want to initiate searching from a particular point in the tree rather than searching from the beginning. For example, to see if there's an item with the text "Orange", we would write:

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

The second argument given to the QListView::findItem function specifies which column should be searched.

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:

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

Since the item is of type QListViewItem, we must cast it to a QCheckListItem using the cast function. If the cast fails, 0 is returned. Then we can call the QCheckListItem::state function on the item and test whether this returns QCheckListItem::On; if it does, then the item is checked.

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

15.1.11.9. How to Test QTable (Qt 3)

A QTable consists of items which can be retrieved using the QTable::item function.

To test whether the cell in row 5, column 4 (using 0-based indexing) contains the text "Kiwi", the following code can be used:

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

Similarly to a QListView, it is also possible to cast the cell items to more sophisticated items, for example, QCheckTableItem—providing the table uses such items, and to query the properties of these items.

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

Squish for 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 for 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 Additional Verification Points (Section 5.3) in the Tutorial: Starting to Test Tk Applications (Chapter 12), 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.10) 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.3)). 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 Squish Spy Perspective (Section 17.1.2.1)'s Application Objects view (Section 17.2.1) 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:

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

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

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

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

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

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

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

Tcl
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.10) 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.3)). 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 Squish Spy Perspective (Section 17.1.2.1)'s Application Objects view (Section 17.2.1) 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.verify(radiobutton.checked)
JavaScript
var radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}");
test.verify(radiobutton.checked);
Perl
my $radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " .
        "form='myform' type='radio' value='Radio 1'}");
test::verify($radiobutton->checked);
Tcl
set radiobutton [waitForObject ":{tagName='INPUT' id='r1' name='rg' \
        form='myform' type='radio' value='Radio 1'}"]
test verify [property get $radiobutton checked]

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

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

In this example, we retrieve a reference to a mulitple selection list—normally represented by a listbox—and then retrieve its option items. We then verify that the first and third options are selected, and that the second is not selected. We also verify the second option's text.

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

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.10.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 for Web now includes support for testing Java applets embedded in the web browser. This works because Squish for Web includes the necessary functionality from the Squish for 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 for Web will handle all the web page interaction, and under the hood the functionality from the Squish for 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.5) 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.3) 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.3).

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 Squish Spy Perspective (Section 17.1.2.1)'s Application Objects view (Section 17.2.1) 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.17. 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.9) 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.18. 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.19. 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 verify [property get $payButton enabled]
}



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.

Here are the supporting functions, each followed by a very brief explanation.

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")
    AnyClass = calendar.getClass()
    SimpleDateFormatClass = AnyClass.forName("java.text.SimpleDateFormat")
    formatter = SimpleDateFormatClass.newInstance()
    formatter.applyPattern("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) == formatter.format(earliest))
    test.verify(formatter.format(model.getEnd()) == formatter.format(latest))

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");
    var AnyClass = calendar.getClass();
    var SimpleDateFormatClass = AnyClass.forName("java.text.SimpleDateFormat");
    var formatter = SimpleDateFormatClass.newInstance();
    formatter.applyPattern("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
		formatter.format(earliest));
    test.verify(formatter.format(model.getEnd()) ==
		formatter.format(latest));
}

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");
    my $AnyClass = $calendar->getClass();
    my $SimpleDateFormatClass = $AnyClass->forName("java.text.SimpleDateFormat");
    my $formatter = $SimpleDateFormatClass->newInstance();
    $formatter->applyPattern("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq $formatter->format($earliest));
    test::verify($formatter->format($model->getEnd()) eq $formatter->format($latest));
}

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 AnyClass [invoke $checkDateSpinner getClass]
    set SimpleDateFormatClass [invoke $AnyClass forName "java.text.SimpleDateFormat"]
    set formatter [invoke $SimpleDateFormatClass newInstance]
    invoke $formatter applyPattern "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
}

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.7) for details, including a way to access such classes without even having to register them or use a .ini file. 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"

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

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

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

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

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.

Python
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 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 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 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"
}

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.20. 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;
    $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 verify [property get $payButton enabled]
}



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.

Here are the supporting functions, each with some brief discussion of their use.

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")
    AnyClass = calendar.getClass()
    SimpleDateFormatClass = AnyClass.forName("java.text.SimpleDateFormat")
    formatter = SimpleDateFormatClass.newInstance()
    formatter.applyPattern("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())

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");
    var AnyClass = calendar.getClass();
    var SimpleDateFormatClass = AnyClass.forName("java.text.SimpleDateFormat");
    var formatter = SimpleDateFormatClass.newInstance();
    formatter.applyPattern("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());
}

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);
            last;
	}
    }
    # (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");
    my $AnyClass = $calendar->getClass();
    my $SimpleDateFormatClass = $AnyClass->forName("java.text.SimpleDateFormat");
    my $formatter = $SimpleDateFormatClass->newInstance();
    $formatter->applyPattern("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());
}

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 AnyClass [invoke $calendar getClass]
    set SimpleDateFormatClass [invoke $AnyClass forName "java.text.SimpleDateFormat"]
    set formatter [invoke $SimpleDateFormatClass newInstance]
    invoke $formatter applyPattern "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]
}

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.

Python
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 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 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 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"
}

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.21. 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.22. 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.23. 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. [12] 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.24. 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.25. 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.26. 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.27. The tst_cash_mode Test Script

Python
def main():
    startApplication("paymentform_swt")
    # 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("paymentform_swt");
    // 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("paymentform_swt");
    # 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 "paymentform_swt"
    # 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.3) 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.9) 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.28. 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.29. The tst_check_mode Test Script's main function

Python
def main():
    startApplication("paymentform_swt")
    # 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("paymentform_swt");
    // 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("paymentform_swt");
    # 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 "paymentform_swt"
    # 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
    # isn't yet valid), so we use findObject() without waiting
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [findObject $payButtonName]
    test compare [invoke $payButton isEnabled] 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
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}



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, except that this time we use the common checkPaymentRange function. The second rule shows how we can test the properties of a DateTime using the common dateTimeEqualsDate function.

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 test we need a class that is not available by default, java.util.Calendar. 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.7) 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 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 ensure and confirm that the check is signed—functionality that we have wholly encapsulated in the ensureSignedCheckBoxIsChecked function. For the fifth rule we populate the account line edits with fake data using the populateCheckFields function. 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.30. The tst_check_mode Test Script's other functions

Python
def ensureSignedCheckBoxIsChecked():
    checkSignedButton = waitForObject(":Check.Check Signed_Button")
    if not checkSignedButton.getSelection():
        clickButton(checkSignedButton)
    snooze(1)
    test.verify(checkSignedButton.getSelection())

def populateCheckFields():
    bankNameText = waitForObject(":Check.Bank Name:_Text")
    type(bankNameText, "A Bank")
    bankNumberText = waitForObject(":Check.Bank Number:_Text")
    type(bankNumberText, "88-91-33X")
    accountNameText = waitForObject(":Check.Account Name:_Text")
    type(accountNameText, "An Account")
    accountNumberText = waitForObject(":Check.Account Number:_Text")
    type(accountNumberText, "932745395")

JavaScript
function ensureSignedCheckBoxIsChecked()
{
    var checkSignedButton = waitForObject(":Check.Check Signed_Button");
    if (!checkSignedButton.getSelection()) {
        clickButton(checkSignedButton);
    }
    snooze(1);
    test.verify(checkSignedButton.getSelection());
}

function populateCheckFields()
{
    var bankNameText = waitForObject(":Check.Bank Name:_Text");
    type(bankNameText, "A Bank");
    var bankNumberText = waitForObject(":Check.Bank Number:_Text");
    type(bankNumberText, "88-91-33X");
    var accountNameText = waitForObject(":Check.Account Name:_Text");
    type(accountNameText, "An Account");
    var accountNumberText = waitForObject(":Check.Account Number:_Text");
    type(accountNumberText, "932745395");
}

Perl
sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedButton = waitForObject(":Check.Check Signed_Button");
    if (!$checkSignedButton->getSelection()) {
        clickButton($checkSignedButton);
    }
    test::verify($checkSignedButton->getSelection());
}

sub populateCheckFields
{
    my $bankNameText = waitForObject(":Check.Bank Name:_Text");
    type($bankNameText, "A Bank");
    my $bankNumberText = waitForObject(":Check.Bank Number:_Text");
    type($bankNumberText, "88-91-33X");
    my $accountNameText = waitForObject(":Check.Account Name:_Text");
    type($accountNameText, "An Account");
    my $accountNumberText = waitForObject(":Check.Account Number:_Text");
    type($accountNumberText, "932745395");
}

Tcl
proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedButton [waitForObject ":Check.Check Signed_Button"]
    if {![invoke $checkSignedButton getSelection]} {
        invoke clickButton $checkSignedButton
    }
    snooze 1
    test verify [invoke $checkSignedButton getSelection]
}

proc populateCheckFields {} {
    set bankNameText [waitForObject ":Check.Bank Name:_Text"]
    invoke type $bankNameText "A Bank"
    set bankNumberText [waitForObject ":Check.Bank Number:_Text"]
    invoke type $bankNumberText "88-91-33X"
    set accountNameText [waitForObject ":Check.Account Name:_Text"]
    invoke type $accountNameText "An Account"
    set accountNumberText [waitForObject ":Check.Account Number:_Text"]
    invoke type $accountNumberText "932745395"
}



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. Just as with "check" mode we have refactored the main function to keep it short and understandable, relying on common functions and test-specific functions in the test.py (or test.js etc.) file to encapsulate particular tests.

Example 15.31. The tst_card_mode Test Script's main function

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

    # Start with the correct tab
    clickTabItem("&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))

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    cardTypeCombo = waitForObject(":Card.Card Type:_Combo")
    for index in range(cardTypeCombo.getItemCount()):
        if cardTypeCombo.getItem(index) != "Master":
            cardTypeCombo.select(index)
            break
    
    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails()

    # 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_org.eclipse.swt.widgets.Button"
    payButton = findObject(payButtonName)
    test.verify(not payButton.isEnabled())
    
    # 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 = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())

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

    // Start with the correct tab
    clickTabItem("&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));

    // Make sure the card type is not Master, i.e., not the default, just
    // to show how to manipulate a Combo.
    var cardTypeCombo = waitForObject(":Card.Card Type:_Combo");
    for (var index = 0; index < cardTypeCombo.getItemCount(); ++index) {
        if (cardTypeCombo.getItem(index) != "Master") {
            cardTypeCombo.select(index);
            break;
	}
    }
    
    // Business rules #2 and #3: the issue date must be no
    // earlier than 3 years ago, and must start out at that date, and the
    // expiry date must be at least a month later than today.
    // All we check here is that both DateTimes are set to their earliest
    // valid date.
    checkCardDateDetails();

    // 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_org.eclipse.swt.widgets.Button";
    var payButton = findObject(payButtonName);
    test.verify(!payButton.isEnabled());
    
    // 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.isEnabled());
}

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

    # Start with the correct tab
    clickTabItem("&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();
    checkPaymentRange($amount_due / 20.0 > 10 ? $amount_due / 20.0 : 10,
                      $amount_due < 5000 ? $amount_due : 5000);

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    my $cardTypeCombo = waitForObject(":Card.Card Type:_Combo");
    for (my $index = 0; $index < $cardTypeCombo->getItemCount(); ++$index) {
        if ($cardTypeCombo->getItem($index) ne "Master") {
            $cardTypeCombo->select($index);
            break;
	}
    }
    
    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails();

    # 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_org.eclipse.swt.widgets.Button";
    my $payButton = findObject($payButtonName);
    test::verify(!$payButton->isEnabled());
    
    # 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 = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}

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

    # Start with the correct tab
    clickTabItem "&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.0]
    set minimum [expr 10 > $five_percent ? 10 : $five_percent]
    set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
    checkPaymentRange $minimum $maximum

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    set cardTypeCombo [waitForObject ":Card.Card Type:_Combo"]
    set count [invoke $cardTypeCombo getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        if {[invoke $cardTypeCombo getItem $index] != "Master"} {
            invoke $cardTypeCombo select $index
            break
        }
    }
    
    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails

    # 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_org.eclipse.swt.widgets.Button"
    set payButton [findObject $payButtonName]
    test compare [invoke $payButton isEnabled] 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
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}



We start by setting the correct tab and then for the first business rule we check the payment range—this is the same as we did for the other modes.

Although it isn't necessary for testing the business rules, we change the item in the Combo widget just to show how it is done.

The second and third business rules are a bit involved so we encapsulate them in the test-specific checkCardDateDetails function.

Initially the Pay button should be disabled, so business rule four checks for this. For the fifth business rule, we provide some fake data for the card account name and number using the populateCardFields function, and since the dates are in range the Pay button should now be enabled as our last check verifies.

Example 15.32. The tst_card_mode Test Script's other functions

Python
def checkCardDateDetails():
    tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    tabFolder = waitForObject(tabFolderName)
    for index in range(tabFolder.getItemCount()):
        tabItem = tabFolder.getItem(index)
        if tabItem.getText() == "&Card":
            break
    dateTimes = []
    control = tabItem.getControl()
    children = control.getChildren()
    for index in range(children.length):
        child = children.at(index)
        if child.getClass().toString() == "class org.eclipse.swt.widgets.DateTime":
            dateTimes.append(child)
    test.verify(len(dateTimes) == 2)

    earliestIssueDate = java_util_Calendar.getInstance()
    earliestIssueDate.add(java_util_Calendar.YEAR, -3)
    test.verify(dateTimeEqualsDate(dateTimes[0], earliestIssueDate))
    earliestExpiryDate = java_util_Calendar.getInstance()
    earliestExpiryDate.add(java_util_Calendar.MONTH, 1)
    test.verify(dateTimeEqualsDate(dateTimes[1], earliestExpiryDate))

def populateCardFields():
    cardAccountNameText = waitForObject(":Card.Account Name:_Text")
    mouseClick(cardAccountNameText, 10, 10, 0, Button.Button1)
    type(cardAccountNameText, "An Account")
    cardAccountNumberText = waitForObject(":Card.Account Number:_Text")
    type(cardAccountNumberText, "1343 876 326 1323 32")

JavaScript
function checkCardDateDetails()
{
    var tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
    var tabFolder = waitForObject(tabFolderName);
    for (var index = 0; index < tabFolder.getItemCount(); ++index) {
        var tabItem = tabFolder.getItem(index);
        if (tabItem.getText() == "&Card") {
            break;
	}
    }
    var dateTimes = [];
    var control = tabItem.getControl();
    var children = control.getChildren();
    for (var index = 0; index <  children.length; ++index) {
        var child = children.at(index);
        if (child.getClass().toString() == "class org.eclipse.swt.widgets.DateTime") {
            dateTimes.push(child);
	}
    }
    test.verify(dateTimes.length == 2);

    var earliestIssueDate = java_util_Calendar.getInstance();
    earliestIssueDate.add(java_util_Calendar.YEAR, -3);
    test.verify(dateTimeEqualsDate(dateTimes[0], earliestIssueDate));
    var earliestExpiryDate = java_util_Calendar.getInstance();
    earliestExpiryDate.add(java_util_Calendar.MONTH, 1);
    test.verify(dateTimeEqualsDate(dateTimes[1], earliestExpiryDate));
}

function populateCardFields()
{
    var cardAccountNameText = waitForObject(":Card.Account Name:_Text");
    mouseClick(cardAccountNameText, 10, 10, 0, Button.Button1);
    type(cardAccountNameText, "An Account");
    var cardAccountNumberText = waitForObject(":Card.Account Number:_Text");
    type(cardAccountNumberText, "1343 876 326 1323 32");
}

Perl
sub checkCardDateDetails
{
    my $tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
    my $tabFolder = waitForObject($tabFolderName);
    my $tabItem;
    for (my $index = 0; $index < $tabFolder->getItemCount(); ++$index) {
        $tabItem = $tabFolder->getItem($index);
        if ($tabItem->getText() eq "&Card") {
            break;
	}
    }
    my @dateTimes = ();
    my $control = $tabItem->getControl();
    my $children = $control->getChildren();
    for (my $index = 0; $index < $children->length(); ++$index) {
        my $child = $children->at($index);
        if ($child->getClass()->toString() eq
	    "class org.eclipse.swt.widgets.DateTime") {
            push @dateTimes, $child;
	}
    }
    test::verify(@dateTimes == 2);

    my $earliestIssueDate = java_util_Calendar::getInstance();
    $earliestIssueDate->add(java_util_Calendar::YEAR, -3);
    test::verify(dateTimeEqualsDate($dateTimes[0], $earliestIssueDate));
    my $earliestExpiryDate = java_util_Calendar::getInstance();
    $earliestExpiryDate->add(java_util_Calendar::MONTH, 1);
    test::verify(dateTimeEqualsDate($dateTimes[1], $earliestExpiryDate));
}

sub populateCardFields
{
    my $cardAccountNameText = waitForObject(":Card.Account Name:_Text");
    mouseClick($cardAccountNameText, 10, 10, 0, Button::Button1);
    type($cardAccountNameText, "An Account");
    my $cardAccountNumberText = waitForObject(":Card.Account Number:_Text");
    type($cardAccountNumberText, "1343 876 326 1323 32");
}

Tcl
proc checkCardDateDetails {} {
    set tabFolderName ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    set tabFolder [waitForObject $tabFolderName]
    set count [invoke $tabFolder getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        set tabItem [invoke $tabFolder getItem $index]
        if {[invoke $tabItem getText] == "&Card"} {
            break
        }
    }
    set dateTimes {}
    set control [invoke $tabItem getControl]
    set children [invoke $control getChildren]
    set count [property get $children length]
    for {set index 0} {$index < $count} {incr index} {
        set child [invoke $children at $index]
        set className [invoke [invoke $child getClass] toString]
        if {$className == "class org.eclipse.swt.widgets.DateTime"} {
            lappend dateTimes $child
        }
    }
    test compare [llength $dateTimes] 2

    set earliestIssueDate [invoke java_util_Calendar getInstance]
    invoke $earliestIssueDate add [property get java_util_Calendar YEAR] -3
    set issueDateTime [lindex $dateTimes 0]
    test verify [dateTimeEqualsDate $issueDateTime $earliestIssueDate]
    set earliestExpiryDate [invoke java_util_Calendar getInstance]
    invoke $earliestExpiryDate add [property get java_util_Calendar MONTH] 1
    set expiryDateTime [lindex $dateTimes 1]
    test verify [dateTimeEqualsDate $expiryDateTime $earliestExpiryDate]
}

proc populateCardFields {} {
    set cardAccountNameText [waitForObject ":Card.Account Name:_Text"]
    invoke mouseClick $cardAccountNameText 10 10 0 [enum Button Button1]
    invoke type $cardAccountNameText "An Account"
    set cardAccountNumberText [waitForObject ":Card.Account Number:_Text"]
    invoke type $cardAccountNumberText "1343 876 326 1323 32"
}



In the checkCardDateDetails function, for the issue date and expiry date DateTime widgets we want to check that they are both set to their earliest valid date. Rather than identifying them by name we have used introspection to find them. We begin by getting a reference to the TabFolder. Then we use the tab folder's API to iterate over each TabItem until we get the one we want. We then retrieve the tab item's array of child widgets—such arrays have a length property and an at method and we use these to iterate over every child item and to keep a reference to the two DateTime items.

Once we have the date time widgets we check that there are exactly two of them and that they are both set to the expected dates.

For the populateCardFields function we use the type function to enter the fake data.

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

15.1.16.3.2. How to Test List, Table, and Tree widgets (Java™/SWT)

In this section we will see how to iterate over every item in Java™ SWT's List, Table, and Tree widgets, and how to retrieve information from each item, such as their text and checked and selected statuses.

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.

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

15.1.16.3.2.1. How to Test List

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

Example 15.33. The tst_list Test Script

Python
def main():
    startApplication("itemviews_swt")
    listWidgetName = ":Item Views_org.eclipse.swt.widgets.List"
    listWidget = waitForObject(listWidgetName)
    for row in range(listWidget.getItemCount()):
        text = listWidget.getItem(row)
        selected = ""
        if listWidget.isSelected(row):
            selected = " +selected"
        test.log("(%d) '%s'%s" % (row, text, selected))

JavaScript
function main()
{
    startApplication("itemviews_swt");
    var listWidgetName = ":Item Views_org.eclipse.swt.widgets.List";
    var listWidget = waitForObject(listWidgetName);
    for (var row = 0; row < listWidget.getItemCount(); ++row) {
        var text = listWidget.getItem(row);
        var selected = "";
        if (listWidget.isSelected(row)) {
            selected = " +selected";
        }
        test.log("(" + String(row) + ") '" + text + "'" + selected);
    }
}

Perl
sub main
{
    startApplication("itemviews_swt");
    my $listWidgetName = ":Item Views_org.eclipse.swt.widgets.List";
    my $listWidget = waitForObject($listWidgetName);
    for (my $row = 0; $row < $listWidget->getItemCount(); ++$row) {
        my $text = $listWidget->getItem($row);
        my $selected = "";
        if ($listWidget->isSelected($row)) {
            $selected = " +selected";
        }
        test::log("($row) '$text'$selected");
    }
}

Tcl
proc main {} {
    startApplication "itemviews_swt"
    set listWidgetName ":Item Views_org.eclipse.swt.widgets.List"
    set listWidget [waitForObject $listWidgetName]
    for {set row 0} {$row < [invoke $listWidget getItemCount]} {incr row} {
        set text [invoke $listWidget getItem $row]
        set selected ""
        if {[invoke $listWidget isSelected $row]} {
            set selected " +selected"
        }
        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.3.2.2. How to Test Table

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

Example 15.34. The tst_table Test Script

Python
def main():
    startApplication("itemviews_swt")
    tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table"
    tableWidget = waitForObject(tableWidgetName)
    for row in range(tableWidget.getItemCount()):
        item = tableWidget.getItem(row)
        checked = ""
        if item.getChecked():
            checked = " +checked"
        selected = ""
        if tableWidget.isSelected(row):
            selected = " +selected"
        texts = []
        for column in range(tableWidget.getColumnCount()):
            texts.append(item.getText(column))
        test.log("(%d, 0) '%s'%s%s" % (row, "|".join(texts), checked, selected))

JavaScript
function main()
{
    startApplication("itemviews_swt");
    var tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table";
    var tableWidget = waitForObject(tableWidgetName);
    for (var row = 0; row < tableWidget.getItemCount(); ++row) {
        var item = tableWidget.getItem(row);
        var checked = "";
        if (item.getChecked()) {
            checked = " +checked";
        }
        var selected = "";
        if (tableWidget.isSelected(row)) {
            selected = " +selected";
        }
        var text = "";
        for (var column = 0; column < tableWidget.getColumnCount(); ++column) {
            if (text != "") {
                text += "|";
            }
            text += item.getText(column);
        }
        test.log("(" + String(row) + ", 0) '" + text + "'" + checked + selected);
    }
}

Perl
sub main
{
    startApplication("itemviews_swt");
    my $tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table";
    my $tableWidget = waitForObject($tableWidgetName);
    for (my $row = 0; $row < $tableWidget->getItemCount(); ++$row) {
        my $item = $tableWidget->getItem($row);
        my $checked = "";
        if ($item->getChecked()) {
            $checked = " +checked";
        }
        my $selected = "";
        if ($tableWidget->isSelected($row)) {
            $selected = " +selected";
        }
        my @texts = ();
        for (my $column = 0; $column < $tableWidget->getColumnCount(); ++$column) {
            push @texts, $item->getText($column);
        }
        $text = join("|", @texts);
        test::log("($row, 0) '$text'$checked$selected");
    }
}

Tcl
proc main {} {
    startApplication "itemviews_swt"
    set tableWidgetName ":Item Views_org.eclipse.swt.widgets.Table"
    set tableWidget [waitForObject $tableWidgetName]
    for {set row 0} {$row < [invoke $tableWidget getItemCount]} {incr row} {
        set item [invoke $tableWidget getItem $row]
        set checked ""
        if {[invoke $item getChecked]} {
            set checked " +checked"
        }
        set selected ""
        if {[invoke $tableWidget isSelected $row]} {
            set selected " +selected"
        }
        set text {}
        for {set column 0} {$column < [invoke $tableWidget getColumnCount]} {incr column} {
            lappend text [invoke $item getText $column]
        }
        set text [join $text "|"]
        test log "($row, 0) '$text'$checked$selected"
    }
}



In this example we only put texts in each TableItem's first column which is why we hard-coded column 0 in the output. Nonetheless the code shows how to access the texts from all the columns, so the code should be easy to adapt.

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.3.2.3. How to Test Tree

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

Example 15.35. The tst_tree Test Script

Python
def checkAnItem(indent, item, selection):
    if indent > -1:
        checked = selected = ""
        if item.getChecked():
            checked = " +checked"
        for i in range(selection.length):
            if selection.at(i) == item:
                selected = " +selected"
                break
        test.log("|%s'%s'%s%s" % (" " * indent, item.getText(), checked, selected))
    else:
        indent = -4
    for row in range(item.getItemCount()):
        child = item.getItem(row)
        checkAnItem(indent + 4, child, selection)
        
def main():
    startApplication("itemviews_swt")
    treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree"
    treeWidget = waitForObject(treeWidgetName)
    checkAnItem(-1, treeWidget, treeWidget.getSelection())

JavaScript
function checkAnItem(indent, item, selection)
{
    if (indent > -1) {
        var checked = "";
        var selected = "";
        if (item.getChecked()) {
            checked = " +checked";
        }
        for (var i = 0; i < selection.length; ++i) {
            if (selection.at(i) == item) {
                selected = " +selected";
                break;
            }
        }
        var offset = "";
        for (var i = 0; i < indent; ++i) {
            offset = offset.concat(" ");
        }
        test.log("|" + offset + "'" + item.getText() + "'" + checked + selected);
    }
    else {
        indent = -4;
    }
    for (var row = 0; row < item.getItemCount(); ++row) {
        var child = item.getItem(row);
        checkAnItem(indent + 4, child, selection);
    }
}
        
function main()
{
    startApplication("itemviews_swt");
    var treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree";
    var treeWidget = waitForObject(treeWidgetName);
    checkAnItem(-1, treeWidget, treeWidget.getSelection());
}

Perl
sub checkAnItem
{
    my ($indent, $item, $selection) = @_;
    if ($indent > -1) {
        my $checked = "";
        my $selected = "";
        if ($item->getChecked()) {
            $checked = " +checked";
        }
        my $padding = " " x $indent;
        for (my $i = 0; $i < $selection->length; ++$i) {
            if ($selection->at($i) eq $item) {
                $selected = " +selected";
                last;
            }
        }
        test::log("|$padding'" . $item->getText() . "'$checked$selected");
    }
    else {
        $indent = -4;
    }
    for (my $row = 0; $row < $item->getItemCount(); ++$row) {
        my $child = $item->getItem($row);
        checkAnItem($indent + 4, $child, $selection);
    }
}
        
sub main()
{
    startApplication("itemviews_swt");
    my $treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree";
    my $treeWidget = waitForObject($treeWidgetName);
    checkAnItem(-1, $treeWidget, $treeWidget->getSelection());
}

Tcl
proc checkAnItem {indent item selection} {
    if {$indent > -1} {
        set checked ""
        set selected ""
        if {[invoke $item getChecked]} {
            set checked " +checked"
        }
        set offset [string repeat " " $indent]
        for {set i 0} {$i < [property get $selection length]} {incr i} {
            if {[invoke [invoke $selection at $i] toString] == [invoke $item toString]} {
                set selected " +selected"
                break
            }
        }
        set text [invoke $item getText]
        test log "|$offset'$text'$checked$selected"
    } else {
        set indent -4
    }
    for {set row 0} {$row < [invoke $item getItemCount]} {incr row} {
        set child [invoke $item getItem $row]
        set offset [expr $indent + 4]
        checkAnItem $offset $child $selection
    }
}
        
proc main {} {
    startApplication "itemviews_swt"
    set treeWidgetName ":Item Views_org.eclipse.swt.widgets.Tree"
    set treeWidget [waitForObject $treeWidgetName]
    checkAnItem -1 $treeWidget [invoke $treeWidget getSelection]
}



The key difference from List and Table is that since Trees are recursive it is easiest if we ourselves use recursion to iterate over all the items. Unfortunately, the Java/SWT Tree's API does not allow us to ask if a particular item is selected, so for each item we must iterate over the array of selected items returned by the Tree.getSelection method. To do this we pass the array to the checkAnItem function in the selection parameter. Then we use the array's length property to see how many items we can iterate over, and its at method to retrieve each item in turn. If we find a matching item we know that it is selected so we add this information to the text we print. (See also, How to Create and Access Java™ Arrays (Section 15.1.6.5).)

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.3.3. How to Test the Table Widget and Use External Data Files (Java/SWT)

In this section we will see how to test the CsvTableSWT.java program shown below. This program uses a Table 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. [12] As we review the tests we will learn how to import test data, manipulate the data, and compare what the Table 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_swt, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHROOT/examples/java/csvtable_swt/suite_py, and the JavaScript version of the tests is in SQUISHROOT/examples/java/csvtable_swt/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.36. The tst_loading Test Script

Python
def main():
    startApplication("csvtable_swt")
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename, "")
    table = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Table'}")
    compareTableWithDataFile(table, filename)
JavaScript
function main()
{
    startApplication("csvtable_swt");
    source(findFile("scripts", "common.js"));
    var filename = "before.csv";
    doFileOpen(filename, "");
    var table = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Table'}");
    compareTableWithDataFile(table, filename);
}

Perl
sub main
{
    startApplication("csvtable_swt");
    source(findFile("scripts", "common.pl"));
    my $filename = "before.csv";
    doFileOpen($filename);
    my $table = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Table'}");
    compareTableWithDataFile($table, $filename);
}

Tcl
proc main {} {
    startApplication "csvtable_swt"
    source [findFile "scripts" "common.tcl"]
    set filename "before.csv"
    doFileOpen $filename ""
    set table [waitForObject "{isvisible='true' type='org.eclipse.swt.widgets.Table'}"]
    compareTableWithDataFile $table $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. (The first argument is the name of the file to open and the second argument is the name of the current file, which in this case is an empty string since we are starting the AUT from scratch.) Next we get a reference to the Table using the waitForObject function, and finally we check that the Table'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.37. Extracts from the Shared Scripts

Python
def doFileOpen(fileToOpen, currentFile):
    invokeMenuItem("File", "Open...", currentFile)
    waitForObject(":SWT")
    chooseFile(":SWT", fileToOpen)
    
def invokeMenuItem(menu, item, filename):
    if filename:
        filename = " - " + filename
    menuName = ("{caption='%s' container=':CSV Table%s_org.eclipse.swt.widgets.Menu' "
                "type='org.eclipse.swt.widgets.MenuItem'}" % (menu, filename))
    waitForObject(menuName)
    activateItem(menuName)
    menuItemName = ("{caption='%s' type='org.eclipse.swt.widgets.MenuItem'}" % item)
    waitForObject(menuItemName)
    activateItem(menuItemName)
    
def compareTableWithDataFile(table, filename):
    for row, record in enumerate(testData.dataset(filename)):
        item = table.getItem(row)
        for column, name in enumerate(testData.fieldNames(record)):
            test.compare(testData.field(record, name), item.getText(column))

JavaScript
function doFileOpen(fileToOpen, currentFile)
{
    invokeMenuItem("File", "Open...", currentFile);
    waitForObject(":SWT");
    chooseFile(":SWT", fileToOpen);
}
    
function invokeMenuItem(menu, item, filename)
{
    if (filename) {
        filename = " - " + filename;
    }
    var menuName = "{caption='" + menu + "'" +
            " container=':CSV Table" + filename +
            "_org.eclipse.swt.widgets.Menu' " +
            "type='org.eclipse.swt.widgets.MenuItem'}";
    waitForObject(menuName);
    activateItem(menuName);
    var menuItemName = "{caption='" + item + "'" +
            " type='org.eclipse.swt.widgets.MenuItem'}";
    waitForObject(menuItemName);
    activateItem(menuItemName);
}
    
function compareTableWithDataFile(table, filename)
{
    var records = testData.dataset(filename);
    for (var row = 0; row < records.length; ++row) {
        var item = table.getItem(row);
        columnNames = testData.fieldNames(records[row]);
        for (var column = 0; column < columnNames.length; ++column) {
            test.compare(testData.field(records[row], column), item.getText(column));
        }
    }
}

Perl
sub doFileOpen
{
    my ($fileToOpen, $currentFile) = @_;
    invokeMenuItem("File", "Open...", $currentFile);
    waitForObject(":SWT");
    chooseFile(":SWT", $fileToOpen);
}
    
sub invokeMenuItem
{
    my ($menu, $item, $filename) = @_;
    if ($filename) {
        $filename = " - " . $filename;
    }
    my $menuName = "{caption='$menu' " .
            "container=':CSV Table${filename}_org.eclipse.swt.widgets.Menu' " .
            "type='org.eclipse.swt.widgets.MenuItem'}";
    waitForObject($menuName);
    activateItem($menuName);
    my $menuItemName = "{caption='$item' type='org.eclipse.swt.widgets.MenuItem'}";
    waitForObject($menuItemName);
    activateItem($menuItemName);
}
    
sub compareTableWithDataFile
{
    my ($table, $filename) = @_;
    my @records = testData::dataset($filename);
    for (my $row = 0; $row < scalar(@records); $row++) {
        my $item = $table->getItem($row);
        my @columnNames = testData::fieldNames($records[$row]);
        for (my $column = 0; $column < scalar(@columnNames); $column++) {
            test::compare(testData::field($records[$row], $column), $item->getText($column));
        }
    }
}

Tcl
proc doFileOpen {fileToOpen currentFile} {
    invokeMenuItem "File" "Open..." $currentFile
    waitForObject ":SWT"
    invoke chooseFile ":SWT" $fileToOpen
}
    
proc invokeMenuItem {menu item filename} {
    if {$filename != ""} {
        set filename " - $filename"
    }
    set menuName "{caption='$menu' container=':CSV Table${filename}_org.eclipse.swt.widgets.Menu' type='org.eclipse.swt.widgets.MenuItem'}"
    waitForObject $menuName
    invoke activateItem $menuName
    set menuItemName "{caption='$item' type='org.eclipse.swt.widgets.MenuItem'}"
    waitForObject $menuItemName
    invoke activateItem $menuItemName
}
    
proc compareTableWithDataFile {table filename} {
    set data [testData dataset $filename]
    for {set row 0} {$row < [llength $data]} {incr row} {
        set item [invoke $table getItem $row]
	set columnNames [testData fieldNames [lindex $data $row]]
	for {set column 0} {$column < [llength $columnNames]} {incr column} {
            test compare [testData field [lindex $data $row] $column] [invoke $item getText $column]
	}
    }
}



The doFileOpen function begins by opening a file through the user interface. This is done by using the custom invokeMenuItem function that we first saw in the tutorial. The file dialog used may not be the same on all platforms but Squish generalises it by providing the Java/SWT-specific chooseFile function.

The invokeMenuItem function in effect 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").

An alternative to invoking menu options is to use toolbar items. For example, the CsvTableSWT example has a toolbar with New, Open, and Save tool items, so for our custom doFileOpen function, we could replace the call to our custom invokeMenuItem function with the lines:

waitForObject(":Open_org.eclipse.swt.widgets.ToolItem");
mouseClick(":Open_org.eclipse.swt.widgets.ToolItem");

These lines wait for the Open tool item to be ready and then click it. The lines will work in Python, JavaScript, and Perl, and it should be easy to see how we could parameterize them by making the Open text the text of whatever tool item we were interested in.

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 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 the text of 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.38. Extracts from the tst_editing Script

Python
#    closeWindow(":CSV Table - Swap Columns_org.eclipse.swt.widgets.Shell")
    # Added by Hand
    source(findFile("scripts", "common.py"))
    table = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Table'}")
    test.verify(table.getColumnCount() == 5)
    test.verify(table.getItemCount() == 12)
    compareTableWithDataFile(table, "after.csv")
    # End of Added by Hand
    waitForObject(":File_org.eclipse.swt.widgets.MenuItem_2")
    activateItem(":File_org.eclipse.swt.widgets.MenuItem_2")
    waitForObject(":Quit_org.eclipse.swt.widgets.MenuItem_2")
    activateItem(":Quit_org.eclipse.swt.widgets.MenuItem_2")
    waitForObject(":SWT")
    closeMessageBox(":SWT", SWT.NO)

JavaScript
    //closeWindow(":CSV Table - Swap Columns_org.eclipse.swt.widgets.Shell");
    // Added by Hand
    source(findFile("scripts", "common.js"));
    var table = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Table'}");
    test.verify(table.getColumnCount() == 5);
    test.verify(table.getItemCount() == 12);
    compareTableWithDataFile(table, "after.csv");
    // End of Added by Hand
    waitForObject(":File_org.eclipse.swt.widgets.MenuItem_2");
    activateItem(":File_org.eclipse.swt.widgets.MenuItem_2");
    waitForObject(":Quit_org.eclipse.swt.widgets.MenuItem");
    activateItem(":Quit_org.eclipse.swt.widgets.MenuItem");
    waitForObject(":SWT");
    closeMessageBox(":SWT", SWT.NO);
}

Perl
    #    closeWindow(":CSV Table - Swap Columns_org.eclipse.swt.widgets.Shell");
    # Added by Hand
    source(findFile("scripts", "common.pl"));
    my $table = waitForObject("{isvisible='true' type='org.eclipse.swt.widgets.Table'}");
    test::verify($table->getColumnCount() == 5);
    test::verify($table->getItemCount() == 12);
    compareTableWithDataFile($table, "after.csv");
    # End of Added by Hand
    waitForObject(":File_org.eclipse.swt.widgets.MenuItem_2");
    activateItem(":File_org.eclipse.swt.widgets.MenuItem_2");
    waitForObject(":Quit_org.eclipse.swt.widgets.MenuItem_2");
    activateItem(":Quit_org.eclipse.swt.widgets.MenuItem_2");
    waitForObject(":SWT");
    closeMessageBox(":SWT", SWT::NO);
}

Tcl
    #    invoke closeWindow ":CSV Table - Swap Columns_org.eclipse.swt.widgets.Shell" 
    # Added by Hand
    source [findFile "scripts" "common.tcl"]
    set table [waitForObject "{isvisible='true' type='org.eclipse.swt.widgets.Table'}"]
    test compare [invoke $table getColumnCount] 5
    test compare [invoke $table getItemCount] 12
    compareTableWithDataFile $table "after.csv"
    # End of Added by Hand
    waitForObject ":File_org.eclipse.swt.widgets.MenuItem_2"
    invoke activateItem ":File_org.eclipse.swt.widgets.MenuItem_2" 
    waitForObject ":Quit_org.eclipse.swt.widgets.MenuItem_2"
    invoke activateItem ":Quit_org.eclipse.swt.widgets.MenuItem_2" 
    waitForObject ":SWT"
    invoke closeMessageBox ":SWT" [enum SWT NO] 
}



Note that it is quite common to need to comment out closeWindow() calls for dialogs that provide a static open method that creates the dialog and calls the dispose function when the user has finished with it (e.g., by clicking OK or Cancel) as is done in CsvTableSWT.java.

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 Table. (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.4. How to Test List and ComboBoxes Using Proxies

Although the different GUI Java™ toolkits that Squish supports have different implementations for list and comboboxes, their items are wrapped by Squish in ItemProxy objects. These objects act as child objects of the list or combobox object. The child objects are called item_0, item_1 and so on.

The ItemProxy objects have the properties text and selected. For the SWT based List and Combo widgets, there is an extra property called control. The AWT/Swing based ones have an extra property called component.

A picture of the spy showing the items of a List with the SWT toolkit.
In the spy you get for each item an ItemProxy derived object.

In SWT, the List and Combo widget have the method getSelectionIndex. Squish generates from this method a property calls selectionindex. So for the SWT toolkit, these two tests are equal:

test.compare(list.selectionindex, 2);
test.verify(list.item_2.selected);

Note however that the second test gives a script error if the list has less then three items. And that the first test only gives one of the selections if the list supports multiple selections. In that case you can only use the generated ItemProxy objects for verification points.

15.1.16.5. How to Test GEF applications

Squish exposes Figures and FigureCanvas from the GEF (Graphical Editing Framework) library. The Figure hierarchy of the FigureCanvas is exposed and can be inspected in the spy. Properties of the Figure items can be tested like this:

item = waitForObject("<name of Figure item>")
test.verify(item.visible);
test.verify(item.enabled);

15.1.17. How to Create Semi-Automatic Tests that Query for User Input

The most common use case for Squish tests is full automation—Squish is used to record a test (or we write a test manually), and then plays the test back and reports the results. But in some situations we might want to create a semi-automatic test that requires the tester to provide some input. For example, when testing some software with a hardware device we might want to ask the user who is running the test if the device's state has been changed in the expected way.

For example, imagine that we want to write tests for some printer-related software. The tests exercise the software, and one of the results should be that the printer prints a page with the text This is a test.

Since Squish can't verify a physical occurrence such as a page coming out of the printer with some specific text on it, any test that has such a requirement must rely on the tester to inform Squish whether the test passed or failed. The following example shows how this can be done.

[Note]Note

The examples in this section happen to use the Qt toolkit, and so it is necessary that a Qt application is set as the AUT for the examples to run successfully. Similarly, if equivalent functions were used with other toolkits—such as a JavaScript alert box or a Tk message box displayed using the evalJS function or the tcleval function—an appropriate AUT that used the relevant toolkit would also have to be set.

Python
def main():
    ...
    if QMessageBox.question(0, "Confirmation Required", 
	    "Did the printer print a page with the contents " +
            "'This is a test'?",
	    "Yes", "No") == 0: # first button is 0
        test.passes("Print succeeded",
            "The test page was printed correctly")
    else:
        test.fail("Print failed",
            "The printer did not print (or printed the wrong text)")
    ...
JavaScript
function main()
{
    // ...
    if (QMessageBox.question(0, "Confirmation Required", 
	    "Did the printer print a page with the contents " +
            "'This is a test'?",
	    "Yes", "No") == 0) // first button is 0
        test.pass("Print succeeded",
            "The test page was printed correctly");
    else
        test.fail("Print failed",
            "The printer did not print (or printed the wrong text)");
    // ...
}
Perl
sub main
{
    # ...
    if (QMessageBox::question(0, "Confirmation Required", 
	    "Did the printer print a page with the contents " .
            "'This is a test'?",
	    "Yes", "No") == 0) { # first button is 0
        test::pass("Print succeeded",
            "The test page was printed correctly");
    }
    else {
        test::fail("Print failed",
            "The printer did not print (or printed the wrong text)");
    }
    # ...
}
Tcl
proc main {} {
    # ...
    set button [invoke QMessageBox question 0 \
        "Confirmation Required" \
        "Did the printer print a page with the contents \
        'This is a test'?" "Yes" "No"]
    if {$button == 0} {
        test pass "Print succeeded" "The test page was printed correctly"
    } else {
        test fail "Print failed" \
            "The printer did not print (or printed the wrong text)"
    }
    # ...
}

The first argument is the parent window the dialog should pop up over—we pass 0 since that is a safe default. Next comes the message box's title, then the body text, and then the button texts.

Here we only asked the user whether or not the page was printed correctly, but QMessageBox allows us to have up to three buttons, so we could have made the test more specific, perhaps with the texts Didn't Print, Printed Wrongly, and Printed Correctly.

We are not limited to using simple button-based dialogs if the underlying GUI toolkit supports more. For example, the Qt toolkit has a QInputDialog class that can be used to get integers or floating-point values, or a string from a predefined list of strings.

Python
def main():
    ...
    pages = QInputDialog.getInteger(0, "Require User Input",
        "How many pages have been printed?")
    if pages == 0:
        test.fail("No pages printed")
    else
        test.passes("%d pages printed" % pages)
    // ...
}
JavaScript
function main()
{
    // ...
    var pages = QInputDialog.getInteger(0, "Require User Input",
        "How many pages have been printed?");
    if (pages == 0)
        test.fail("No pages printed");
    else
        test.pass(pages + " pages printed");
    // ...
}
Perl
sub main
{
    # ...
    my $pages = QInputDialog::getInteger(0, "Require User Input",
        "How many pages have been printed?");
    if ($pages == 0) {
        test::fail("No pages printed");
    }
    else {
        test::pass("$pages pages printed");
    }
    # ...
}
Tcl
proc main {} {
    # ...
    set pages [invoke QInputDialog getInteger 0  "Require User Input" \
        "How many pages have been printed?"]
    if {$pages == 0} {
        test fail "No pages printed"
    } else {
        test pass "$pages pages printed"
    }
    // ...
}

Here we ask the user to enter the number of pages. As with the previous example, the first argument is the parent window—and again we pass 0 as a safe default. The second argument is the dialog's window title, and the third argument the body text.

This section again shows that using Squish's powerful script bindings to classes like QMessageBox and QInputDialog (or to the DOM/HTML, Tk, XView, or other APIs, depending on which Squish edition is being used), can be used in test scripts so that testers can be asked for input when necessary. However, asking for tester input is something to be avoided where possible, since fully automated tests are much faster and more convenient.

15.1.18. How to Create Automatic Screenshots on Test Failures and Errors

To make it easier to track down the causes of test failures and errors, it is possible to tell Squish to take a screenshot whenever a test fails or has errors. We can then view the screenshot of the complete desktop taken at the time the failure or error occurred. This is especially helpful when it comes to the debugging of test failures and errors that occur during unattended automated test runs.

To enable this feature, it is necessary to set the global settings object, testSettings Object (Section 16.1.3.13)'s logScreenshotOnFail and/or logScreenshotOnError properties to true (or True or 1 depending on your scripting language—see Equivalent Script API (Section 16.1.2).).

Once one or both of these properties are activated, all tests that result in failures or errors will produce a message text that contains the path to an image file that contains the saved screenshot. In the Squish IDE the screenshots can be viewed directly by right-clicking on the corresponding test result.

15.1.19. How to Interact With Files and With the Environment in Test Scripts

In addition to the test-specific functionality that Squish provides, test scripts can also use the native functionality (including the standard libraries) provided by the scripting languages themselves. In this subsection we will show how to use native functionality to read data from an external file, write data to an external file, check for the existence of an external file, and delete an external file. In addition, we will see how to compare two external files, and also how to read the environment variables that are set when the script is running.

[Note]Python-specific

The Python examples don't show any import statements, but these are of course required when non-global functions are used. The imports ought to be done before main function is defined, with those shown below being sufficient for the examples shown in this section.

Python
import codecs, filecmp, os, subprocess, sys
[Note]Perl-specific

The Perl examples don't show any use statements, but these are of course required when non-global functions are used. The uses ought to be done before main function is defined, with those shown below being sufficient for the examples shown in this section.

Python
use File::Basename;
use File::Spec;

15.1.19.1. How to Interact with External Files in Test Scripts

Here we will see how to read data from an external file, write data to an external file, check for the existence of an external file, and delete an external file.

15.1.19.1.1. How to Read Data from an External File

Reading an external file involves getting its complete filename (including path), and then reading it in the standard way that the scripting language supports. For example:

Python
    infile = findFile("testdata", "before.csv")
    infile = infile.replace("/", os.sep)
    test.log("Reading %s" % infile)
    file = codecs.open(infile, "r", "utf-8")
    lines = []
    for line in file:
        lines.append(line)
    file.close()
    test.verify(len(lines) == 13)


JavaScript
    infile = findFile("testdata", "before.csv");
    infile = infile.replace(/[\/]/, File.separator);
    test.log("Reading " + infile);
    file = File.open(infile, "r");
    var lines = [];
    var i = 0;
    while (true) {
        var line = file.readln();
        if (line == null)
            break;
        lines[i++] = line;
    }
    file.close();
    test.verify(lines.length == 13);


Perl
    my $sep = File::Spec->rootdir();
    my $infile = findFile("testdata", "before.csv");
    $infile =~ s,/,$sep,g;
    test::log("Reading $infile");
    open(FILE, "<:encoding(UTF-8)", $infile) or test::fail("Failed to read $infile");
    my @lines = <FILE>;
    close(FILE);
    test::verify(scalar(@lines) == 13);


Tcl
    set infile [findFile "testdata" "before.csv"]
    set infile [file normalize $infile]
    test log "Reading $infile"
    set fh [open $infile]
    set text [read $fh]
    close $fh
    set text [string trimright $text]
    set lines [split $text "\n"]
    test compare [llength $lines] 13


Here, we read a file called before.csv that is in the suite's (or the test case's) testdata directory. The file is a text file using the UTF-8 encoding. We open the file and read it line by line into a list (or array) of lines or into a string which we then break into lines, depending on the scripting language. And at the end we check that we have got exactly the number of lines we expected.

Squish uses Unix-style path separators internally on all platforms, but because we want to show the path to the user (using the test.log function), we replace these with the path separator that is appropriate for the platform (e.g., \ on Windows).

JavaScript has no native support for file handling or for operating system interaction, so Squish provides the File Object (Section 16.1.15.3) and the OS Object (Section 16.1.15.4) to fill these gaps.

15.1.19.1.2. How to Write Data to an External File

Writing to an external file is simply a matter of creating a filename, opening the file for writing, and writing data to it in the standard way that the scripting language supports. For example:

Python
    outfile = os.path.join(os.getcwd(), os.path.basename(infile) + ".tmp")
    outfile = outfile.replace("/", os.sep)
    test.log("Writing %s" % outfile)
    file = codecs.open(outfile, "w", "utf-8")
    for line in lines:
        file.write(line)
    file.close()


JavaScript
    outfile = infile + ".tmp";
    var i = outfile.lastIndexOf(File.separator);
    if (i > -1)
        outfile = outfile.substr(i + 1);
    outfile = OS.cwd().replace(/[\/]/, File.separator) + File.separator + outfile;
    test.log("Writing " + outfile);
    file = File.open(outfile, "w")
    for (var i in lines)
        file.write(lines[i] + "\n");
    file.close();


Perl
    my $sep = File::Spec->rootdir();
    my $outfile = File::Spec->rel2abs(basename($infile) . ".tmp");
    $outfile =~ s,/,$sep,g;
    test::log("Writing $outfile");
    open(FILE, ">:encoding(UTF-8)", $outfile) or test::fail("Failed to write $outfile");
    print FILE @lines;
    close(FILE);


Tcl
    set outfile [file tail "$infile.tmp"]
    set outfile [file normalize $outfile]
    test log "Writing $outfile"
    set fh [open $outfile "w"]
    foreach line $lines {
        puts $fh $line
    }
    close $fh


Here, we write a file that has the same basename as the file we read, but with .tmp appended (e.g., before.csv.tmp), and save it into the script's current working directory. Since we write exactly the same data as we read, this file and the original should be identical. (We'll see how to check this in a later subsection.)

Just as we did when reading a file, we replace the Unix-style path separators with the path separator that is appropriate for the platform.

15.1.19.1.3. How to Check the Existance of an External File

Here is an example that checks two files: the first is expected to exist and the second is not expected to exist.

Python
    test.verify(os.path.exists(infile), "infile correctly present")
    test.verify(not os.path.exists(outfile), "outfile sucessfully deleted")


JavaScript
    test.verify(File.exists(infile), "infile correctly present");
    test.verify(!File.exists(outfile), "outfile sucessfully deleted");


Perl
    test::verify(-e $infile, "infile correctly present");
    test::verify(!-e $outfile, "outfile sucessfully deleted");


Tcl
    test verify [file exists $infile] "infile correctly present"
    test verify [expr ![file exists $outfile]] "outfile sucessfully deleted"


We have used the two-argument form of the test.verify function to provide more useful detail information than simply True expression.

15.1.19.1.4. How to Remove an External File

Removing an external file is easy—but not reversible!

Python
    os.remove(outfile)


JavaScript
    File.remove(outfile);


Perl
    unlink $outfile;


Tcl
    file delete $outfile


It would make sense to follow this with a call to the test.verify function in conjunction with an existence test to check that the file has been removed as expected.

15.1.19.2. How to Compare External Files inside Test Scripts

To compare two external files, for most scripting languages there are two approaches we can take. The easiest approach is to read in the entire contents of each file—for example, into two strings—and compare the strings. Unfortunately this approach can be very slow if the files are large or if lots of files are compared. Another approach—that is likely to be very fast—is to use an external program designed for the job. On Unix-like systems, such a program is diff (difference) and on Windows the program is fc (file compare).

Python
    if sys.platform in ("win32", "cygwin"):
        command = ["fc"]
    else:
        command = ["diff"]
    command.extend(('"%s"' % infile, '"%s"' % outfile))
    result = subprocess.call(" ".join(command), shell=True)
    test.verify(result == 0, "infile and outfile equal according to %s" % command[0])


JavaScript
    var diff = OS.name == "Windows" ? "fc" : "diff";
    var command = diff;
    command += ' "' + infile + '" "' + outfile + '"';
    var result = OS.system(command);
    test.verify(result == 0,
                "infile and outfile equal according to " + diff);


Perl
    my $diff = $^O eq "MSWin32" ? "fc" : "diff";
    my $command = $diff;
    $command .= " '$infile' '$outfile'";
    system($command);
    my $result = $? >> 8;
    test::verify($result == 0, "infile and outfile equal according to $diff");


Tcl
    if {$::tcl_platform(platform) eq "windows"} {
        set diff "fc"
    } else {
        set diff "diff"
    }
    set result 0
    if {[catch {exec $diff $infile $outfile} results options]} {
        set details [dict get $options -errorcode]
        if {[lindex $details 0] eq "CHILDSTATUS"} {
            set result [lindex $details 2]
            test fail "infile and outfile not equal according to $diff"
        } else {
            test fail "Failed to get $diff's result"
        }
    } else {
        test pass "infile and outfile equal according to $diff"
    }


We have to make our script account for the fact that we use the fc program on Windows and the diff program on other platforms. Fortunately, both programs exhibit the same behavior: if the two files are the same they return 0 to the operating system and if the two files differ they return 1. (They may return other values, e.g., 2, if an error in the command line they are given is encountered.)

Note that it is essential that the filenames use the correct path separators for the platform. We also put the filenames in quotes—except for the Tcl example—in case they (or their paths) contain spaces—something quite possible on Windows.

[Note]Python-specific

Python programmers can avoid using an external program and also the inefficiency of loading entire files into memory by taking advantage of the Python standard library's filecmp module. This reduces comparing two files to a simple one-liner:

Python
    test.verify(filecmp.cmp(infile, outfile, False), "infile and outfile equal according to filecmp library")


The filecmp.cmp function returns a Boolean with True indicating that the files are the same. The third parameter should always be False to avoid any risk of false-positives.

15.1.19.3. How to Read Environment Variables inside Test Scripts

Here is an example of how to read the Squish-specific environment variables that are set while the test script is running.

Python
    for key, value in sorted(os.environ.items()):
        if key.upper().startswith("SQUISH"):
            test.log("%s = %s" % (key, value))


JavaScript
    var keys = ["SQUISHRUNNER_DEBUGLOG", "SQUISHRUNNER_HOST",
                "SQUISHRUNNER_PORT", "SQUISH_PREFIX"];
    for (i in keys)
        test.log(keys[i] + " = " + OS.getenv(keys[i]));


Perl
    foreach $key (sort keys %ENV) {
        if (uc $key =~ /^SQUISH/) {
            test::log("$key = $ENV{$key}");
        }
    }


Tcl
    global env
    foreach key [array names env] {
        if {[string first "SQUISH" $key] > -1} {
            test log "$key = $env($key)"
        }
    }


For the examples that have an if condition it is easy to see that we can log all the environment variables, not just the Squish-specific ones, simply by eliminating the condition.

15.1.20. How to Access Databases from Squish Test Scripts

Squish test scripts can access databases where the underlying scripting language provides a suitable library. (And in the case of JavaScript, that has no such library, Squish provides one.)

There are two main uses that can be made of database access, both of which are show in this section's two subsections. We can access a database to compare a database table's contents with data in the AUT, and we can log our test results directly to a database if that is more convenient than processing Squish's test log files.

[Note]Python-specific

The examples in this section use SQLite 3, bindings to which are provided by the pysqlite package which has been supplied as part of Python's standard library since Python 2.5.

By default Squish binary packages for all platforms use the Python 2.4 that is installed inside the Squish directory. On Unix-like systems, if Squish is built from a source package, Squish uses whatever Python 2.x it finds installed on the system. If you have Python 2.3 or 2.4 (the oldest versions that Squish supports), you will need to install the pysqlite package manually. The package is available from pysqlite and is available in source form for all systems and in binary form for Windows.

Incidentally, PyPI (Python Package Index) provides many different database bindings packages, so you are not limited to SQLite when using Python.

[Note]Perl-specific

The examples in this section use SQLite 3, bindings to which are provided by the DBD::SQLite package which is available from CPAN.

Windows users can install this package by starting a Console session and invoking Perl at the command line with perl -MCPAN -e shell (this assumes that the machine is connected to the Internet). This will produce the CPAN prompt where you must install two packages. First type in install DBI, and then install DBD::SQLite. For Unix-like system users, use the package management tools to install the DBI and DBD::SQLite packages.

[Note]Tcl-specific

The examples in this section use SQLite 3, bindings to which are provided by the SQLite developers.

Linux users should be able to obtain the bindings via their package management tools—the package name should be tclsqlite or similar. Windows users will need to download the bindings from SQLite. Click the Download link and get the binary tclsqlite-version.zip package. Mac OS X users might have to build the package from source—like the Windows binary package it is available from SQLite after clicking the Download link.

15.1.20.1. How to Compare Application Data with Database Data

Sometimes it is convenient to compare application data with data in a database. Some scripting languages include modules for database access in their standard libraries. Unfortunately this isn't the case for JavaScript, so Squish provides the SQL Object (Section 16.1.15.6.1) which can be used to interact with databases from JavaScript test scripts.

In this subsection we will look at how to read data from a table widget and for each row, verify that each cell has the same data as the corresponding SQL database's row's field. In the examples we will use Java AWT/Swing's JTable as the data-holding widget, but of course, we could use exactly the same approach using a Java SWT Table or a Qt QTableWidget, or any other supported toolkit's table.

The structure of our main function is very similar to one we used earlier in the CsvTable example where we compared the contents of a JTable with the contents of the .csv file from which the table was populated. Here though, instead of a custom compareTableWithDataFile function, we have a compareTableWithDatabase function. (See How to Test JTable and Use External Data Files (Java—AWT/Swing) (Section 15.1.16.2.3), How to Test the Table Widget and Use External Data Files (Java/SWT) (Section 15.1.16.3.3), and How to Test Table Widgets and Use External Data Files (Qt 4) (Section 15.1.11.4).)

Python
def main():
    startApplication("CsvTable.class")
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename)
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    compareTableWithDatabase(jtable)

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'}");
    compareTableWithDatabase(jtable);
}

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'}");
    compareTableWithDatabase($jtable);
}

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'}}]
    compareTableWithDatabase $jtable
}

The main function begins by loading some common convenience functions, including a doOpenFile function that navigates the AUT's menu system to open a file with the given name. Once the file is loaded the JTable is populated with the file's contents and we then call the custom compareTableWithDatabase function to see if what we've loaded from the .csv file matches the data in a SQLite 3 database file.

Unfortunately, the database APIs vary quite a lot between the different scripting languages, so although the structure of the custom compareTableWithDatabase functions are all the same, the details are somewhat different. In view of this we will look at each language's implementation in is own separate subsubsection—each subsubsection is complete in itself, so you only need to read the one relevant to the scripting language that interests you.

15.1.20.1.1. Comparing a GUI Table with a Database Table in Python
Python
import sqlite3

def compareTableWithDatabase(jtable):
    db3file = findFile("testdata", "before.db3")
    db = cursor = None
    try:
        tableModel = jtable.getModel()
        db = sqlite3.connect(db3file)
        cursor = db.cursor()
        cursor.execute("SELECT id, pollutant, type, standard, averaging_time, "
                       "regulatory_citation FROM csv ORDER BY id")
        for record in cursor:
            row = record[0] - 1
            for column in range(0, 5):
                test.compare(tableModel.getValueAt(row, column).toString(), record[column + 1])
    finally:
        if cursor is not None:
            cursor.close()
        if db is not None:
            db.close()

The first thing we must do—before writing any of our functions—is import the sqlite3 module that the pysqlite package provides.

To connect to a SQLite database we only need to supply a filename. The means of connection varies between scripting languages and libraries, as do their SQL APIs, but they are all the same in principle, even if the details of the syntax vary, although in most cases they require a username, password, hostname, and port, rather than a filename.

In Python we must obtain a connection, and then use the connection to obtain a database cursor. It is through this cursor that we execute queries. In this particular example, the SQL database table has a field that isn't present in the .csv file—id—which actually corresponds to the record's row (but using 1-based indexing). Once we have the connection and cursor, we get a reference to the JTable's underlying model—naturally, this is different if we use a different toolkit, but whether we access a table widget's cells directly or via a model, we still get access to each cell's data. Then we execute the SELECT query. We can iterate over the rows returned by the query (if any), by iterating over the cursor.

Each row returned by the cursor is effectively a tuple. We begin by retrieving the record's id which is the record tuple's first item, and deducting 1 to account for the fact that the JTable uses 0-based rows and the database uses 1-based IDs that correspond to rows. Then we iterate over every column, retrieving the JTable's text for the given row and column and comparing it with the database record with the corresponding row (ID) and column. (We have to add 1 to the database column because the database has an extra column at the beginning storing the IDs.)

And at the end, we close the cursor and the connection to the database, providing we made a successful connection in the first place. Although it doesn't matter much for SQLite, closing the connection to other databases is usually very important, so we have used a try ... finally construct to ensure that no matter what happens after the connection is made, the connection is safely closed in the end. (Of course, Squish would close the connection for us anyway, but we prefer to take a best-practice approach to our test code.)

15.1.20.1.2. Comparing a GUI Table with a Database Table in JavaScript
JavaScript
function compareTableWithDatabase(jtable)
{
    var db3file = findFile("testdata", "before.db3");
    var db;
    try {
        var tableModel = jtable.getModel();
        db = SQL.connect({Driver: "SQLite", Host: "localhost", 
                          Database: db3file, UserName: "", Password: ""});
        var result = db.query("SELECT id, pollutant, type, standard, averaging_time, " +
                              "regulatory_citation FROM csv ORDER BY id");
        while (result.isValid) {
            var row = result.value("id") - 1;
            for (var column = 0; column < 5; ++column) 
                test.compare(tableModel.getValueAt(row, column).toString(), result.value(column + 1));
            result.toNext();
        }
    }
    finally {
        if (db)
            db.close();
    }
}

For the SQLite database it isn't necessary to provide a host, username, or password, but we have done so here in the JavaScript version because they are needed by pretty well every other database (although in most cases, host will sensibly default to localhost if not specified). Another SQLite quirk is that we must specify a database filename. The means of connection varies between scripting languages and libraries, as do their SQL APIs, but they are all the same in principle, even if the details of the syntax vary.

In JavaScript using Squish's SQL Object (Section 16.1.15.6.1), we can execute queries on the connection object itself. In fact, the JavaScript API has two kinds of query function we can use, the sqlConnection.query function for executing SELECT statements, and the sqlConnection.execute function for all other kinds of SQL statements (e.g., DELETE, INSERT, UPDATE).

In this particular example, the SQL database table has a field that isn't present in the .csv file—id—which actually corresponds to the record's row (but using 1-based indexing). Once we have the connection, we get a reference to the JTable's underlying model—naturally, this is different if we use a different toolkit, but whether we access a table widget's cells directly or via a model, we still get access to each cell's data. Then we execute the SELECT query. The query returns a SQLResult Object (Section 16.1.15.6.3), and this automatically navigates to the first record in the result set (assuming that there were any results). This gives us access to the first record in the results set.

The JavaScript API's SQLResult Object (Section 16.1.15.6.3)'s isValid property is true if we have navigated to a valid record. The sqlResult.value method can accept either a field index (in this case, 0 for the id field, 1 for the pollutant field, and so on), or a field name. We begin by retrieving the record's id using the field name, and deducting 1 to account for the fact that the JTable uses 0-based rows and the database uses 1-based IDs that correspond to rows. Then we iterate over every column, retrieving the JTable's text for the given row and column and comparing it with the database record with the corresponding row (ID) and column. (We have to add 1 to the database column because the database has an extra column at the beginning storing the IDs.) Once all the table's row's cells have been compared with the database's record's fields, we attempt to navigate to the next record in the database using the sqlResult.toNext method.

And at the end, we close the connection to the database, providing we made a successful connection in the first place. Although it doesn't matter much for SQLite, closing the connection to other databases is usually very important, so we have used a try ... finally construct to ensure that no matter what happens after the connection is made, the connection is safely closed in the end. (Of course, Squish would close the connection for us anyway, but we prefer to take a best-practice approach to our test code.)

15.1.20.1.3. Comparing a GUI Table with a Database Table in Perl
Perl
require Encode;
use DBI;

sub compareTableWithDatabase
{
    my $jtable = shift(@_);
    my $db3file = findFile("testdata", "before.db3");
    eval {
        my $db = DBI->connect("dbi:SQLite:$db3file") || die("Failed to connect: $DBI::errstr");
        my $tableModel = $jtable->getModel();
        my $records = $db->selectall_arrayref(
                "SELECT id, pollutant, type, standard, averaging_time, " .
                "regulatory_citation FROM csv ORDER BY id");
        foreach my $record (@$records) {
            my $row = $record->[0] - 1;
            foreach $column (0..4) {
                my $field = $record->[$column + 1];
                Encode::_utf8_on($field);
                test::compare($tableModel->getValueAt($row, $column)->toString(), $field);
            }
        }
    };
    if ($@) {
        test::fatal("$@");
    }
    else {
        $db->disconnect;
    }
}

The first thing we must do—before writing any of our functions—is require the Encode module (the need for which we will explain shortly), and use the DBI module that provides Perl's database access.

To connect to a SQLite database we only need to supply a filename. The means of connection varies between scripting languages and libraries, as do their SQL APIs, but they are all the same in principle, even if the details of the syntax vary, although in most cases they require a username, password, hostname, and port, rather than a filename.

In Perl we must obtain a connection and then use the connection object to perform our database operations. In this particular example, the SQL database table has a field that isn't present in the .csv file—id—which actually corresponds to the record's row (but using 1-based indexing). Once we have the connection, we get a reference to the JTable's underlying model—naturally, this is different if we use a different toolkit, but whether we access a table widget's cells directly or via a model, we still get access to each cell's data. Then we execute the SELECT query, asking to get our results as a reference to the results array (rather than copying the array, which would be inefficient). We can iterate over the rows returned by the query (if any), by iterating over the array's items.

Each array element holds one record. We begin by retrieving the record's id which is the record's first item, and deducting 1 to account for the fact that the JTable uses 0-based rows and the database uses 1-based IDs that correspond to rows. Then we iterate over every column, retrieving the JTable's text for the given row and column and comparing it with the database record with the corresponding row (ID) and column. (We have to add 1 to the database column because the database has an extra column at the beginning storing the IDs.)

Java™—and therefore the JTable—stores text as Unicode, and the text in our SQLite 3 database is also stored as Unicode (using the UTF-8 encoding). However, Perl assumes that text uses the local 8-bit encoding by default, so when we retrieve each text field from each record we must make sure that Perl knows that it is Unicode so that the comparison is correctly performed between Unicode strings and not between a Unicode string from the JTable and (possibly invalid) local 8-bit text from the database. This is achieved by using the Encode module's _utf8_on method. (Note that this method should only be used if we are certain that the string we mark holds UTF-8 text.)

And at the end, we close the connection to the database, providing we made a successful connection in the first place. Although it doesn't matter much for SQLite, closing the connection to other databases is usually very important, so we have used an eval block to ensure that no matter what happens after the connection is made, the connection is safely closed in the end. (Of course, Squish would close the connection for us anyway, but we prefer to take a best-practice approach to our test code.)

15.1.20.1.4. Comparing a GUI Table with a Database Table in Tcl
Tcl
package require sqlite3

proc compareTableWithDatabase {jtable} {
    sqlite3 db [findFile "testdata" "before.db3"]
    set tableModel [invoke $jtable getModel]
    set fields [list pollutant type standard averaging_time regulatory_citation]
    set row 0
    db eval {SELECT id, pollutant, type, standard, averaging_time, regulatory_citation FROM csv ORDER BY id} values {
        for {set column 0} {$column < 5} {incr column} {
            set table_value [invoke [invoke $tableModel getValueAt $row $column] toString]
            set db_value $values([lindex $fields $column])
            test compare $table_value $db_value
        }
        incr row
    }
    db close
}

The first thing we must do—before writing any of our functions—is import the sqlite3 module. If the package is installed in a standard location it can be imported using a package require statement. Otherwise it is necessary to load the shared library that contains the bindings—for example, by replacing the package require statement with, say, load "C:\tclsqlite3.dll".

To connect to a SQLite database we only need to supply a filename. The means of connection varies between scripting languages and libraries, as do their SQL APIs, but they are all the same in principle, even if the details of the syntax vary, although in most cases they require a username, password, hostname, and port, rather than a filename.

In Tcl we can perform all our database operations through the connection object. In this particular example, the SQL database table has a field that isn't present in the .csv file—id—which actually corresponds to the record's row (but using 1-based indexing). Once we have the connection, we get a reference to the JTable's underlying model—naturally, this is different if we use a different toolkit, but whether we access a table widget's cells directly or via a model, we still get access to each cell's data. Then we execute the SELECT query. The query returns each row in turn in the values array.

In this example we ignore the first field of each row that's returned (the id) since this field isn't present in the JTable. For the remaining fields, we iterate over each one, retrieving the JTable's text for the given row and column and comparing it with the database record with the corresponding row and column.

And at the end, we close the connection to the database. Although it doesn't matter much for SQLite, closing the connection to other databases is usually very important—of course, Squish would close the connection for us anyway, but we prefer to be explicit about our intentions.

15.1.20.2. How to Log Test Results Directly into a Database

Squish can output its test results in plain text or XML, so it is very easy to parse the results to analyse them and to produce reports. (See, for example, How to Do Automated Batch Testing (Section 15.5) and Processing Test Results (Section 16.2.3).) However, if we prefer, we can log the test results directly from our test scripts ourselves. One way to do this is to use the scripting language's logging facilities (if it has any)—for example, using Python's logging module. Another way is to log the results directly into a database—this is the approach we will look at in this subsection.

For our example we will use a simple SQLite 3 database stored in the test suite's shared test data in file logfile.db3. The database has three fields, id (an auto-incrementing integer), result, and message, both text fields. Our test code assumes that the database exists (and so, is initially empty).

We will start by looking at a test case's main function and where calls to Squish's test.log function have been replaced with calls to a custom DB class instances's log method, and similarly calls to Squish's test.compare and test.verify functions have been replaced with calls to our custom db object's compare and verify methods. (Note that for Tcl we don't create a custom class or object, but just use plain functions.)

Python
def main():
    startApplication("ItemViews.class")
    db = None
    try:
        db = DB()
        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"
                message = "(%d, %d) '%s'%s" % (row, column, item.toString(), selected)
                db.log(message)
            expected = bool(row in (14, 24))
            db.compare(model.getValueAt(row, 0).toString(), str(expected))
        db.verify(model.getRowCount() == 25)
    finally:
        if db is not None:
            db.close()

JavaScript
function main()
{
    startApplication("ItemViews.class");
    var db;
    try {
        db = new DB();
        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";
                }
                var message = "(" + String(row) + ", " + String(column) + ") '" +
                         item.toString() + "'" + selected;
                db.log(message);
            }
            var expected = new Boolean((row == 14 || row == 24) ? true : false);
            db.compare(model.getValueAt(row, 0).toString(), expected.toString());
        }
        db.verify(model.getRowCount() == 25);
    }
    finally {
        if (db)
            db.close();
    }
}

Perl
sub main
{
    startApplication("ItemViews.class");
    my $db;
    eval {
        $db = new DB(findFile("testdata", "logfile.db3"));
        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";
                }
                $db->log("($row, $column) '$item'$selected");
            }
            my $expected = ($row == 14 || $row == 24) ? "true" : "false";
            $db->compare($model->getValueAt($row, 0), $expected);
        }
        $db->verify($model->getRowCount() == 25);
    };
    if ($@) {
        test::fatal("$@");
    }
    else {
        $db->close;
    }
}

Tcl
proc main {} {
    startApplication "ItemViews.class"
    sqlite3 db [findFile "testdata" "logfile.db3"]
    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]
            set message "($row, $column) '$text'$selected"
            db:log db $message
        }
        set expected "false"
        if {$row == 14 || $row == 24} {
            set expected "true"
        }
        set value [invoke [invoke $model getValueAt $row 0] toString]
        db:compare db $value $expected
    }
    db:verify db [expr {[invoke $model getRowCount] == 25}]
    db close
}

The main function is very similar to one we saw in the itemviews example (see How to Test JList, JTable, and JTree widgets (Java—AWT/Swing) (Section 15.1.16.2.2)). The function iterates over every row in a table widget and over every cell in every row. For each cell we log its contents with a string of the form (row, column) text, optionally appending +selected to the text for cells that are selected. The table's first row consists of checkboxes—the text for these comes out as true or false—and we check each one to make sure that it is unchecked (or in the case of rows 14 and 24, checked). And at the end we verify that the table has exactly 25 rows.

The DB class's methods (and for Tcl, the db:* functions) are simpler and less sophisticated than Squish's built-in test methods, but they show the proof of concept—you can of course make your own database logging functions as advanced as you like.

In terms of the DB class, we begin by creating an instance—and as we will see in a moment, the database connection is made in the constructor. Then we call methods on the db object in place of the Squish test methods we would normally use.

Most scripting languages either don't have destructors, or have destructors that are not guaranteed to be called (or in the case of JavaScript, don't have a notion of destructors at all), so we use the appropriate scripting-language construct to ensure that if the db object is created successfully, it is closed at the end—and inside this close method, the database connection is closed.

We are now ready to review the DB class and its methods (or for Tcl, the db:* functions). But, as we mentioned in the previous section, the database APIs vary quite a lot between the different scripting languages, so we will look at each language's implementation of this class in is own separate subsubsection—each subsubsection is complete in itself, so you only need to read the one relevant to the scripting language that interests you.

15.1.20.2.1. Logging Results Directly to a Database in Python
Python
import sqlite3

class DB:

    def __init__(self):
        self.db = self.cursor = None
        self.db = sqlite3.connect(findFile("testdata", "logfile.db3"))
        self.cursor = self.db.cursor()

    def log(self, message):
        self.cursor.execute("INSERT INTO log (result, message) VALUES ('LOG', ?)", (message,))

    def compare(self, first, second):
        if first == second:
            result = "PASS"
        else:
            result = "FAIL"
        self.cursor.execute("INSERT INTO log (result, message) VALUES (?, 'Comparison')", (result,))

    def verify(self, condition):
        if condition:
            result = "PASS"
        else:
            result = "FAIL"
        self.cursor.execute("INSERT INTO log (result, message) VALUES (?, 'Verification')", (result,))

    def close(self):
        self.db.commit()
        if self.cursor is not None:
            self.cursor.close()
        if self.db is not None:
            self.db.close()

We must, of course, begin by importing the sqlite3 module.

The DB class assumes that the database already exists and contains a table called log that has at least two text fields, result and message. In fact, for this example the SQLite we used to create the table was: CREATE TABLE log (id INTEGER PRIMARY KEY, result TEXT, message TEXT). The id field is autoincrementing which is why we don't need to explicitly insert values for it.

One small point to note is that if we use placeholders in calls to the cursor.execute method (i.e., ?, as we ought to, and do here), then the second argument must be a tuple, even if we only have one value to pass as in all the methods implemented here.

Clearly the DB class is very simple, but it shows the fundamentals of how we could create a database-savvy object that we could use to store whatever test data and results we liked, ready for post-processing or reporting.

15.1.20.2.2. Logging Results Directly to a Database in JavaScript
JavaScript
function DB()
{
    var logfile = findFile("testdata", "logfile.db3");
    this.connection = SQL.connect({Driver: "SQLite", Host: "localhost", 
                                   Database: logfile, UserName: "", Password: ""});
}

DB.prototype.log = function(message)
{
    message = message.replace(RegExp("'", "g"), "");
    this.connection.execute("INSERT INTO log (result, message) VALUES ('LOG', '" + message + "')");
}

DB.prototype.compare = function(first, second)
{
    var result = first == second ? "PASS" : "FAIL";
    this.connection.execute("INSERT INTO log (result, message) VALUES ('" + result + "', 'Comparison')");
}

DB.prototype.verify = function(condition)
{
    var result = condition ? "PASS" : "FAIL";
    this.connection.execute("INSERT INTO log (result, message) VALUES ('" + result + "', 'Verification')");
}


DB.prototype.close = function()
{
    this.connection.close();
}

The DB function is the constructor and we use it to create the database connection. To provide the object returned by calling new DB() with methods, we create anonymous functions which we immediately assign to the DB class's prototype, using the names by which we want to call them.

In the case of the DB.log method, we remove any single quotes from the message since we create the SQL to execute purely as a string, and single quotes would confuse things. (An alternative would be to escape them.)

The DB class assumes that the database already exists and contains a table called log that has at least two text fields, result and message. In fact, for this example the SQLite we used to create the table was: CREATE TABLE log (id INTEGER PRIMARY KEY, result TEXT, message TEXT). The id field is autoincrementing which is why we don't need to explicitly insert values for it.

Clearly the DB class is very simple, but it shows the fundamentals of how we could create a database-savvy object that we could use to store whatever test data and results we liked, ready for post-processing or reporting.

15.1.20.2.3. Logging Results Directly to a Database in Perl
Perl
use DBI;

package DB;

sub new {
    my $self = shift;
    my $class = ref($self) || $self;
    my $db3file = shift;
    my $db = DBI->connect("dbi:SQLite:$db3file") || die("Failed to connect: $DBI::errstr");
    $self = { "db" => $db };
    return bless $self, $class;
}

sub log {
    my ($self, $message) = @_;
    my $query = $self->{db}->prepare("INSERT INTO log (result, message) VALUES ('LOG', ?)");
    $query->execute($message);
}

sub compare {
    my ($self, $first, $second) = @_;
    my $result = ($first eq $second) ? "PASS" : "FAIL";
    my $query = $self->{db}->prepare("INSERT INTO log (result, message) VALUES (?, 'Comparison')");
    $query->execute($result);
}

sub verify {
    my ($self, $condition) = @_;
    my $result = $condition ? "PASS" : "FAIL";
    my $query = $self->{db}->prepare("INSERT INTO log (result, message) VALUES (?, 'Verification')");
    $query->execute($result);
}

sub close {
    my $self = shift;
    $self->{db}->disconnect;
}

We must, of course, begin by using the DBI module to provide database access.

The DB class assumes that the database already exists and contains a table called log that has at least two text fields, result and message. In fact, for this example the SQLite we used to create the table was: CREATE TABLE log (id INTEGER PRIMARY KEY, result TEXT, message TEXT). The id field is autoincrementing which is why we don't need to explicitly insert values for it.

One small point to note is that if we use placeholders in calls to the prepare function (i.e., ?, as we ought to, and do here), we must pass the actual values to use when we call the execute method.

Clearly the DB class is very simple, but it shows the fundamentals of how we could create a database-savvy object that we could use to store whatever test data and results we liked, ready for post-processing or reporting.

15.1.20.2.4. Logging Results Directly to a Database in Tcl
Tcl
package require sqlite3

proc db:log {db message} {
    db eval {INSERT INTO log (result, message) VALUES ("LOG", $message)}
}

proc db:compare {db first second} {
    if {$first == $second} {
        set result "PASS"
    } else {
        set result "FAIL"
    }
    db eval {INSERT INTO log (result, message) VALUES ($result, "Comparison")}
}

proc db:verify {db condition} {
    if {$condition} {
        set result "PASS"
    } else {
        set result "FAIL"
    }
    db eval {INSERT INTO log (result, message) VALUES ($result, "Verification")}
}

We must, of course, begin by importing the sqlite3 module. (See Comparing a GUI Table with a Database Table in Tcl (Section 15.1.20.1.4) for another way to do the import.)

The db:* functions all expect to be passed a SQLite 3 database connection object as their first argument. All three functions assume that the database already exists and contains a table called log that has at least two text fields, result and message. In fact, for this example the SQLite we used to create the table was: CREATE TABLE log (id INTEGER PRIMARY KEY, result TEXT, message TEXT). The id field is autoincrementing which is why we don't need to explicitly insert values for it.

Clearly these functions are very simple, but they show the fundamentals of how we could create database-savvy functions that we could use to store whatever test data and results we liked, ready for post-processing or reporting.

15.1.21. How to Handle Exceptions Raised in Test Scripts

Some of Squish's functions raise catchable exceptions on failure. We can write our test scripts so that they can catch these exceptions and respond accordingly—for example, by recording a test failure in the test log.

The exception handling mechanism works the same way (for each scripting language), no matter what function raised the exception, so we only need to look at one example to see how it is done.

Python
    try:
        checkBox = waitForObject(":Make Payment.Check Signed_QCheckBox")
        test.passes("Found the checkbox as expected")
    except LookupError, err:
        test.fail("Unexpectedly failed to find the checkbox", str(err))


JavaScript
    try {
        checkBox = waitForObject(":Make Payment.Check Signed_QCheckBox");
        test.pass("Found the checkbox as expected");
    } catch (err) {
        test.fail("Unexpectedly failed to find the checkbox", err);
    }


Perl
    eval {
        my $checkBox = waitForObject(":Make Payment.Check Signed_QCheckBox");
        test::pass("Found the checkbox as expected");
    };
    test::fail("Unexpectedly failed to find the checkbox", "$@") if $@;


Tcl
    if {[catch {
        set checkBox [waitForObject ":Make Payment.Check Signed_QCheckBox"]
        } result]} {
        test fail "Unexpectedly failed to find the checkbox" $result
    } else {
        test pass "Found the checkbox as expected"
    }


The waitForObject function tries to find the specified object. If the object is not accessible—perhaps because it isn't visible—within the timeout period, the function raises a catchable exception. In this example, we call the test.pass function if the object (a checkbox in this example) is found within the timeout period, and the test.fail function if the object isn't found and giving the exception (in string form) as the details text.

[Note]Python-specific

Notice that we must use the test.passes function instead of test.pass to avoid a name clash with Python's built-in pass statement.

15.1.22. How to Modify Squish Functions

In some situations it is useful to modify Squish functions—for example, to record every call to a particular function in the test log. Some scripting languages support the replacing of one function with another of the same name, so we can take advantage of this facility in Squish. Keep in mind though, that most of Squish's functions are not accessible until after a call to the startApplication function, so any modifications we want to do must be done after that call.

Suppose, for example, that we want to change the clickButton function so that it writes to the test log every time it is called. Here's how it can be done:

Python
def addLoggingToClickButton(clickButtonFunction): # Add this function
    def wrappedFunction(button, logText="clickButton() called"):
        test.log(logText, str(button))
        clickButtonFunction(button)
    return wrappedFunction

def main():
    startApplication("addressbook")
    global clickButton                                 # Add this line
    clickButton = addLoggingToClickButton(clickButton) # Add this line
    # ...


JavaScript
function addLoggingToClickButton(clickButtonFunction) // Add this function
{
    return function(button, logText)
    {
        if (!logText)
            logText = "clickButton() called";
        test.log(logText, button);
        clickButtonFunction(button);
    }
}

function main()
{
    startApplication("addressbook");
    clickButton = addLoggingToClickButton(clickButton) // Add this line
    // ...


This creates a custom clickButton function that has a different API from Squish's built-in clickButton function in that the custom function can accept an additional optional argument. This means that existing calls will continue to work as normal (only now each call will result in a test log entry), and that we can add additional information to any calls to the clickButton function as we like. For example, we could replace, say:

clickButton(waitForObject(":Address Book - Add.OK_QPushButton"));

with:

clickButton(waitForObject(":Address Book - Add.OK_QPushButton"),
        "Add Requested");

If we make this change the Test Results will list a Log entry with the Message text Add Requested, and with a Location of the file and line number where the call was made. And for all calls of the clickButton function that are left unchanged each one will result in a Log entry with a Message text of clickButton() called since that's the default text we have used in the addLoggingToClickButton function.

Naturally, the addLoggingToClickButton function could be put in a shared script if you wanted to use it in several different test cases. And it should be straightforward to use the addLoggingToClickButton function as a model for wrapping other Squish functions, either to add logging, or to make them do other things before or after calling the original function—or both before and after.

[Note]Python-specific

If we are willing to assume that Squish will always use positional rather than keyword arguments (or are willing to take the chance), we can make a single generic wrapping function that can be used to add logging to any Squish Python function rather than having to use one wrapping function per function to be wrapped.

Here's the function and how we'd register it for use:

Python
def addLogging(function): # Add this function
    def wrappedFunction(*args, **kwargs):
        if "logText" in kwargs:
            logText = kwargs["logText"]
        else:
            logText = "Logged function called"
        arg0 = ""
        if args:
            args0 = args[0]
        test.log(logText, str(arg0))
        function(*args)
    return wrappedFunction

def main():
    startApplication("addressbook")
    global clickButton                    # Add this line
    clickButton = addLogging(clickButton) # Add this line
    # ...


As before the clickButton function will behave normally, but now it will accept an optional keyword argument called logText. So now, for example, we could replace, say:

clickButton(waitForObject(":Address Book - Add.OK_QPushButton"));

with:

clickButton(waitForObject(":Address Book - Add.OK_QPushButton"),
        logText="Add Requested");

The advantage of this more generic wrapper is that it can be used on any Squish function, even those that take more than one positional argument.




[12] It is also possible to import test data files in .tsv (tab-separated values format), .csv (comma-separated values format), and .xls (Microsoft® Excel™ spreadsheet format). Both .csv and .tsv files are assumed to use the Unicode UTF-8 encoding—the same encoding used for all test scripts.