This section discusses Squish's scripting support, the different scripting languages Squish supports, and the script APIs which are available when working with test scripts. It is illustrated by many examples
![]() | Important |
|---|---|
The Squish IDE loads and saves test scripts (files with names matching
Note also that some characters, most notably double quotes
( |
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.
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:
cashWidget = waitForObject("{name='CashWidget' type='QLabel'}")
var cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");
my $cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");
set cashWidget [waitForObject {{name='CashWidget' type='QLabel'}}]
To create a string that represents a real (multi-property) name, we
create a string which has an opening brace, then two or more
space-separated property items (each having the form,
propertyname='value'),
and finally a closing brace. One of the properties must be the
object's type. If the object has an object name, using just
the type and name properties is sufficient
(providing that the name is unique amongst objects of the specified
type).
Once we have a reference to an object we can access its properties, for example, to check them against expected values, or to change them. We will see how to do this in the Squish edition-specific sections that follow.
Unfortunately, reality is not often so convenient. Programmers may not give unique names to objects, or they might not set explicit names at all, and in any case some objects are created as a result of program execution rather than directly by programmers. Objects that don't have names are unnamed, and in most testing situations the majority of objects we want to test are unnamed. Squish has two solutions to this problem. One solution is an extension of the multiple-properties approach (real names), and the other is to use symbolic names.
When we are faced with unnamed objects we can almost always uniquely
identify them by creating a name consisting of multiple properties. For
example, here is how we can identify and access the
payButton button:
payButtonName = ("{type='QPushButton' text='Pay' unnamed='1'"
"visible='1'}")
payButton = waitForObject(payButtonName)
var payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" +
"visible='1'}";
var payButton = waitForObject(payButtonName);
my $payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" .
"visible='1'}";
my $payButton = waitForObject($payButtonName);
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:
paymentSpinBoxName = ("{buddy=':Make Payment.This Payment:_QLabel'"
"type='QSpinBox' unnamed='1' visible='1'}")
paymentSpinBox = waitForObject(paymentSpinBoxName)
var paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" +
"type='QSpinBox' unnamed='1' visible='1'}";
var paymentSpinBox = waitForObject(paymentSpinBoxName);
my $paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" .
"type='QSpinBox' unnamed='1' visible='1'}";
my $paymentSpinBox = waitForObject($paymentSpinBoxName);
set paymentSpinBoxName {{buddy=':Make Payment.This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}}
set paymentSpinBox [waitForObject $paymentSpinBoxName]
Here, the buddy is identified using a symbolic name copied from the Object Map (Section 16.9).
If there is no obvious way of identifying an object, either use Squish's How to Use the Spy (Section 15.2.4) tool to get Squish to provide a suitable name, or record a quick throwaway test in which you interact with the object of interest and then look in the Object Map (Section 16.9) to see what real and symbolic names Squish used, and then use one of these names in your test code.
In some cases we might want to use a property whose text varies. For example, if we want to identify a window whose caption text changes depending on the window's contents. This is possible using Squish's sophisticated matching capabilities and is described later in Improving Object Identification (Section 16.8).
If the waitForObject() function cannot find the
object with the given name a LookupError exception is
raised which if left uncaught leads to an error entry to be added to
Squish's log. This is normally what we want since it probably
means we mistyped one of the property's values. However, if an object
may exist only in some cases (for example, if a particular tab of a tab
widget is chosen), we can use the
object.exists() function to check if an
object of the given name exists, and if it does to perform any tests we
want on it in that case. For example, in Python we could write this:
moreOptionsButtonName = "{type='QPushButton' name='More Options'}"
if object.exists(moreOptionsButtonName):
moreOptionsButton = waitForObject(moreOptionsButtonName)
clickButton(moreOptionsButton)
One advantage of this approach is that if the object does not exist the script finds out straight away. Compare it with this approach:
try:
moreOptionsButtonName = "{type='QPushButton' name='More Options'}"
moreOptionsButton = waitForObject(moreOptionsButtonName)
except LookupError:
pass # button doesn't exist so don't click the button
else:
clickButton(moreOptionsButton)
This is potentially slower than using the object.exists() function since the waitForObject() function will wait for 20 seconds
(the default timeout, which can be changed by giving a second argument),
although both approaches are valid.
When Squish records a test it uses symbolic names to identify the
widgets. Some symbolic names are quite easy to understand, for example,
":fileNameEdit_QLineEdit", while others can be more
cryptic, for example,
":CSV Table - before.csv.File_QTableWidget"—this
symbolic name includes the window caption which shows the name of the
current file. Symbolic names are generated programmatically by Squish
although they can also be used in hand-written code, or when modifying
or using extracts from recorded tests.
Symbolic names have one major advantage over real names: if a property that a real name depends on changes (i.e., due to a change in the AUT), the real name will no longer be valid, and all uses of it in test scripts will have to be updated. But if a symbolic name has been used, the real name that the symbolic name refers to, (i.e., the name's properties and their values), can simply be updated in the Object Map, and no changes to tests are necessary. (See Object Map (Section 16.9).)
Squish
distinguishes between the two by the fact that symbolic names begin with
a colon (:) while real names are always enclosed in braces
({}).
One of Squish's most useful features is the ability to access the complete Qt API (and optional application API) from test scripts. This gives test engineers a huge amount of flexibility allowing them to test just about anything in the AUT.
With Squish's Qt API it is possible to find and query objects,
call methods, and access properties and enums. Furthermore,
Squish 4 automatically recognizes Qt QObject and
QWidget properties and slots.
This means that building custom wrappers is rarely necessary since
application developers can expose custom object properties by using the
Q_PROPERTY macro, and can expost custom object methods by
making them into slots. This even applies (from Qt 4.6) to
automatically recognizing the properties and slots of
QGraphicsWidgets and QGraphicsObjects and
custom subclasses that derive from them.
In addition, Squish provides a How to Use the Qt Convenience API (Section 15.1.2.4) to execute common GUI actions such as clicking a button or selecting a menu item
The chapter How to Test Qt Widgets (Section 15.1.11) later in this manual presents several different examples that show how to use the scripting Qt API to access and test complex Qt applications.
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.
cashRadioButtonName = ("{text='Cash' type='QRadioButton' visible='1'"
"window=':Make Payment_MainWindow'}")
cashRadioButton = waitForObject(cashRadioButtonName)
if not cashRadioButton.checked:
clickButton(cashRadioButton)
test.compare(cashRadioButton.checked, True)
var cashRadioButtonName = "{text='Cash' type='QRadioButton' visible='1'" +
"window=':Make Payment_MainWindow'}";
var cashRadioButton = waitForObject(cashRadioButtonName);
if (!cashRadioButton.checked) {
clickButton(cashRadioButton);
}
test.compare(cashRadioButton.checked, true);
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);
set cashRadioButtonName {{text='Cash' type='QRadioButton' visible='1'
window=':Make Payment_MainWindow'}}
set cashRadioButton [waitForObject $cashRadioButtonName]
if {![property get $cashRadioButton checked]} {
invoke clickButton $cashRadioButton
}
test compare [property get $cashRadioButton checked] true
In this example we get the value of a property, set the property (indirectly by clicking the widget), and then get the value of the property again so that we can test that it has the correct value.
Here is another example, this time one that sets and gets a
QLineEdit's,
text property, and prints the property's value to
Squish's test log.
lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName")
lineedit.text = "A new text"
text = lineedit.text
test.log(str(text))
var lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
lineedit.text = "A new text";
var text = lineedit.text;
test.log(String(text));
my $lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
$lineedit->text = "A new text";
my $text = $lineedit->text;
test::log("$text");
set lineedit [waitForObject "Addressbook.ABCentralWidget1.FirstName"] property set $lineedit text "A new text" set text [property get $lineedit.text] test log [toString $text]
Notice that here we have used symbolic rather than real names. Although it is best to use real names in hand-written code, when Squish records a script it uses symbolic names, so when we copy and paste or modify code from a recorded script we will often end up using symbolic names like these.
With Squish it is possible to call every public function on any Qt object. In addition it is possible to call static functions provided by Qt.
In the example below we change the button text of the button we queried in the previous section using QButton::setText().
button = waitForObject("Addressbook.ABCentralWidget1.AddButton")
button.setText("Changed Button Text")
var button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
button.setText("Changed Button Text");
my $button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
$button->setText("Changed Button Text");
set button [waitForObject "Addressbook.ABCentralWidget1.AddButton"] invoke $button setText "Changed Button Text"
Similarly, static Qt functions can be called. As an example, we will
query the currently active modal widget (e.g. a dialog box) using the
static QApplication::activeModalWidget()
function. If this returns a valid object, we will print the
object's object name (or "unnamed" if no name has been set) to the test
log. To check if the object is valid (i.e., not null), we can use
Squish's isNull() function. To find the
object's name we access its objectName property.
widget = QApplication.activeModalWidget()
if not isNull(widget):
test.log(widget.objectName or "unnamed")
var widget = QApplication.activeModalWidget();
if (!isNull(widget)) {
var name = widget.objectName;
test.log(name.isEmpty() ? "unnamed" : name);
}
my $widget = QApplication::activeModalWidget();
if (!isNull($widget)) {
test::log($widget->objectName() || "unnamed");
}
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"
}
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:
label = waitForObject("Addressbook.ABCentralWidget1.labelFirstName")
label.setAlignment(Qt.AlignLeft)
var label = waitForObject("Addressbook.ABCentralWidget1.labelFirstName");
label.setAlignment(Qt.AlignLeft);
my $label = waitForObject("Addressbook.ABCentralWidget1.labelFirstName");
$label->setAlignment(Qt::AlignLeft);
set label [waitForObject Addressbook.ABCentralWidget1.labelFirstName] invoke $label setAlignment [enum Qt AlignLeft]
This section describes the script API Squish offers on top of the standard Qt API to make it easy to perform common user actions such as clicking a button or activating a menu option. A complete list of this API is available in the Qt Convenience API (Section 16.1.4) section in the Reference Manual (Chapter 16).
Here are some examples to give a flavor of how the API is used. The first line shows how to click a button, the second line shows how to double-click an item (for example, an item in a list, table, or tree), and the last example shows how to activate a menu option.
clickButton("Addressbook.AddButton")
doubleClickItem("Addressbook.addressList", "Max|Mustermann|*", 0, 0, 0, Qt.LeftButton)
activateItem("Addressbook.menubar", "Quit")
clickButton("Addressbook.AddButton");
doubleClickItem("Addressbook.addressList", "Max|Mustermann|*", 0, 0, 0, Qt.LeftButton);
activateItem("Addressbook.menubar", "Quit");
clickButton("Addressbook.AddButton");
doubleClickItem("Addressbook.addressList", "Max|Mustermann|*", 0, 0, 0, Qt::LeftButton);
activateItem("Addressbook.menubar", "Quit");
invoke clickButton "Addressbook.AddButton" invoke doubleClickItem "Addressbook.addressList" "Max|Mustermann|*" 0 0 0 [enum Qt LeftButton] invoke activateItem "Addressbook.menubar" "Quit"
One of Squish's most useful features is the ability to access the toolkit's API from test scripts. This gives test engineers sufficient flexibility to allow them to test just about any aspect of the AUT.
With Squish's Tk-specific API it is possible to find and query objects, access properties, and evaluate arbitrary Tcl code in the AUT's interpreter.
In addition, Squish provides a convenience API (How to Use the Tk Convenience API (Section 15.1.3.4)) to execute common GUI actions such as clicking a button or selecting a menu item.
The chapter How to Test Tk Widgets (Section 15.1.13) later in this manual presents different examples that show how to use the scripting Tk API to access and test complex Tk widgets.
Squish provides the waitForObject() function
which returns a reference to the object with the given qualified object
name. A qualified object name is a name like
myapp.frame1.okbutton. The period notation is used as a
separator (rather like / or \ in file paths),
that is used to identify a particular object by its position in the
object hierarchy. The application's main window is the root of the
hierarchy, and contains all the application's top-level widgets, some of
which contain child widgets, and so on. In the example above, the
okbutton is a child of frame, which in turn is
a child of myapp (the applicaton's main window).
To find out the name of an object, you can use the Spy tool to introspect the application. See the How to Use the Spy (Section 15.2.4) section for details.
To get a reference to an object—which can then be queried to check
the object's properties, or which can be used to interact with the
object—use the waitForObject()
function. For example, in Tcl you would use code like this:
set button [waitForObject "myapp.frame1.okbutton"]
If waitForObject() can't find the specified
object—or if the object is not available before the timeout, for
example if it is hidden—a script error is thrown which stops the
script execution. In some situations it might be desirable to check to
see if the object exists and only interact with the object if it is
found. This can be done by using the object.exists() function.
For example, suppose we want to find the okbutton as we did
before, and click it—but only if it exists. In Tcl we can achieve
this with the following code:
if {[object exists "myapp.frame1.okbutton"]} {
set button [waitForObject "myapp.frame1.okbutton"]
invoke clickButton $button
}
Using qualified object names with the waitForObject() function, means that test engineers
can query and interact with all the objects in the AUT's object
hierarchy.
Using the Tk script API it is possible to access almost all of Tk's widget properties.
For example, if we want to change the text in an entry
widget, we can do so using the following Tcl code, and of course,
substituting the qualified object name and the new text appropriately:
set entry [waitForObject "myapp.frame1.e1"] property set $entry text "New text" set text [property get $entry text] test log [toString $text]
The first two lines set the new text; the third line creates a new
variable, text, and the last line prints the
text. When test scripts print to stdout or to
stderr, the printed text appears in the Squish IDE's Runner Log;
here we've written to the Test Log instead.
Although Squish test scripts can access the Tk widget properties, this
is not sufficient for testing purposes, because not all the information
we want to query is available through these properties. Fortunately,
Squish provides a solution for this: the tcleval function. This function can execute
arbitrary Tcl code which is interpreted within the scope of the AUT.
For example, if we want to retrieve the contents of a Tk
text widget, we cannot do so through the widget's
properties since the text is not available as a property. What we can do
instead is to call the text widget's get
function, since this returns the text widget's text between
given indices. So to get the entire text we use indices 1.0 and
end. Here's how we can use the tcleval
function to call get on a text widget:
set text [invoke tcleval ".textfield get 1.0 end"]
Notice that the entire argument to tceval is passed as a
string. The “.textfield” is the name of the
text widget (recall that . is the root of the
widget hierarcy in pure Tcl/Tk).
This section provides a glimpse of the script API Squish offers on top of Tk to make it easy to perform common user actions such as clicking a button. Details of the full API are given in the Tk Convenience API (Section 16.1.5) section of the Reference Manual (Chapter 16). Here we will just show a few examples to give a taste of what the API offers and how to use it.
invoke clickButton "myapp.button1" invoke doubleClickItem "myapp.list1" "Banana" 0 0 0 1 invoke activateItem "myapp.filemenu" "Quit" invoke type "myapp.frame1.e1" "New text"
Here, we click a button, double click a list item with the given text, invoke a menu option, and enter some text. These are the most commonly used Tk convenience functions, although there are additional ones in the API.
One of Squish's most useful features is the ability to access the toolkit's API from test scripts. This gives test engineers great deal of flexibility and allows them to test just about anything in the AUT. With Squish's Web-specific API it is possible to find and query objects, access properties and methods, and evaluate arbitrary JavaScript code in the Web-application's context. In addition, Squish provides a convenience API (see How to Use the Web Convenience API (Section 15.1.5.6)) that provides facilities for executing common actions on Web sites such as clicking a button or entering some text.
A variety of examples that show how to use the scripting Web API to access and test complex Web elements is given in the How to Test Web Elements (Section 15.1.14) section.
Squish provides two functions, findObject()
and waitForObject(), that return a reference
to the object (HTML or DOM element), for a given qualified object name.
The difference between them is that waitForObject() waits for an object to become
available (up to its default timeout, or up to a specified timeout), so
it is usually the most convenient one to use. However, only
findObject() can be used on hidden objects.
See the Web Object API (Section 16.1.9) for full details of Squish's Web classes and methods.
There are several ways to indentify a particular Web object:
Multiple-property (real) names—These names
consist of a list of one or more
property–name/property–value pairs, separated by spaces if
there is more than one, and the whole name enclosed in curly braces.
Given a name of this kind, Squish will search the document's DOM tree
until it finds a matching object. An example of such a name is:
“{tagName='INPUT' id='r1' name='rg' form='myform'
type='radio' value='Radio 1'}”.
Single property value—Given a particular value,
Squish will search the document's DOM tree until it finds an object
whose id, name or innerText
property has the specified value.
Path—The full path to the element is given.
An example of such a path is
“DOCUMENT.HTML1.BODY1.FORM1.SELECT1”.
To find an object's name, you can use the Spy to introspect the Web application's document. See the How to Use the Spy (Section 15.2.4) section for details.
If we want to interact with a particular object—for example, to check its properties, or to do something to it, such as click it, we must start by getting a reference to the object.
If we use the findObject() function, it will
either return immediately with the object, or it will throw a script
error if the object isn't available. (An object might not be available
because it is an AJAX object that only appears under certain conditions,
or it might only appear as the result of some JavaScript code executing,
and so on.) Here's a Python code snippet that shows how to use findObject() without risking an error being thrown,
by using the object.exists() function:
radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}"
if object.exists(radioName):
radioButton = findObject(radioName)
clickButton(radioButton)
var radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}";
if (object.exists(radioName)) {
var radioButton = findObject(radioName);
clickButton(radioButton);
}
my $radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}"
if (object::exists($radioName)) {
my $radioButton = findObject($radioName);
clickButton($radioButton);
}
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:
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}")
clickButton(radioButton)
var radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
clickButton(radioButton);
my $radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
clickButton($radioButton);
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.
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:
div = findObject("{tagName='DIV' id='mydiv'}")
link = div.evaluateXPath("A[contains(@href, 'www.froglogic.com')]").snapshotItem(0)
var div = findObject("{tagName='DIV' id='mydiv'}");
var link = div.evaluateXPath("A[contains(@href, 'www.froglogic.com')]").snapshotItem(0);
my $div = findObject("{tagName='DIV' id='mydiv'}");
my $link = $div->evaluateXPath("A[contains(@href, 'www.froglogic.com')]")->snapshotItem(0);
set div [findObject {{tagName='DIV' id='mydiv'}}]
set link [invoke [invoke $div evaluateXPath "A[contains(@href, 'www.froglogic.com')]"] snapshotItem 0]
The XPath used here says, “find all A (anchor) tags
that have an href attribute, and whose value is
www.froglogic.com”. We then call the
snapshotItem() method and ask it to retrieve the first
match—it uses 0-based indexing—this is returned as an object
of type HTML_Object Class (Section 16.1.9.14).
Each XPath query can produce a boolean (true or false), a number, a
string, or a group of elements as the result. Consequently, the
HTML_Object.evaluateXPath()
method returns an object of type
HTML_XPathResult Class (Section 16.1.9.21) on which
you can query the result of the XPath evaluation.
How to Access Table Cell Contents (Section 15.1.14.6) has an example of
using the HTML_Object.evaluateXPath()
method to extract the contents of an HTML table's cell.
![]() | Tip |
|---|---|
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. |
Using the script API it is possible to access most of the DOM properties for any HTML or DOM element in a Web application. See the Web Object API (Section 16.1.9) for full details of Squish's Web classes and methods.
Here is an example where we will change and query the
text property of a form's text element.
entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}")
entry.text = "Some new text"
test.log(entry.text)
var entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
entry.text = "Some new text";
test.log(entry.text);
my $entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
$entry->text = "Some new text";
test::log($entry->text);
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:
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
test.compare(div.property("style.display"), "none")
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test.compare(div.property("style.display"), "none");
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test::compare($div->property("style.display"), "none");
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.
In addition to properties, you can call standard DOM functions on all Web objects from test scripts, using the API described in the Web Object API (Section 16.1.9).
For example, to get the first child node of a DIV element,
you could use the following test script which makes use of the
HTML_Object.firstChild() function:
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
child = div.firstChild()
test.log(child.tagName)
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
var child = div.firstChild();
test.log(child.tagName);
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
my $child = $div->firstChild();
test::log($child->tagName);
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"] set child [invoke $div firstChild] test log [property get $child tagName]
Or, to get the text of the selected option from a select form element, we could use the following JavaScript code:
element = findObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
option = element.optionAt(element.selectedIndex)
test.log(option.text)
var element = findObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
var option = element.optionAt(element.selectedIndex);
test.log(option.text);
my $element = findObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
my $option = $element->optionAt($element->selectedIndex);
test::log($option->text);
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:
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
div.invoke("myOwnFunction", "an argument")
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
div.invoke("myOwnFunction", "an argument");
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
$div->invoke("myOwnFunction", "an argument");
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"] invoke $div "myOwnFunction" "an argument"
Beyond the DOM API bindings and the invoke()
function, Squish offers a Browser object which can be
used by test scripts to query which browser is being used, as the
following Python snippet shows:
test.log("We are running in " + Browser.name()) # will print out the name of the browser
if Browser.id() == InternetExplorer:
...
elif Browser.id() == Mozilla:
...
elif Browser.id() == Firefox:
...
elif Browser.id() == Safari:
...
elif Browser.id() == Konqueror:
...
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:
style_display = evalJS("var d = document.getElementById('busyDIV'); d ? d.style.display : ''")
var style_display = evalJS("var d = document.getElementById('busyDIV'); d ? d.style.display : ''");
my $style_display = evalJS("var d = document.getElementById('busyDIV'); d ? d.style.display : ''");
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 : ''.
This section describes the script API Squish offers on top of the DOM API to make it easy to perform common user actions such as clicking a link, entering text, etc. All the functions provided by the API are listed in the Web Object API (Section 16.1.9) section in the Reference Manual (Chapter 16). Here we will show a few examples to illustrate how the API is used.
In the example below, we click a link, select an option, and enter some text.
clickLink(":{tagName='A' innerText='Advanced Search'}")
selectOption(":{tagName='INPUT' id='sel' form='myform' type='select-one'}", "Banana")
setText(":{tagName='INPUT' id='input' form='myform' type='text'}", "Some Text")
clickLink(":{tagName='A' innerText='Advanced Search'}");
selectOption(":{tagName='INPUT' id='sel' form='myform' type='select-one'}", "Banana");
setText(":{tagName='INPUT' id='input' form='myform' type='text'}", "Some Text");
clickLink(":{tagName='A' innerText='Advanced Search'}");
selectOption(":{tagName='INPUT' id='sel' form='myform' type='select-one'}", "Banana");
setText(":{tagName='INPUT' id='input' form='myform' type='text'}", "Some Text");
invoke clickLink ":{tagName='A' innerText='Advanced Search'}"
invoke selectOption ":{tagName='INPUT' id='sel' form='myform' type='select-one'}" "Banana"
invoke setText ":{tagName='INPUT' id='input' form='myform' type='text'}" "Some Text"
In these cases we identified the object using real (multi-property) names; we could just have easily used symbolic names, or even object references, instead. Note also that the full API contains far more functions than the three mentioned here, although all of them are just as easy to use.
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 button, we could ensure that the page is loaded before attempting to click the button, using the following code:
loaded = waitFor("isPageLoaded()", 5000)
if loaded:
clickButton(":{tagName='INPUT' type='button' value='Login'}")
else:
test.fatal("Page loading failed")
var loaded = waitFor("isPageLoaded()", 5000);
if (loaded)
clickButton(":{tagName='INPUT' type='button' value='Login'}");
else
test.fatal("Page loading failed");
my $loaded = waitFor("isPageLoaded()", 5000);
if ($loaded) {
clickButton(":{tagName='INPUT' type='button' value='Login'}");
}
else {
test::fatal("Page loading failed");
}
set loaded [waitFor "isPageLoaded()" 5000]
if {$loaded} {
invoke clickButton ":{tagName='INPUT' type='button' value='Login'}"
} else {
test fatal "Page loading failed"
}
Additionally, we can wait for any object to be ready using the
waitForObject() function:
login_button = waitForObject(":{tagName='INPUT' type='button' value='Login'}")
clickButton(login_button)
var login_button = waitForObject(":{tagName='INPUT' type='button' value='Login'}");
clickButton(login_button);
my $login_button = waitForObject(":{tagName='INPUT' type='button' value='Login'}");
clickButton($login_button);
set login_button [waitForObject ":{tagName='INPUT' type='button' value='Login'}"]
invoke clickButton $login_button
In advanced AJAX applications, waiting for a page to be loaded is often insufficient, since parts of the page will be loaded using asynchronous AJAX requests. In such cases we must take more sophisticated approaches to synchronization.
In many cases, simply waiting for a particular object to become
available will suffice, in which case we can just use the waitForObject() function.
But many AJAX toolkits refresh objects using AJAX requests in the background. In such situations it might be necessary to wait until the background loading has finished in addition to waiting for particular objects to become available.
When background loading is taking place, most Web toolkits display a visual cue—for example, a box which says "loading..."—to indicate to the user that the application is loading. We can use such a visual cue to synchronize our script by waiting until the loading cue has disappeared.
To show how to handle situations where AJAX is used for background loading we will develop an AJAX synchronization function for test scripts used for testing applications based on the Backbase AJAX toolkit. Backbase is just one of many Web toolkits that Squish supports, and although the example is specific to Backbase, it should translate for use with other toolkits without too much trouble.
Backbase uses a text box that displays the text "loading..." when loading is taking place, so we must develop a function that will tell us if loading is in progress:
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
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;
}
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;
}
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:
def synchBackbase():
loading = waitFor("isBackbaseLoading()", 2000)
if not loading:
return
waitFor("not isBackbaseLoading()")
function synchBackbase()
{
var loading = waitFor("isBackbaseLoading()", 2000);
if (!loading)
return;
waitFor("! isBackbaseLoading()");
}
sub synchBackbase
{
my $loading = waitFor("isBackbaseLoading()", 2000);
if (!$loading) {
return;
}
waitFor("! isBackbaseLoading()");
}
proc synchBackbase {} {
set loading [waitFor "isBackbaseLoading()" 2000]
if {!$loading} {
return;
}
waitFor "![invoke isBackbaseLoading]"
}
Here we wait for up to two seconds to see if loading is taking place,
and if it is, we then wait (indefinitely) for the loading to be
finished (i.e., for isBackbaseLoading() to return
false).
With the synchBackbase() function available, we can call it
just after every operation that could cause loading to take place. But
if we were to do that our code would end up littered with calls to
synchBackbase()—making it less clear—and also
it would be quite easy to forget to call it in some places. Fortunately,
Squish provides a nice solution to this problem. If we implement a
special function called waitUntilObjectReady(), Squish
will call it automatically from every waitForObject() call.
Here is a simple implementation that will ensure that background loading is always finished:
sub waitUntilObjectReady(obj):
synchBackbase()
function waitUntilObjectReady(obj)
{
synchBackbase();
}
sub waitUntilObjectReady
{
synchBackbase();
}
proc waitUntilObjectReady {obj} {
invoke synchBackbase
}
Now, whenever we call waitForObject(),
Squish will call our waitUntilObjectReady() function, and
this in turn will ensure that the AJAX application has finished loading.
This allows us to synchronize the test even if some of our test script's
actions cause asynchronous AJAX requests to take place.
We can put these functions into a shared script and include it in all of our test cases—this will allow us to use this advanced synchronization technique for all of our tests. (See also, How to Create and Use Shared Data and Shared Scripts (Section 15.4).)
Although the functions here are specific to the Backbase toolkit,
similar functions can be implemented for any other AJAX toolkit.
Squish's examples include the
examples/web/suite_examples/tst_backbase_pim
example which shows these functions in action. (See also
How to Create and Use Synchronization Points (Section 15.1.9).)
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 Java™ GUI 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).
Squish provides the waitForObject() function
which returns the object for a given qualified object name as soon as it
becomes available—for example, when it becomes visible. (For hidden
objects use the findObject() function
instead).
Squish supports three notations for identifying an object by name:
Symbolic name—these names are generated
algorithmically and used in the Squish Object Map (Section 16.9)
to make it easier to create tests that are robust in the face of changes
to the AUT's object hierarchy. These names are similar to hierarchical
names in that they begin with a colon and consist of one or more
period-separated texts—for example, :Payment
Form.Pay_javax.swing.JButton. Symbolic names are preferred for
test scripts (and are the ones Squish uses when recording scripts),
since they make test script maintainence easier. (See Editing an Object Map (Section 16.9.2.2) for more about handling object hierarchy changes.)
Multiple property name—a list of
property-name=value pairs in curly braces that uniquely identifies the
object. Squish will search the GUI
parent–child hierarchy until it finds a matching object. Here is
an example of such a name: {caption='Pay'
type='javax.swing.JButton' visible='true' window=':Payment
Form_PaymentForm'}. To be valid, a multi-property name (also
called a “real name”) must include a type
property and at least one other property. Notice also that in this
example another object was referred to (the window); and in
this case the reference used a symbolic name. In general using symbolic
names is more robust, but if we need to identify an object with a
variable property (for example, a caption that might change), then we
must use a multi-property name, since this naming scheme supports
wildcards. (See Improving Object Identification (Section 16.8) for more about wildcards.)
Hierarchical name—from the top Frame (or
Shell in SWT) the “path” to the object,
where all parent GUI elements are included with each
one separated by a period. Here is an example of such a name:
:frame0.JRootPane.null_layeredPane.null_contentPane.JLabel.
These names are supported for backwards compatibility but should not be
used in new tests.
To find the name of an object, you can use the Spy tool to introspect the AUT. See the How to Use the Spy (Section 15.2.4) section for details.
It is perfectly okay to use both real and symbolic names in tests. The most common scenario is to use symbolic names (often cut and pasted from the Object Map or from a recorded script), and to only use real names when the wildcard functionality is required.
To get a reference to an object you can use either a symbolic name or a
real (multi-property) name—or even a hierarchical name. The name
is passed to the waitForObject() function.
For example:
forenameTextField = waitForObject(":Address Book - Add.Forename:_javax.swing.JTextField")
There are four basic idioms that can be used to access objects. The
first is simply to use the waitForObject()
function as shown here. This is ideal for most situations where the
object in question is expected to be visible. For situations where the
object may not be visible (for example, an object on a Tab page widget
that isn't currently shown), or may not even exist (for example, an
object that is only created by the AUT in certain circumstances), there
are three approaches we can use, depending on our needs.
If we expect the object to be present and visible, but want to account for rare occasions when it isn't we can use code like this (using Python in this example):
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")
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");
}
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");
};
catch {
set textField [waitForObject ":Credit Card.Account Name:_javax.swing.JTextField"]
# here we can use the textField object reference
} result options
if {[dict get $options -code]} {
test::fail("Could not find the account name text field");
}
If we expect an object to be absent (for example, a button that should disappear in some situations), we can check like this (again using Python):
code = 'waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")'
test.exception(code, "Correctly didn't find the text field")
var code = 'waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")';
test.exception(code, "Correctly didn't find the text field");
my $code = 'waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")';
test::exception($code, "Correctly didn't find the text field");
set code = {[waitForObject ":Credit Card.Account Name:_javax.swing.JTextField"]}
test exception $code "Correctly didn't find the text field"
If we expect an object to be hidden but nonetheless, present (for
example, on a Tab page that isn't the current one), we can still access
it, but this time we cannot use the waitForObject() function—which only works for
visible objects—but instead must use the object.exists() function in conjunction with the
findObject() function:
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")
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");
}
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");
}
}
if {[object exists ":Credit Card.Account Name:_javax.swing.JTextField"]} {
set textField [findObject ":Credit Card.Account Name:_javax.swing.JTextField"]
if {![isNull $textField]} {
test pass "Correctly found the hidden object"
}
}
Using these techniques it is possible to query and access every object in the AUT's object hierarchy, including both visible and hidden objects.
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:
s = java_lang_String("A string")
var s = new java_lang_String("A string");
my $s = java_lang_String->new("A string"); # "old"-style
my $s = new java_lang_String("A string"); # "new"-style
set s [construct java_lang_String "A string"]
![]() | Note |
|---|---|
When referring to Java™ objects which are qualified by package names in Squish scripts, the normal periods (“.”) are replaced with underscores (“_”). This is done because period is not allowed in identifier names (and in some cases has a special meaning) in most of the scripting languages that Squish supports. |
The example below uses the calculator demo application as the AUT. The
tiny JavaScript test script changes the multiply button's text from
* to x:
button = waitForObject(":frame0.*_javax.swing.JButton")
button.setText("x")
var button = waitForObject(":frame0.*_javax.swing.JButton");
button.setText("x");
my $button = waitForObject(":frame0.*_javax.swing.JButton");
$button->setText("x");
set button [waitForObject ":frame0.*_javax.swing.JButton"] invoke $button setText "x"
It is also possible to call static functions. Here is an example that
uses Java™'s static Integer.parseInt(String s) function:
i = java_lang_Integer.parseInt("12")
var i = java_lang_Integer.parseInt("12");
my $i = java_lang_Integer::parseInt("12");
set i [invoke java_lang_Integer parseInt "12"]
Java™ objects can have fields (sometimes called properties). Public fields are accessible in Squish as the following example demonstrates:
point = java_awt_Point(5, 8) test.log(point.x)
var point = new java_awt_Point(5, 8); test.log(point.x);
my $point = java_awt_Point->new(5, 8); # "old"-style my $point = new java_awt_Point(5, 8); # "new"-style test::log($point->x);
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:
button = waitForObject(":frame0.*_javax.swing.JButton")
button.text = "New Text"
test.log(button.text)
var button = waitForObject(":frame0.*_javax.swing.JButton");
button.text = "New Text";
test.log(button.text);
my $button = waitForObject(":frame0.*_javax.swing.JButton");
$button->text = "New Text";
test::log($button->text);
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 |
|---|---|
These synthetic properties make it easier to add more verifications points to test scripts. (See How to Create and Use Verification Points (Section 15.3) for more details.) |
Squish knows only about a limited set of classes. If you get errors accessing a class method referring to a super class, then it is likely that this class is not wrapped as standard. (See Wrapping custom Java™ classes (Section 16.4.8) how to extend the set of known classes.) Note that Squish 4 automatically wraps all the classes used by the AUT so errors of this kind should no longer occur.
This section describes the script API Squish offers on top of Java™'s API to make it easy to perform common user actions such as clicking a button, entering text, etc. A complete list of the API is available in the Java™ Convenience API (Section 16.1.7) section in the Reference Manual (Chapter 16). Below are a few examples to give a flavor of how the API's functions are used.
clickButton(":frame0_Notepad$1")
type(":frame0_javax.swing.JTextArea", "Some text")
activateItem(":frame0_javax.swing.JMenuBar", "File")
activateItem(":frame0.File_javax.swing.JMenu", "Exit")
clickButton(":frame0_Notepad$1");
type(":frame0_javax.swing.JTextArea", "Some text");
activateItem(":frame0_javax.swing.JMenuBar", "File");
activateItem(":frame0.File_javax.swing.JMenu", "Exit");
clickButton(":frame0_Notepad$1");
type(":frame0_javax.swing.JTextArea", "Some text");
activateItem(":frame0_javax.swing.JMenuBar", "File");
activateItem(":frame0.File_javax.swing.JMenu", "Exit");
invoke clickButton ":frame0_Notepad$1" invoke type ":frame0_javax.swing.JTextArea" "Some text" invoke activateItem ":frame0_javax.swing.JMenuBar" "File" invoke activateItem ":frame0.File_javax.swing.JMenu" "Exit"
Here, we have used a Python script to click a button, type in some text, and activate a menu and a menu item. We could use exactly the same code in JavaScript or Perl, except that we would need to add a semi-colon at the end of each line. Many more examples are given later on in the manual—they cover both AWT/Swing and SWT, including interactions with many different widgets such as line edits, spinners, lists, tables, and trees—see How to Test Java™ Applications (Section 15.1.16).
The complete API contains a lot more functions than the three we have shown here. Note also, that the same API works for both AWT/Swing applications and for SWT applications—the only difference is that they have different widgets and different object names.
Some of the methods in the Java™ API return Arrays rather
than single objects. The number of elements in such an array is
accessible using the length property, and individual
elements can be accessed using the at() method
parameterized by the array index. Here is an example that lists the
JPanel's in a JTabbedPane:
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)))
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));
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");
}
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.
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.
lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName")
test.verify(lineedit.enabled)
test.compare(lineedit.text, "Jane")
test.log("Important note", "This is an important note about the test")
test.warning("Suspicious warning", "This test is incomplete and should be extended!")
var lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
test.verify(lineedit.enabled);
test.compare(lineedit.text, "John");
test.log("Important note", "This is an important note about the test");
test.warning("Suspicious warning", "This test is incomplete and should be extended!");
my $lineedit = waitForObject("Addressbook.ABCentralWidget1.FirstName");
test::verify($lineedit->enabled);
test::compare($lineedit->text, "Jane");
test::log("Important note", "This is an important note about the test");
test::warning("Suspicious warning", "This test is incomplete and should be extended!");
set lineedit [waitForObject "Addressbook.ABCentralWidget1.FirstName"] test verify [property get $lineedit enabled] test compare [property get $lineedit text] "John" test log "Important note" "This is an important note about the test" test warning "Suspicious warning" "This test is incomplete and should be extended!"
Both the test.log() and
test.warning() functions can be given either
one or two arguments, the first is the “message” text, and
the optional second argument can be used to provide additional details.
Many other test functions are available, including ones for verifying expected failures and expected exceptions, and various functions for writing messages to the test log. The complete API is documented in the Verification Functions (Section 16.1.3.7) section in the Reference Manual (Chapter 16).
In Squish test scripts it is possible to react to events that occur in the AUT. This can be useful, for example, to provide a test script response for when a dialog appears unexpectedly, such as an error message box. This can be done by registering an event handler function for a particular event and that should be called when that event occurs on a specified object, or on an object of a specified type, or for any object.
Event handler functions are registered by calling an installEventHandler() function. For a handler that
should apply to all the AUT's objects—that is, a global event
handler—just the event type and the handler function are passed as
arguments. For a handler that should apply to a particular object or to
all objects of a particular type, the object or type is passed as the
first argument, followed by the event type and the handler function. In
addition to standard toolkit events (such as Qt's
QKeyEvent), some Squish- and toolkit-specific generic
events are supported such as MessageBoxOpened and
Crash.
![]() | Squish/Web-specific |
|---|---|
For Squish/Web, event handler functions are always called with
no argument, rather than passed an object
(typically the object the event happened to). It is still possible to
access objects inside Squish/Web event handlers, but we must obtain
references to the objects ourselves, for example, using the |
In the following subsections we will look at example event handlers for all three cases.
When a message box pops up the MessageBoxOpened event
occurs. (In fact, the MessageBoxOpened event only applies
to the Squish/Java™, Squish/Qt, and Squish/Windows editions;
however, there are similar events for the other toolkits.) Like all such
events the test script will ignore the event, but we can register an
event handler function to be called whenever such events occur. It
doesn't really make sense to associate a global event like this with a
particular object or type, so it is usually handled by a global event
handler.
Here we will look at an example of creating and installing a handler for message boxes.
def handleMessageBox(messageBox):
test.log("MessageBox opened: '%s' - '%s'" % (messageBox.windowText, messageBox.text))
messageBox.close()
def main():
installEventHandler("MessageBoxOpened", "handleMessageBox")
...
function handleMessageBox(messageBox)
{
test.log("MessageBox opened: '" + messageBox.windowText + "' - '" + messageBox.text + "'");
messageBox.close();
}
function main()
{
installEventHandler("MessageBoxOpened", "handleMessageBox");
// ...
}
sub handleMessageBox
{
my $messageBox = shift @_;
test::log("MessageBox opened: '" . $messageBox->windowText . "' - '" . $messageBox->text + "'");
$messageBox->close();
}
sub main
{
installEventHandler("MessageBoxOpened", "handleMessageBox");
# ...
}
proc handleMessageBox {messageBox} {
test log [concat "MessageBox opened: '" [property get $messageBox windowText] "' - '" [property get $messageBox text] "'"]
invoke $messageBox close
}
proc main {} {
installEventHandler "MessageBoxOpened" "handleMessageBox"
# ...
}
Note that if we were using a similar Squish/Web event (e.g.,
ModalDialogOpened), the dialog would
not be passed as an argument, because Squish/Web
event handlers have no arguments.
Another special event is Crash. This is useful when we want
to install an event handler to be called when the AUT crashes—for
example, to do cleanups or to restart the AUT. (The Crash
event is supported by all Squish versions, except for Squish/Web.)
Here's an example:
def crashHandler():
test.log("Deleting lock files after AUT crash")
deleteLockFiles()
def main():
installEventHandler("Crash", "crashHandler")
...
function crashHandler()
{
test.log("Deleting lock files after AUT crash");
deleteLockFiles();
}
function main()
{
installEventHandler("Crash", "crashHandler");
// ...
}
sub crashHandler
{
test::log("Deleting lock files after AUT crash");
deleteLockFiles();
}
sub main
{
installEventHandler("Crash", "crashHandler");
# ...
}
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.
It is possible to set up an event handler that will respond to
particular types of events for all objects of a specified type. For
example, using Squish/Qt, we can install an event handler which is
always called when a QMouseEvent occurs on a
QCheckBox. This means that every time the event occurs,
that is, whenever any of the AUT's checkboxes is clicked, the event
handler is called. Here's an example:
def handleCheckBox(obj):
test.log("QCheckBox '%s' clicked" % objectName(obj))
def main():
installEventHandler("QCheckBox", "QMouseEvent", "handleCheckBox")
...
function handleCheckBox(obj) {
test.log("QCheckBox '" + objectName(obj) + "' clicked");
}
function main() {
installEventHandler("QCheckBox", "QMouseEvent", "handleCheckBox");
// ...
}
sub handleCheckBox
{
my $obj = shift @_;
test::log("QCheckBox '" . objectName($obj) . "' clicked");
}
sub main
{
installEventHandler("QCheckBox", "QMouseEvent", "handleCheckBox");
# ...
}
proc handleCheckBox {obj} {
test log [concat "QCheckBox '" [objectName $obj] "' clicked"]
}
proc main {} {
installEventHandler "QCheckBox" "QMouseEvent" "handleCheckBox"
# ...
}
Similar event handlers for similar events can be created using the other
toolkits that Squish supports, but recall that for Squish/Web, no
argument is passed to the event handler, so if we want to interact with
an object we must first obtain a reference to it (e.g., using the waitForObject() function.)
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:
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")
...
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");
// ...
}
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");
# ...
}
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.
When recording a script in Squish, the event recorder must ensure that
the AUT and the test script are synchronized. One way of achieving this
is for the recorder to automatically insert snooze() statements into the script. These
statements force the script to wait for a specified number of seconds
(which might be a fractional amount such as 2.5). This is necessary to
ensure that a script is replayed at the same speed as it was recorded.
For example, if the user waited for a window to pop up, the script will
wait for the same amount of time. This is important to prevent Squish
from running the AUT too fast for the AUT's toolkit to keep up.
Using snooze() statements is the simplest way
to synchronize the AUT and a test script. But in many cases, simply
waiting for a certain amount of time isn't sufficient. For example, if a
script is recorded on a fast machine and later replayed on a slow
machine the time waited by snooze() might
not be long enough.
Another way of synchronizing is to use waitForObject() statements instead of snooze() statements. If the waitForObject() function is used, before every
action that is recorded, a waitForObject()
statement will be recorded so that the object can be accessed. So on
replay, instead of waiting for a specific amount of time, Squish will
wait for the given object to exist and be accessible (i.e., visible).
Since using the waitForObject() function has
proved much more reliable than using the snooze(), it is the default method used when
recording new tests.
A third alternative is to use the waitFor()
function. This function waits until a given condition becomes true, or
optionally, until a specified time out expires. The condition can be
anything from a property to a complex script statement. Here is an
example that waits for a particular dialog to pop up, and logs a fatal
error if the dialog doesn't appear within 5 seconds:
ok = waitFor("object.exists('Addressbook.FileSave')", 5000)
if not ok:
test.fatal("Addressbook.FileSave dialog didn't appear")
var ok = waitFor("object.exists('Addressbook.FileSave')", 5000);
if (!ok)
test.fatal("Addressbook.FileSave dialog didn't appear");
my $ok = waitFor("object::exists('Addressbook.FileSave')", 5000);
if (!$ok) {
test::fatal("Addressbook.FileSave dialog didn't appear");
}
set ok [waitFor "object exists 'Addressbook.FileSave'" 5000]
if {!$ok} {
test fatal "Addressbook.FileSave dialog didn't appear"
}
Here is another example, this time one that will wait “forever” since no timeout is specified. So if the expected file doesn't exist and isn't created, the test script will be stuck:
waitFor("QFile.exists('addresses.tsv')")
waitFor("QFile.exists('addresses.tsv')");
waitFor("QFile::exists('addresses.tsv')");
waitFor "invoke QFile exists 'addresses.tsv'"
This last example waits up to 2 seconds for an button to become disabled:
button = waitForObject("Addressbook.ABCentralWidget1.AddButton")
ok = waitFor("button.enabled" 2000)
if ok == False:
test.fatal("Add button still enabled after 2 seconds")
var button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
var ok = waitFor("button.enabled" 2000);
if (ok == false)
test.fatal("Add button still enabled after 2 seconds");
my $button = waitForObject("Addressbook.ABCentralWidget1.AddButton");
my $ok = waitFor("$button->enabled" 2000);
if ($ok == 0) {
test::fatal("Add button still enabled after 2 seconds");
}
set button [waitForObject "Addressbook.ABCentralWidget1.AddButton"]
set ok [waitFor {property get $button enabled} 2000]
if {$ok == false} {
test fatal "Add button still enabled after 2 seconds"
}
These examples show different variations of synchronization points. As
the condition which is passed to the waitFor() function can be any script code which can
be evaluated, including function calls, there are no limits to
creating synchronization points.
More on synchronization for Web applications and advanced AJAX synchronization can be found at How to Synchronize Web Page Loading for Testing (Section 15.1.5.7).
Usually, a single application under test is specified for each test suite. This AUT is then executed and accessed by each test case. All the tutorials show this one test suite/one AUT approach, but in fact it is possible to start multiple applications and access and test all of them from within a single test suite. This makes it possible to test the interaction between different applications or between multiple instances of the same application. For example, being able to test multiple applications is essential for testing client/server systems.
Whenever an AUT is started a corresponding Application Context Functions (Section 16.1.3.10) object is created, and it is this object
that is used by Squish to provide access to the AUT. Squish allows
us to access the ApplicationContext object directly in
our code, and this means that we can query the AUT for information such
as the command line it was launched with, its current state, and so on.
This information can also be accessed when a single AUT is used by
making use of the context object returned by the defaultApplicationContext() function.
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 toolbar button (in the Test Suites view) to make the test suite's Settings editor view visible. Now, in the editor's "Application Under Test (AUT)" section, make sure that the checkbox is unchecked.
The function used to start an application is startApplication(). This function starts the given
application (assuming it is located in an application path—see
AUTs and Settings (Section 16.4)) using the given command line arguments
and returns the corresponding ApplicationContext object.
The application context object is a handle that refers to the
application.
Optionally, as the second and third parameters, a host and port can be
passed to the startApplication() function.
This way, the startApplication() function
will connect to the squishserver on the specified host and listen to
the specified port, instead of using the default host and port (as
specified in the Squish IDE's settings or on the squishrunner's command
line). This allows us to control multiple applications on multiple
computers from a single test script.
Special care must be taken if the application is using a different GUI
toolkit than the test suite's default toolkit. The global testSettings Object (Section 16.1.3.13) object allows us to set the configuration
of the toolkit wrapper on a per-AUT basis. See the testSettings.setWrappersForApplication() function
for details on how to do this.
If we run two or more AUTs within a test script, which one should test
code apply to? We can make one of the AUTs the “active”
application by using the setApplicationContext() function, passing an
ApplicationContext as the sole parameter. Once the call is
made, all script code applies to the active application—unless
another setApplicationContext() call is made
to change the active application. Note that whenever we call the startApplication() function, not only is the
application's ApplicationContext object returned, but the
application is automatically set to be the active application.
We can obtain a list of all the currently running AUTs'
ApplicationContext objects, by calling the applicationContextList() function. And we can
retrieve the context object of the active application by calling the
currentApplicationContext() function.
![]() | Note |
|---|---|
If you want to record and access applications which are started by the AUT itself, and not by Squish, see the Recording the Sub-Processes started by the AUT (Section 16.7.1) section. |
We will now look at some examples that show how to start multiple AUTs
and how to use ApplicationContext objects to query them.
We will take as an example a client/server chat system. The system has a
chat server called chatserver which must be running
for communication to take place, and two chat clients, one written in Qt
called chatclientqt, and the other written in Java
called chatclientjava.
In the test we will first start the chat server. Then we start two clients; these automatically connect to the chat server at startup. We will then type something into the message editor of the first client and check that the second client received the message.
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")
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");
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");
startApplication "chatserver" set client1 [startApplication "chatclientqt" "Qt"] set client2 [startApplication "chatclientjava" "Java"] setApplicationContext $client1 set editor [waitForObject "ChatWindow.messageEditor"] invoke type $editor "Message for client #2" setApplicationContext $client2 set msgView [waitForObject "ChatWindow.messageView"] test verify [string equal [property get $msgView text] "Message for client #2"]
We begin by starting each of the applications in turn, although we only
keep references to the client AUTs' ApplicationContext
objects since we don't directly access the server in the test. Once the
applications are running we make the first client the active AUT since
the active AUT is currently client2 since that was the AUT
started by the most recent startApplication()
call. Then we get a reference to the client's chat editor and type some
text into it. And at the end, we make the second client the active AUT,
get a reference to its chat editor (a different widget this time since
the toolkit is different—Java rather than Qt), and we compare the
second client's editor's text with the text we sent from the first
client. (For the Tcl version we prefer to use the test.verify() function rather than test.compare() because it is usually more
convenient.)
It is possible to use an ApplicationContext object to
retieve information about the AUT it refers to. The application context
of the AUT defined in the test suite settings can be retrieved using the
defaultApplicationContext()
function. When multiple AUTs are started there should not be any AUT
defined in the test suite settings—each AUT's context object can
be retrieved as the return value of the call to the startApplication() function which is used to start
the AUT, or from the applicationContextList()
function which returns all the AUTs' context objects.
The Application Context Functions (Section 16.1.3.10) section details the properties and
functions that are accessible from ApplicationContext
objects. Here are some examples.
ctx = defaultApplicationContext() test.log(ctx.commandLine) test.log(ctx.cwd)
var ctx = defaultApplicationContext(); test.log(ctx.commandLine); test.log(ctx.cwd);
my $ctx = defaultApplicationContext(); test::log($ctx->commandLine); test::log($ctx->cwd);
set ctx [defaultApplicationContext] test log [property get $ctx commandLine] test log [property get $ctx cwd]
Here we print the command line the AUT was invoked with and its current working directory—both are properties.
ctx = startApplication("myapp")
peakMemory = 0
while ctx.isRunning:
peakMemory = max(ctx.usedMemory, peakMemory)
if not ctx.isFrozen(20):
break
test.log(peakMemory)
ctx = startApplication("myapp");
var peakMemory = 0;
while (ctx.isRunning) {
peakMemory = Math.max(ctx.usedMemory, peakMemory);
if (!ctx.isFrozen(20))
break;
}
test.log(peakMemory);
my $ctx = startApplication("myapp");
my $peakMemory = 0;
while ($ctx->isRunning) {
if ($ctx->usedMemory > $peakMemory) {
$peakMemory = $ctx->usedMemory;
}
if (!$ctx->isFrozen(20)) {
last;
}
}
test::log($peakMemory)
Here we start an application called myapp and so
long as it is running we keep track of the maximum amount of memory it
is using. (The memoryUsage property is
only available in conjunction with the Squish memory module
add-on.) We break out of the loop if the application stops
running (in which case isRunning will be false), or if the
application becomes unresponsive (frozen), after waiting 20 seconds.
ctx = startApplication("myapp")
test.log("STDOUT", ctx.readStdout())
test.warning("STDERR", ctx.readStderr())
var ctx = startApplication("myapp");
test.log("STDOUT", ctx.readStdout());
test.warning("STDERR", ctx.readStderr());
my $ctx = startApplication("myapp");
test::log("STDOUT", $ctx->readStdout());
test::warning("STDERR", $ctx->readStderr());
set ctx [startApplication "myapp"] test log "STDOUT" [invoke $ctx readStdout] test warning "STDERR" [invoke $ctx readStderr]
Here we have added everything that the AUT has written to stdout and stderr to the test log, classifying all stderr messages as warnings.
In this section we will see how the Squish API makes it straightforward to check the values and states of individual widgets so that we can test our application's business rules.
As we saw in the tutorial, we can use Squish's recording facility to create tests. However, it is often useful to modify such tests, or create tests entirely from scratch in code, particularly when we want to test business rules that involves multiple widgets.
In general there is no need to test a widget's standard behavior. For example, if an unchecked two-valued checkbox isn't checked after being clicked, that's a bug in the toolkit not in our code. If such a case arose we may need to write a workaround (and write tests for it), but normally we don't write tests just to check that a widget behaves as documented. On the other hand, what we do want to test is whether our application provides the business rules we intended to build into it. Some tests concern individual widgets in isolation—for example, testing that a combobox contains the appropriate items. Other tests concern inter-widget dependencies and interactions. For example, if we have a group of "payment method" radio buttons, we will want to test that if the "cash" radio button is chosen the check and credit card-relevant widgets are all hidden.
Whether we are testing individual widgets or inter-widget dependencies
and interactions, we must first be able to identify the widgets we want
to test. Once identified we can then verify that they have the values and
are in the states that we expect. One way to identify a widget is to
record a test that involves its use and see what name Squish uses for
it. But the easiest way to identify a widget so that we can use it in
our test code is to use the How to Use the Spy (Section 15.2.4) tool. (See also
waitForObject().)
The purpose of this section is to explain and show how to access various Qt widgets and perform common operations using these widgets—such as getting and setting their properites—with the Perl, Python, JavaScript, and Tcl scripting languages.
After completing this section you should be able to access Qt widgets, gather data from those Qt widgets, and perform tests against expected values. The principles covered in this chapter apply to all Qt widgets, so even if you need to test a widget that isn't specifically mentioned here, you should have no problem doing so.
![]() | Note |
|---|---|
The first time you use any of the script examples that are referenced in this guide, you will need to register the application with the squish server. The test suite will know which application you are using, but the squish server will not have an entry for it, until it is registered. The easiest way to accomplish this is to change the settings on an existing test suite and press the button next to the application text field. You will now be able to find the application and register it with the squish server. |
To test and verify a widget and its properties or contents,
first we need access to the widget in the test script. To obtain a
reference to the widget, the waitForObject()
function is used. This function finds the widgets with the given name and
returns a reference to it.
For this purpose we need to know the name of the widget we want to test, and we can get the name using the How to Use the Spy (Section 15.2.4) tool.
The steps to find out the name are as follows:
Start the Squish IDE and make the test suite we are working in active
Start the Spy on the application under test
Switch the Spy in Pause mode
Switch to the AUT and work through the GUI until the widget we want to test is visible (e.g. open the dialog it is contained in)
Switch back to the Squish IDE and switch the Spy into Pick mode
Switch to the AUT and click on the widget you want to test
Switch back to the Squish IDE. In the Spy object view the selected widget and its tree will be displayed. Right-click onto the object name and choose "Copy to clipboard".
Exit the Spy
Now the object name we were looking for is saved in the
clipboard and we can paste it into the script as the argument to
waitForObject().
Each Qt widget has a set of properties and states associated with it that you can query with Squish to perform checks in your test scripts. These properties can be things like, focus (does the widget have focus), enabled (is this widget enabled), visible (is the widget visible), height (what is the height of the widget), width (what is the width of the widget), etc. All of these properties are documented on the Qt/Trolltech web site. Just pick the version of Qt you are running (for example: Qt 4.3 / Qtopia Core 4.3), click on , and then search for the Qt class name you are looking for.
For example, lets imagine you have a button in your application and you
used the Spy tool to discover that the Qt class name for this widget
is QPushButton. In
the section of the website,
search for QPushButton and select it. You will see
that this widget only has a few properties, however, there
are additional properties inherited from the QAbstractButton
class, and many more properties inherited from the QWidget class, and
one property inherited from the QObject class. By
visiting each of these parent classes, you will see all of the
properties that you can query with Squish in your test scripts. We
will see many examples of accessing and testing widget properties in the
following setions.
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.

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 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 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 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 QRadioButton is checked:
No irrelevant widgets (e.g., check date, check signed), must be visible. (In practice we only have to check that the card widget is visible and that the check and card widgets are hidden.)
The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.
For non-Visa cards the issue date must be no earlier than three years ago.
The expiry date must be at least one month later than today.
The account name and account number line edits must be nonempty.
We will write three tests, one for each of the form's modes. And to make
it slightly simpler to check the widgets in the QStackedWidget,
we have explicitly given them object names (using QObject's
setObjectName() method)—"CashWidget", "CheckWidget",
and "CardWidget". In the same way we have also given the name
"AmountDueLabel" to the QLabel that displays
the amount due.
The source code for the payment form is in the directory
SQUISHROOT/examples/qt4/paymentform, and the test
suites are in subdirectories underneath—for example, the Python
version of the tests is in the directory
SQUISHROOT/examples/qt4/paymentform/suite_py, and
the JavaScript version of the tests is in
SQUISHROOT/examples/qt4/paymentform/suite_js.
We will begin by reviewing the test script for testing the form's "cash" mode. First we will show the code, then we will explain it. (Don't worry that the code seems long—when we look at the next test script we will see how to break things down into managable pieces.)
Example 15.1. The tst_cash_mode Test Script
def main():
startApplication("paymentform")
# Make sure the Cash radio button is checked so we start in the mode
# we want to test
cashRadioButtonName = ("{text='Cash' type='QRadioButton' visible='1'"
"window=':Make Payment_MainWindow'}")
cashRadioButton = waitForObject(cashRadioButtonName)
if not cashRadioButton.checked:
clickButton(cashRadioButton)
test.compare(cashRadioButton.checked, True)
# Business rule #1: only the QStackedWidget's CashWidget must be
# visible in cash mode
# (The name "CashWidget" was set with QObject::setObjectName())
cashWidget = waitForObject("{name='CashWidget' type='QLabel'}")
test.compare(cashWidget.visible, True)
checkWidgetName = "{name='CheckWidget' type='QWidget'}"
# No waiting for a hidden object
checkWidget = findObject(checkWidgetName)
test.compare(checkWidget.visible, False)
cardWidgetName = "{name='CardWidget' type='QWidget'}"
# No waiting for a hidden object
cardWidget = findObject(cardWidgetName)
test.compare(cardWidget.visible, False)
# Business rule #2: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}")
chars = []
for char in unicode(amountDueLabel.text):
if char.isdigit():
chars.append(char)
amount_due = cast("".join(chars), int)
maximum = min(2000, amount_due)
paymentSpinBoxName = ("{buddy=':Make Payment.This Payment:_QLabel'"
"type='QSpinBox' unnamed='1' visible='1'}")
paymentSpinBox = waitForObject(paymentSpinBoxName)
test.verify(paymentSpinBox.minimum == 1)
test.verify(paymentSpinBox.maximum == maximum)
# Business rule #3: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
payButtonName = ("{type='QPushButton' text='Pay' unnamed='1'"
"visible='1'}")
payButton = waitForObject(payButtonName)
test.compare(payButton.enabled, True)
function main()
{
startApplication("paymentform");
// Make sure the Cash radio button is checked so we start in the mode
// we want to test
var cashRadioButtonName = "{text='Cash' type='QRadioButton' visible='1'" +
"window=':Make Payment_MainWindow'}";
var cashRadioButton = waitForObject(cashRadioButtonName);
if (!cashRadioButton.checked) {
clickButton(cashRadioButton);
}
test.compare(cashRadioButton.checked, true);
// Business rule #1: only the QStackedWidget's CashWidget must be
// visible in cash mode
// (The name "CashWidget" was set with QObject::setObjectName())
var cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");
test.compare(cashWidget.visible, true);
var checkWidgetName = "{name='CheckWidget' type='QWidget'}";
// No waiting for a hidden object
var checkWidget = findObject(checkWidgetName);
test.compare(checkWidget.visible, false);
var cardWidgetName = "{name='CardWidget' type='QWidget'}";
// No waiting for a hidden object
cardWidget = findObject(cardWidgetName);
test.compare(cardWidget.visible, false);
// Business rule #2: the minimum payment is $1 and the maximum is
// $2000 or the amount due whichever is smaller
var amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
var chars = [];
var amountDueText = new String(amountDueLabel.text);
for (var i = 0; i < amountDueText.length; ++i) {
var ch = amountDueText.charAt(i);
if ("0123456789".indexOf(ch) > -1) {
chars.push(ch);
}
}
var amount_due = parseFloat(chars.join(""));
var maximum = Math.min(2000, amount_due);
var paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" +
"type='QSpinBox' unnamed='1' visible='1'}";
var paymentSpinBox = waitForObject(paymentSpinBoxName);
test.verify(paymentSpinBox.minimum == 1);
test.verify(paymentSpinBox.maximum == maximum);
// Business rule #3: the Pay button is enabled (since the above tests
// ensure that the payment amount is in range)
var payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" +
"visible='1'}";
var payButton = waitForObject(payButtonName);
test.compare(payButton.enabled, true);
}
sub main
{
startApplication("paymentform");
# Make sure the Cash radio button is checked so we start in the mode
# we want to test
my $cashRadioButtonName = "{text='Cash' type='QRadioButton' " .
"visible='1'window=':Make Payment_MainWindow'}";
my $cashRadioButton = waitForObject($cashRadioButtonName);
if (!$cashRadioButton->checked) {
clickButton($cashRadioButton);
}
test::compare($cashRadioButton->checked, 1);
# Business rule #1: only the QStackedWidget's CashWidget must be
# visible in cash mode
# (The name "CashWidget" was set with QObject::setObjectName())
my $cashWidget = waitForObject("{name='CashWidget' type='QLabel'}");
test::compare($cashWidget->visible, 1);
$checkWidgetName = "{name='CheckWidget' type='QWidget'}";
# No waiting for a hidden object
my $checkWidget = findObject($checkWidgetName);
test::compare($checkWidget->visible, 0);
my $cardWidgetName = "{name='CardWidget' type='QWidget'}";
# No waiting for a hidden object
my $cardWidget = findObject($cardWidgetName);
test::compare($cardWidget->visible, 0);
# Business rule #2: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
my $amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
my $amount_due = $amountDueLabel->text;
$amount_due =~ s/\D//g; # remove non-digits
my $maximum = 2000 < $amount_due ? 2000 : $amount_due;
my $paymentSpinBoxName = "{buddy=':Make Payment.This Payment:_QLabel'" .
"type='QSpinBox' unnamed='1' visible='1'}";
my $paymentSpinBox = waitForObject($paymentSpinBoxName);
test::verify($paymentSpinBox->minimum == 1);
test::verify($paymentSpinBox->maximum == $maximum);
# Business rule #3: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
my $payButtonName = "{type='QPushButton' text='Pay' unnamed='1'" .
"visible='1'}";
my $payButton = waitForObject($payButtonName);
test::compare($payButton->enabled, 1);
}
proc main {} {
startApplication "paymentform"
# Make sure the Cash radio button is checked so we start in the mode
# we want to test
set cashRadioButtonName {{text='Cash' type='QRadioButton' visible='1'
window=':Make Payment_MainWindow'}}
set cashRadioButton [waitForObject $cashRadioButtonName]
if {![property get $cashRadioButton checked]} {
invoke clickButton $cashRadioButton
}
test compare [property get $cashRadioButton checked] true
# Business rule #1: only the QStackedWidget's CashWidget must be
# visible in cash mode
# (The name "CashWidget" was set with QObject::setObjectName())
set cashWidget [waitForObject {{name='CashWidget' type='QLabel'}}]
test compare [property get $cashWidget visible] true
set checkWidgetName {{name='CheckWidget' type='QWidget'}}
# No waiting for a hidden object
set checkWidget [findObject $checkWidgetName]
test compare [property get $checkWidget visible] false
set cardWidgetName {{name='CardWidget' type='QWidget'}}
# No waiting for a hidden object
set cardWidget [findObject $cardWidgetName]
test compare [property get $cardWidget visible] false
# Business rule #2: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
set amountDueLabel [waitForObject {{name='AmountDueLabel' type='QLabel'}}]
set amountText [toString [property get $amountDueLabel text]]
regsub -all {\D} $amountText "" amountText
set amount_due [expr $amountText]
set maximum [expr $amount_due < 2000 ? $amount_due : 2000]
set paymentSpinBoxName {{buddy=':Make Payment.This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}}
set paymentSpinBox [waitForObject $paymentSpinBoxName]
test compare [property get $paymentSpinBox minimum] 1
test compare [property get $paymentSpinBox maximum] $maximum
# Business rule #3: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
set payButtonName {{type='QPushButton' text='Pay' unnamed='1'
visible='1'}}
set payButton [waitForObject $payButtonName]
test compare [property get $payButton enabled] true
}
We must start by making sure that the form is in the mode we want to
test. To access visible widgets the process is always the same: we
create a variable holding the widget's property-based (real) name, then
we call waitForObject() to get a reference to
the widget. Once we have the reference we can use it to access the
widget's properties and to call the widget's methods. We use this
approach to see if the cash radio button is checked, and if it is not,
we click it. In either case we then use the test.compare() method to confirm that the cash
radio button is checked and ensure that we do the rest of the tests with
the form in the correct mode.
Note that the clickButton() function can be
used to click any button that inherits QAbstractButton,
that is, QCheckBox, QPushButton,
QRadioButton,
and QToolButton.
The first business rule to be tested is that if the cash widget is
visible, the check and card widgets must be hidden. Checking that a
widget is visible is easily done by accessing the widget's
visible property, and follows exactly the same pattern as
we used to access the checked property. But for hidden
widgets, the approach is slightly different—we do not (and must
not) call waitForObject(); instead we call
findObject() immediately. We can use a
similar approach to checking that a particular tab page widget in a
QTabWidget
or particular item widget in a QToolBox is
visible.
The second business rule concerns the minimum and maximum allowed
payment amounts. As usual we begin by using waitForObject() to get references to the widgets we
want—in this case starting with the amount due label. This label's
text might contain a currency symbol and grouping markers (for example,
$1,700 or €1.700), so to convert this into an integer we must strip
away any non-digit characters first. We do this in different ways
depending on the underlying scripting language, but in all cases we
retrieve the label's text property's characters and convert
them to an integer. (For example, in Python, we iterate over each
character and join all those that are digits into a single string and
use the cast() function which takes an object
and the type the object should be converted to, and returns an object of
the requested type—or 0 on failure. We use a similar approach in
JavaScript, but for Perl and Tcl we simply replace non-digit characters
using a regular expression.) The resulting integer is the amount due, so
we can now trivially calculate the maximum amount that can be paid in
cash.
With the minimum and maximum amounts known we next get a reference to
the payment spinbox. (Notice how the spinbox has no name, but is
uniquely identified by its buddy—the label beside it.) Once we
have a reference to the spinbox we use the test.verify() method to ensure that it has the
correct minimum and maximum amounts set. (For Tcl we have used the test.compare() method instead of test.verify() since it is more convenient to do so.)
Checking the last business rule is easy in this case since if the amount
is in range (and it must be because we have just checked it), then
payment is allowed so the 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
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 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 button.
Although the "cash" mode test works well, there are a few places where we
use essentially the same code. So before creating the test for "check"
mode, we will create some common functions that we can use to refactor
our tests with. (The process used to create shared code is described a
little later in How to Create and Use Shared Data and Shared Scripts (Section 15.4)—essentially all we
need to do is create a new script under the Test Suite's shared item's
scripts item.) The Python common code is in
common.py, the JavaScript common code is in
common.js, and so on. We will also create some
test-specific functions to make the main() function smaller
and easier to understand—and we will put these functions in the
test.py file (or test.js and
so on) above the main() function.
Example 15.2. The Shared Code
def clickRadioButton(text):
radioButton = waitForObject("{text='%s' type='QRadioButton' visible='1'"
"window=':Make Payment_MainWindow'}" % text)
if not radioButton.checked:
clickButton(radioButton)
test.compare(radioButton.checked, True)
def getAmountDue():
amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}")
chars = []
for char in unicode(amountDueLabel.text):
if char.isdigit():
chars.append(char)
return cast("".join(chars), int)
def checkVisibleWidget(visible, hidden):
widget = waitForObject("{name='%s' type='QWidget'}" % visible)
test.compare(widget.visible, True)
for name in hidden:
widget = findObject("{name='%s' type='QWidget'}" % name)
test.compare(widget.visible, False)
def checkPaymentRange(minimum, maximum):
paymentSpinBox = waitForObject("{buddy=':Make Payment.This Payment:_QLabel' "
"type='QSpinBox' unnamed='1' visible='1'}")
test.verify(paymentSpinBox.minimum == minimum)
test.verify(paymentSpinBox.maximum == maximum)
function clickRadioButton(text)
{
var radioButton = waitForObject("{text='" + text + "' type='QRadioButton' " +
"visible='1' window=':Make Payment_MainWindow'}");
if (!radioButton.checked) {
clickButton(radioButton);
}
test.compare(radioButton.checked, true);
}
function getAmountDue()
{
var amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
var chars = [];
var amountDueText = new String(amountDueLabel.text);
for (var i = 0; i < amountDueText.length; ++i) {
var ch = amountDueText.charAt(i);
if ("0123456789".indexOf(ch) > -1) {
chars.push(ch);
}
}
return parseFloat(chars.join(""));
}
function checkVisibleWidget(visible, hidden)
{
var widget = waitForObject("{name='" + visible + "' type='QWidget'}");
test.compare(widget.visible, true);
for (var i = 0; i < hidden.length; ++i) {
var name = hidden[i];
var widget = findObject("{name='" + name + "' type='QWidget'}");
test.compare(widget.visible, false);
}
}
function checkPaymentRange(minimum, maximum)
{
var paymentSpinBox = waitForObject("{buddy=':Make Payment." +
"This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}");
test.verify(paymentSpinBox.minimum == minimum);
test.verify(paymentSpinBox.maximum == maximum);
}
sub clickRadioButton
{
my $text = shift(@_);
my $radioButton = waitForObject("{text='$text' type='QRadioButton' " .
"visible='1' window=':Make Payment_MainWindow'}");
if (!$radioButton->checked) {
clickButton($radioButton);
}
test::compare($radioButton->checked, 1);
}
sub getAmountDue
{
my $amountDueLabel = waitForObject("{name='AmountDueLabel' type='QLabel'}");
my $amount_due = $amountDueLabel->text;
$amount_due =~ s/\D//g; # remove non-digits
return $amount_due;
}
sub checkVisibleWidget
{
my ($visible, @hidden) = @_;
my $widget = waitForObject("{name='$visible' type='QWidget'}");
test::compare($widget->visible, 1);
foreach (@hidden) {
my $widget = findObject("{name='$_' type='QWidget'}");
test::compare($widget->visible, 0);
}
}
sub checkPaymentRange
{
my ($minimum, $maximum) = @_;
my $paymentSpinBox = waitForObject("{buddy=':Make Payment." .
"This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}");
test::verify($paymentSpinBox->minimum == $minimum);
test::verify($paymentSpinBox->maximum == $maximum);
}
proc clickRadioButton {text} {
set radioButton [waitForObject "{text='$text' type='QRadioButton' visible='1' window=':Make Payment_MainWindow'}"]
if (![property get $radioButton checked]) {
invoke clickButton $radioButton
}
test compare [property get $radioButton checked] true
}
proc getAmountDue {} {
set amountDueLabel [waitForObject {{name='AmountDueLabel' type='QLabel'}}]
set amountText [toString [property get $amountDueLabel text]]
regsub -all {\D} $amountText "" amountText
return [expr $amountText]
}
proc checkVisibleWidget {visible hidden} {
set widget [waitForObject "{name='$visible' type='QWidget'}"]
test compare [property get $widget visible] true
foreach name $hidden {
set widget [findObject "{name='$name' type='QWidget'}"]
test compare [property get $widget visible] false
}
}
proc checkPaymentRange {minimum maximum} {
set paymentSpinBox [waitForObject {{buddy=':Make Payment.This Payment:_QLabel' type='QSpinBox' unnamed='1' visible='1'}}]
test compare [property get $paymentSpinBox minimum] $minimum
test compare [property get $paymentSpinBox maximum] $maximum
}
The clickRadioButton function is used to click the radio
button with the given text—this is used to set the correct page in
the widget stack. The getAmoutDue() function reads the text
from the amount due label, strips out formatting characters (e.g.,
commas), and converts the result to an integer. The
checkVisibleWidget() function checks that the visible
widget is visible and that the hidden widgets are not visible. One
subtle point is that for visible widgets we must always use the
waitForObject() function but for hidden
widgets we must not use it but rather use the
findObject() function instead.
Finally, the checkPaymentRange() function checks that the
payment spinbox's range matches the range we expect it to have.
Now we can write our test for "check" mode and put more of our effort
into testing the business rules and less into some of the basic chores.
The code we have put in the test.py (or
test.js, and so on) file is broken down into
several functions. The main() function is special for
Squish—this function is the only function
that Squish calls in a test, so we are free to add other functions, as
we have done here, to make our main function clearer.
We will first show the main() function, and then we will
show the functions it calls that are in the same
test.py file (since we have already seen the
functions that are called from common.py above).
Note that in the actual files, the main() function is last
but we prefer to show it first for ease of explanation.
Example 15.3. The tst_check_mode Test Script's main() function
def main():
startApplication("paymentform")
# Import functionality needed by more than one test script
source(findFile("scripts", "common.py"))
# Make sure we start in the mode we want to test: check mode
clickRadioButton("Check")
# Business rule #1: only the CheckWidget must be visible in check mode
checkVisibleWidget("CheckWidget", ("CashWidget", "CardWidget"))
# Business rule #2: the minimum payment is $10 and the maximum is
# $250 or the amount due whichever is smaller
amount_due = getAmountDue()
checkPaymentRange(10, min(250, amount_due))
# Business rule #3: the check date must be no earlier than 30 days
# ago and no later than tomorrow
today = QDate.currentDate()
checkDateRange(today.addDays(-30), today.addDays(1))
# Business rule #4: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'"
"visible='1'}")
test.compare(payButton.enabled, False)
# Business rule #5: the check must be signed (and if it isn't we
# will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked()
# Business rule #6: the Pay button should be enabled since all the
# previous tests pass, the check is signed and now we have filled in
# the account details
populateCheckFields()
payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'"
"visible='1'}")
test.compare(payButton.enabled, True)
function main()
{
startApplication("paymentform");
// Import functionality needed by more than one test script
source(findFile("scripts", "common.js"));
// Make sure we start in the mode we want to test: check mode
clickRadioButton("Check");
// Business rule #1: only the CheckWidget must be visible in check mode
checkVisibleWidget("CheckWidget", ["CashWidget", "CardWidget"]);
// Business rule #2: the minimum payment is $10 and the maximum is
// $250 or the amount due whichever is smaller
var amount_due = getAmountDue();
checkPaymentRange(10, Math.min(250, amount_due));
// Business rule #3: the check date must be no earlier than 30 days
// ago and no later than tomorrow
var today = QDate.currentDate();
checkDateRange(today.addDays(-30), today.addDays(1));
// Business rule #4: the Pay button is disabled (since the form's data
// isn't yet valid), so we use findObject() without waiting
var payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'" +
"visible='1'}");
test.compare(payButton.enabled, false);
// Business rule #5: the check must be signed (and if it isn't we
// will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked();
// Business rule #6: the Pay button should be enabled since all the
// previous tests pass, the check is signed and now we have filled in
// the account details
populateCheckFields();
payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'" +
"visible='1'}");
test.compare(payButton.enabled, true);
}
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);
}
proc main {} {
startApplication "paymentform"
# Import functionality needed by more than one test script
source [findFile "scripts" "common.tcl"]
# Make sure we start in the mode we want to test: check mode
clickRadioButton "Check"
# Business rule #1: only the CheckWidget must be visible in check mode
checkVisibleWidget "CheckWidget" {"CashWidget" "CardWidget"}
# Business rule #2: the minimum payment is $10 and the maximum is
# $250 or the amount due whichever is smaller
set amount_due [getAmountDue]
set maximum [expr 250 > $amount_due ? $amount_due : 250]
checkPaymentRange 10 $maximum
# Business rule #3: the check date must be no earlier than 30 days
# ago and no later than tomorrow
set today [invoke QDate currentDate]
set thirtyDaysAgo [toString [invoke $today addDays -30]]
set tomorrow [toString [invoke $today addDays 1]]
checkDateRange $thirtyDaysAgo $tomorrow
# Business rule #4: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
set payButton [findObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
test compare [property get $payButton enabled] false
# Business rule #5: the check must be signed (and if it isn't we
# will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked
# Business rule #6: the Pay button should be enabled since all the
# previous tests pass, the check is signed and now we have filled in
# the account details
populateCheckFields
set payButton [waitForObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
test compare [property get $payButton enabled] true
}
The source() function is used to read in a
script and execute it. Normally such a script is used purely to define
things—for example, functions—and these then become
available to the test script.
Getting the form into the right mode is now a one-liner thanks to our
custom clickRadioButton() function.
All the business rules are similar to before, but in each case the code
to test the rule has been reduced to one or two lines thanks to our use
of common functions (clickRadioButton(),
checkVisibleWidget(), getAmoutDue(), and
checkPaymentRange()), and the use of test-specific
functions (checkDateRange(),
populateCheckFields(), and
ensureSignedCheckBoxIsChecked()).
Example 15.4. The tst_check_mode Test Script's other functions
def checkDateRange(minimum, maximum):
checkDateEdit = waitForObject("{buddy=':Make Payment.Check Date:_QLabel' "
"type='QDateEdit' unnamed='1' visible='1'}")
test.verify(checkDateEdit.minimumDate == minimum)
test.verify(checkDateEdit.maximumDate == maximum)
def ensureSignedCheckBoxIsChecked():
checkSignedCheckBox = waitForObject("{text='Check Signed' type='QCheckBox' "
"unnamed='1' visible='0' window=':Make Payment_MainWindow'}")
if not checkSignedCheckBox.checked:
clickButton(checkSignedCheckBox)
test.compare(checkSignedCheckBox.checked, True)
def populateCheckFields():
bankNameLineEdit = waitForObject("{buddy=':Make Payment.Bank Name:_QLabel' "
"type='QLineEdit' unnamed='1' visible='1'}")
type(bankNameLineEdit, "A Bank")
bankNumberLineEdit = waitForObject(
"{buddy=':Make Payment.Bank Number:_QLabel' type='QLineEdit' "
"unnamed='1' visible='1'}")
type(bankNumberLineEdit, "88-91-33X")
accountNameLineEdit = waitForObject(
"{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' "
"unnamed='1' visible='1'}")
type(accountNameLineEdit, "An Account")
accountNumberLineEdit = waitForObject(
"{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' "
"unnamed='1' visible='1'}")
type(accountNumberLineEdit, "932745395")
function checkDateRange(minimum, maximum)
{
var checkDateEdit = waitForObject("{buddy=':Make Payment." +
"Check Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}");
test.verify(checkDateEdit.minimumDate == minimum);
test.verify(checkDateEdit.maximumDate == maximum);
}
function ensureSignedCheckBoxIsChecked()
{
var checkSignedCheckBox = waitForObject("{text='Check Signed' " +
"type='QCheckBox' unnamed='1' visible='0' " +
"window=':Make Payment_MainWindow'}");
if (!checkSignedCheckBox.checked) {
clickButton(checkSignedCheckBox);
}
test.compare(checkSignedCheckBox.checked, true);
}
function populateCheckFields()
{
var bankNameLineEdit = waitForObject("{buddy=':Make Payment." +
"Bank Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
type(bankNameLineEdit, "A Bank");
var bankNumberLineEdit = waitForObject("{buddy=':Make Payment." +
"Bank Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
type(bankNumberLineEdit, "88-91-33X");
var accountNameLineEdit = waitForObject("{buddy=':Make Payment." +
"Account Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
type(accountNameLineEdit, "An Account");
var accountNumberLineEdit = waitForObject("{buddy=':Make Payment." +
"Account Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
type(accountNumberLineEdit, "932745395");
}
sub checkDateRange
{
my ($minimum, $maximum) = @_;
$checkDateEdit = waitForObject("{buddy=':Make Payment.Check Date:_QLabel' " .
"type='QDateEdit' unnamed='1' visible='1'}");
test::verify($checkDateEdit->minimumDate == $minimum);
test::verify($checkDateEdit->maximumDate == $maximum);
}
sub ensureSignedCheckBoxIsChecked
{
my $checkSignedCheckBox = waitForObject("{text='Check Signed' " .
"type='QCheckBox' unnamed='1' visible='0' " .
"window=':Make Payment_MainWindow'}");
if (!$checkSignedCheckBox->checked) {
clickButton($checkSignedCheckBox);
}
test::compare($checkSignedCheckBox->checked, 1);
}
sub populateCheckFields
{
my $bankNameLineEdit = waitForObject("{buddy=':Make Payment." .
"Bank Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}");
type($bankNameLineEdit, "A Bank");
my $bankNumberLineEdit = waitForObject(
"{buddy=':Make Payment.Bank Number:_QLabel' type='QLineEdit' " .
"unnamed='1' visible='1'}");
type($bankNumberLineEdit, "88-91-33X");
my $accountNameLineEdit = waitForObject(
"{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' " .
"unnamed='1' visible='1'}");
type($accountNameLineEdit, "An Account");
my $accountNumberLineEdit = waitForObject(
"{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' " .
"unnamed='1' visible='1'}");
type($accountNumberLineEdit, "932745395");
}
proc checkDateRange {minimum maximum} {
set checkDateEdit [waitForObject {{buddy=':Make Payment.Check Date:_QLabel' type='QDateEdit' unnamed='1' visible='1'}}]
set minimumDate [toString [property get $checkDateEdit minimumDate]]
set maximumDate [toString [property get $checkDateEdit maximumDate]]
test verify [string equal $minimum $minimumDate]
test verify [string equal $maximum $maximumDate]
}
proc ensureSignedCheckBoxIsChecked {} {
set checkSignedCheckBox [waitForObject {{text='Check Signed' type='QCheckBox' unnamed='1' visible='0' window=':Make Payment_MainWindow'}}]
if (![property get $checkSignedCheckBox checked]) {
invoke clickButton $checkSignedCheckBox
}
test compare [property get $checkSignedCheckBox checked] true
}
proc populateCheckFields {} {
set bankNameLineEdit [waitForObject {{buddy=':Make Payment.Bank Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
invoke type $bankNameLineEdit "A Bank"
set bankNumberLineEdit [waitForObject {{buddy=':Make Payment.Bank Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
invoke type $bankNumberLineEdit "88-91-33X"
set accountNameLineEdit [waitForObject {{buddy=':Make Payment.Account Name:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
invoke type $accountNameLineEdit "An Account"
set accountNumberLineEdit [waitForObject {{buddy=':Make Payment.Account Number:_QLabel' type='QLineEdit' unnamed='1' visible='1'}}]
invoke type $accountNumberLineEdit "932745395"
}
The checkDateRange() function shows how we can test the
properties of a QDateEdit. (Note
for Tcl users: we have compared dates by converting them to strings.)
The ensureSignedCheckBoxIsChecked() function checks the
checkbox if it isn't already checked—and then it checks that the
checkbox is checked.
The populateCheckFields() function uses the type() function to simulate the user entering text.
It is almost always better to simulate user interaction than to set
widget properties directly—after all, it is the application's
behavior as experienced by the user that we normally need to test.
Once the fields are populated the button
should be enabled, and this is checked in the main()
function's business rule six after calling the
populateCheckFields() function.
Another point to note is that in this form we have two unnamed line
edits both with the label "Account Name", and two other's with the label
"Account Number". Squish is able to distinguish them because only one
of each is visible at any one time. We could of course use
setObjectName() to give them unique names if we wanted to.
We are now ready to look at the last test of the form's business
logic—the test of "card" mode. Just as with "check" mode we have
shortened and simplified the main() function by using
functions defined in the common.py (or
common.js, and so on) file and by using
test-specific functions in the test.py file (or
test.js and so on).
Example 15.5. The tst_card_mode Test Script's main() function
def main():
startApplication("paymentform")
source(findFile("scripts", "common.py"))
# Make sure we start in the mode we want to test: card mode
clickRadioButton("Credit Card")
# Business rule #1: only the CardWidget must be visible in check mode
checkVisibleWidget("CardWidget", ("CashWidget", "CheckWidget"))
# Business rule #2: the minimum payment is $10 or 5% of the amount due
# whichever is larger and the maximum is $5000 or the amount due
# whichever is smaller
amount_due = getAmountDue()
checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))
# Business rule #3: for non-Visa cards the issue date must be no
# earlier than 3 years ago
# Business rule #4: the expiry date must be at least a month later
# than today---we will make sure this is the case for the later tests
checkCardDateEdits()
# Business rule #5: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
payButton = findObject("{type='QPushButton' text='Pay' unnamed='1'}")
test.compare(payButton.enabled, False)
# Business rule #6: the Pay button should be enabled since all the
# previous tests pass, and now we have filled in the account details
populateCardFields()
payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'"
"visible='1'}")
test.compare(payButton.enabled, True)
function main()
{
startApplication("paymentform");
source(findFile("scripts", "common.js"));
// Make sure we start in the mode we want to test: card mode
clickRadioButton("Credit Card");
// Business rule #1: only the CardWidget must be visible in check mode
checkVisibleWidget("CardWidget", ["CashWidget", "CheckWidget"]);
// Business rule #2: the minimum payment is $10 or 5% of the amount due
// whichever is larger and the maximum is $5000 or the amount due
// whichever is smaller
var amount_due = getAmountDue();
checkPaymentRange(Math.max(10, amount_due / 20.0), Math.min(5000, amount_due));
// Business rule #3: for non-Visa cards the issue date must be no
// earlier than 3 years ago
// Business rule #4: the expiry date must be at least a month later
// than today---we will make sure this is the case for the later tests
checkCardDateEdits();
// Business rule #5: the Pay button is disabled (since the form's data
// isn't yet valid), so we use findObject() without waiting
var payButton = findObject("{type='QPushButton' text='Pay' " +
"unnamed='1' visible='1'}");
test.compare(payButton.enabled, false);
// Business rule #6: the Pay button should be enabled since all the
// previous tests pass, and now we have filled in the account details
populateCardFields();
payButton = waitForObject("{type='QPushButton' text='Pay' unnamed='1'" +
"visible='1'}");
test.compare(payButton.enabled, true);
}
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);
}
proc main {} {
startApplication "paymentform"
source [findFile "scripts" "common.tcl"]
# Make sure we start in the mode we want to test: card mode
clickRadioButton "Credit Card"
# Business rule #1: only the CardWidget must be visible in check mode
checkVisibleWidget "CardWidget" {"CashWidget" "CheckWidget"}
# Business rule #2: the minimum payment is $10 or 5% of the amount due
# whichever is larger and the maximum is $5000 or the amount due
# whichever is smaller
set amount_due [getAmountDue]
set five_percent [expr $amount_due / 20.0]
set minimum [expr 10 < $five_percent ? $five_percent : 10]
set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
checkPaymentRange $minimum $maximum
# Business rule #3: for non-Visa cards the issue date must be no
# earlier than 3 years ago
# Business rule #4: the expiry date must be at least a month later
# than today---we will make sure this is the case for the later tests
checkCardDateEdits
# Business rule #5: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
set payButton [findObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
test compare [property get $payButton enabled] false
# Business rule #6: the Pay button should be enabled since all the
# previous tests pass, and now we have filled in the account details
populateCardFields
set payButton [waitForObject {{type='QPushButton' text='Pay' unnamed='1' visible='1'}}]
test compare [property get $payButton enabled] true
}
The testing of each business rule is very similar to what we did for
"check" mode—for example, business rules one and two use the same
functions but with different parameters. We have combined the test for
business rules three and four into a single test-specific function,
checkCardDateEdits(), that we will see in a moment.
Business rules five and six work exactly the same way as before only this
time we must populate different widgets to enable the
button and have created the test-specific
populateCardFields() function to do this.
Example 15.6. The tst_card_mode Test Script's other functions
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")
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");
}
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");
}
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 button will be enabled
later on. Initially though, the 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 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.
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.)
It is very easy to iterate over all the items in a list widget and retrieve their texts and check their checked and selected statuses, as the following test example shows:
Example 15.7. The tst_listwidget Test Script
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))
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);
}
}
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");
}
}
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.
The view classes don't hold any data themselves; instead they visualize the data held in a model. So if we want to access all the items associated with a view we must first retrieve the view's model, and then iterate over the model's items. Furthermore, selections are held separately from the data model—in a selection model. This is because a selection is about visual interaction and does not affect the underlying data. (Of course a user might make a selection and then apply a change to the selection, but from the data model's point of view the change is simply applied to one or more items and the model doesn't know or care how those items were chosen.)
Example 15.8. The tst_listview Test Script
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))
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);
}
}
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");
}
}
proc main {} {
startApplication "itemviews"
set listViewName {{type='QListView' unnamed='1' visible='1'}}
set listView [waitForObject $listViewName]
set model [invoke $listView model]
set selectionModel [invoke $listView selectionModel]
for {set row 0} {$row < [invoke $model rowCount]} {incr row} {
set index [invoke $model index $row 0]
set text [toString [invoke [invoke $model data $index] toString]]
set checked ""
set selected ""
set checkState [invoke [invoke $model data $index [enum Qt CheckStateRole]] toInt]
if {$checkState == [enum Qt Checked]} {
set checked " +checked"
}
if [invoke $selectionModel isSelected $index] {
set selected " +selected"
}
test log "($row) '$text'$checked$selected"
}
}
Notice that all data in a model is accessed using a QModelIndex. A model index has three attributes: a row, a column, and a parent. For lists only the row is used—the column is always 0; for tables the row and column are used; and for trees all three are used.
Notice also that the checked state is an attribute of the data, so we
use the QAbstractItemModel.data() method to access it.
(When we use this method without explicitly specifying a role, the role
is taken to be Qt.DisplayRole which usually holds the
item's text.) The QAbstractItemModel.data() method returns
a QVariant,
so we must always convert it to the correct type before using it.
In this subsection and the previous one we have seen how to iterate over
list widgets and list views to check each item. In the next couple of
subsections we will write similar tests for table widgets and table
views. In addition we show how to populate a table widget with
data—and the same approach can be used for populating list or tree
widgets. Populating models is not shown since it is very similar to what
we have seen above—we simply call
QAbstractItemModel.setData() for each item whose value we
want to set, giving an appropriate model index, role, and value.
In this section we will look at two pieces of example code (in all the
main scripting languages that Squish supports). The first example
shows how to set the number of rows and columns a table has and how to
populate a table with items—including making items checkable and
selected—and also how to hide rows. The second example shows how
to iterate over every item in a table (but skipping hidden rows), and
printing the item's text and state information to Squish's log.
(The code shown in this section is taken from the
examples/qt4/csvtable example's
tst_iterating test suites.)
Example 15.9. Setting up a Table Widget
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)
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);
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);
set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
invoke $tableWidget setRowCount 4
invoke $tableWidget setColumnCount 3
set count 0
for {set row 0} {$row < [property get $tableWidget rowCount]} {incr row} {
for {set column 0} {$column < [property get $tableWidget columnCount]} {incr column} {
set tableItem [construct QTableWidgetItem "Item $count"]
incr count
if {$column == 2} {
invoke $tableItem setCheckState [enum Qt Unchecked]
if {$row == 1 || $row == 3} {
invoke $tableItem setCheckState [enum Qt Checked]
}
}
invoke $tableWidget setItem $row $column $tableItem
if {$count == 6 || $count == 10} {
invoke $tableItem setSelected 1
}
}
}
invoke $tableWidget setRowHidden 2 true
The table that the code produces is shown in the screenshot below:
Naturally, the approach shown in these examples can be used to set other aspects of table widget items, such as their font, background color, text alignment and so on.
Whether we have set up a table using our own test code as shown above, or have a table of data that was populated by some other means (for example, by the AUT loading a data file), we need to be able to iterate over the table's items, and check their text and other attributes. This is exactly what the next example shows.
Example 15.10. Testing a Table Widget's Items
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))
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);
}
}
$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");
}
}
set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
for {set row 0} {$row < [property get $tableWidget rowCount]} {incr row} {
if {[invoke $tableWidget isRowHidden $row]} {
test log "Skipping hidden row $row"
continue
}
for {set column 0} {$column < [property get $tableWidget columnCount]} {incr column} {
set tableItem [invoke $tableWidget item $row $column]
set text [toString [invoke $tableItem text]]
set checked ""
set selected ""
if {[invoke $tableItem checkState] == [enum Qt Checked]} {
set checked " +checked"
}
if {[invoke $tableItem isSelected]} {
set selected " +selected"
}
test log "($row, $column) '$text'$checked$selected"
}
}
The log output produced by the above is:
(0, 0) 'Item 0' (0, 1) 'Item 1' (0, 2) 'Item 2' (1, 0) 'Item 3' (1, 1) 'Item 4' (1, 2) 'Item 5' checked selected Skipping hidden row 2 (3, 0) 'Item 9' selected (3, 1) 'Item 10' (3, 2) 'Item 11' checked
And as we noted earlier, the same techniques can be used to test other attributes, such as each table item's font, background color, text alignment, and so on.
Another useful way to test an entire table is to compare all its items
to a data file in
.tsv (tab-separated values format),
.csv (comma-separated values format), or
.xls (Microsoft® Excel™ spreadsheet format).
An example of how to do this is given in
How to Test Table Widgets and Use External Data Files (Qt 4) (Section 15.1.11.5).
Table views, like all the other view classes, presents the data held in a model rather than holding any data itself. So the key to performing tests on the data shown by a table view is to get the table view's model, and work on the model's data. The example below—which is very similar to the list view example shown earlier—shows how to do this.
Example 15.11. The tst_tableview Test Script
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))
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);
}
}
}
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");
}
}
}
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.
Tree widgets (and models shown in tree views) are rather different to test than list or table widgets and views. This is because trees have a more complex underlying structure. The structure is essentially this: a sequence of rows (top-level items), each of which can have one or more columns, and each of which can have its own row of child items. Each child item can have one or more columns, and can have its own row of child items, and so on.
The easiest way to iterate over a tree is to use a recursive procedure
(that its, a procedure that calls itself), starting it off with the
tree's "invisible root item", and then working on every item's child
items, and their child items, and so on. An example is shown below.
(Note that when more than one function is defined in a test, Squish
always (and only) calls the one called main—this
function can then call the other functions as required.)
Example 15.12. The tst_treewidget Test Script
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)
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);
}
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);
}
proc checkAnItem {indent item root} {
if {$indent > -1} {
set checked ""
set selected ""
if {[invoke $item checkState 0] == [enum Qt Checked]} {
set checked " +checked"
}
if [invoke $item isSelected] {
set selected " +selected"
}
set text [toString [invoke $item text 0]]
set pad [string repeat " " $indent]
test log "|$pad'$text'$checked$selected"
} else {
set indent [expr -4]
}
# Only show visible child items
if {$item != $root && [invoke $item isExpanded] || $item == $root} {
for {set row 0} {$row < [invoke $item childCount]} {incr row} {
checkAnItem [expr $indent + 4] [invoke $item child $row] $root
}
}
}
proc main {} {
startApplication "itemviews"
set treeWidgetName {{type='QTreeWidget' unnamed='1' visible='1'}}
set treeWidget [waitForObject $treeWidgetName]
set root [invoke $treeWidget invisibleRootItem]
checkAnItem -1 $root $root
}
The indent is used purely to show the tree's structure when printing out to Squish's log, and the leading |s are used because normally Squish strips whitespace from the ends of log messages and we don't want to do that here. For example:
|'Green algae' | 'Chlorophytes' | 'Chlorophyceae' | 'Ulvophyceae' | 'Trebouxiophyceae' | 'Desmids & Charophytes' | 'Closteriaceae' +checked | 'Desmidiaceae' | 'Gonaozygaceae' +selected | 'Peniaceae' |'Bryophytes' |'Pteridophytes' | 'Club Mosses' | 'Ferns' |'Seed plants' | 'Cycads' +checked +selected | 'Ginkgo' | 'Conifers' | 'Gnetophytes' | 'Flowering Plants'
Notice that we only check items in the first column—if we need to check items in other columns, we must introduce a loop to iterate over the columns and use a column index rather than simply using the 0 (for the first column) that is shown in the example.
Another point to notice is that the 'Bryophytes' entry actually has
three child items ('Liverworts', 'Hornworts', and, 'Mosses'), but these
don't appear because the 'Bryophytes' item is collapsed (doesn't show
its children and has a + to indicate it is expandable,
whereas the others have - to indicate that they are
expanded). In the code we ignore non-visible child items—we do
this by only calling checkAnItem() if the current item is
the root of the tree (i.e., the notional parent of all top-level items),
or if the current item is not the root, but is
expanded (meaning that its child items are visible in the tree).
And we could of course, not skip the non-visible child items, by just
removing the last if statement in
checkAnItem().
Keep in mind that even if an item is visible, it might not be visible to the user—for example, if the item is not in the tree's visible area. However, it will be visible if the user scrolls to it.
Tree views use a tree-structured model and so the easiest way to iterate over all their model's items is to use a recursive procedure, just as we did for tree widgets in the previous subsection. Here's an example:
Example 15.13. The tst_treeview Test Script
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)
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);
}
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);
}
proc checkAnItem {indent index treeView model selectionModel} {
if {$indent > -1 && [invoke $index isValid]} {
set text [toString [invoke [invoke $model data $index] toString]]
set checked ""
set selected ""
set checkState [invoke [invoke $model data $index [enum Qt CheckStateRole]] toInt]
if {$checkState == [enum Qt Checked]} {
set checked " +checked"
}
if [invoke $selectionModel isSelected $index] {
set selected " +selected"
}
set pad [string repeat " " $indent]
test log "|$pad'$text'$checked$selected"
} else {
set indent [expr -4]
}
# Only show visible child items
if {[invoke $index isValid] && [invoke $treeView isExpanded $index] || ![invoke $index isValid]} {
for {set row 0} {$row < [invoke $model rowCount $index]} {incr row} {
checkAnItem [expr $indent + 4] [invoke $model index $row 0 $index] $treeView $model $selectionModel
}
}
}
proc main {} {
startApplication "itemviews"
set treeViewName {{type='QTreeView' unnamed='1' visible='1'}}
set treeView [waitForObject $treeViewName]
set model [invoke $treeView model]
set selectionModel [invoke $treeView selectionModel]
checkAnItem -1 [construct QModelIndex] $treeView $model $selectionModel
}
The code here is structurally almost the same as for iterating over the
items in a tree widget, only here we use model indexes to identify
items. In a model the "invisible root item" is represented by an invalid
model index, that is, a model index created without any arguments. (The
last statement in the main() functions shown above show how
to create an invalid model index.) By using a recursive procedure we
ensure that we can iterate over the entire tree, no matter how deep it
is.
And just as we did for the QTreeWidget example shown
before, for the QTreeView we skip collapsed (non-visible)
child items. And we could easily not skip them by just removing the
last if statement in checkAnItem().
In this section we will see how to test the
csvtable program shown below. This program uses a
QTableWidget to
present the contents of a .csv (comma-separated
values) file, and provides some basic functionality for manipulating the
data—inserting and deleting rows, editing cells, and swapping
columns.
[5]
As we review the tests we will learn how to import test data,
manipulate the data, and compare what the QTableWidget
shows with what we expect its contents to be. And since the
csvtable program is a main-window-style
application, we will also learn how to test that menu options and
toolbar buttons behave as expected (and implicitly that their underlying
actions get carried out). In addition, we will develop some generic
functions that may be useful in several different tests.

csvtable example.
The source code for this example is in the directory
SQUISHROOT/examples/qt4/csvtable, and the test
suites are in subdirectories underneath—for example, the Python
version of the tests is in the directory
SQUISHROOT/examples/qt4/csvtable/suite_py, and
the JavaScript version of the tests is in
SQUISHROOT/examples/qt4/csvtable/suite_js.
The first test we will look at is deceptively simple and consists of just four executable statements. This simplicity is achieved by putting almost all the functionality into a shared script, to avoid code duplication. Here is the code:
Example 15.14. The tst_loading Test Script
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")
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");
}
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");
}
proc main {} {
startApplication "csvtable"
source [findFile "scripts" "common.tcl"]
doFileOpen "suite_tcl/shared/testdata/before.csv"
set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
compareTableWithDataFile $tableWidget "before.csv"
}
We begin by loading in the script that contains common functionality,
just as we did in the previous section. Then we call a custom
doFileOpen() function that tells the program to open
the given file—and this is done through the user interface as we
will see. Next we get a reference to the table widget using the
waitForObject() function, and finally we
check that the table widget's contents match the contents of the data
file held amongst the test suite's test data. Note that both the
csvtable program and Squish load and parse the
data file using their own completely independent code. (See How to Create and Use Shared Data and Shared Scripts (Section 15.4) for how to import test data into Squish.)
Now we will look at the custom functions we have used in the above test.
Example 15.15. Extracts from the Shared Scripts
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())
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());
}
}
}
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));
}
}
}
proc doFileOpen {path_and_filename} {
chooseMenuOptionByKey "File" "F" "o"
waitForObject ":fileNameEdit_QLineEdit"
set components [split $path_and_filename "/"]
foreach component $components {
invoke type ":fileNameEdit_QLineEdit" $component
waitForObject ":fileNameEdit_QLineEdit"
invoke type ":fileNameEdit_QLineEdit" "<Return>"
}
}
proc chooseMenuOptionByKey {menuTitle menuKey optionKey} {
set windowName "{type='MainWindow' unnamed='1' visible='1' windowTitle?='CSV Table*'}"
waitForObject $windowName
invoke type $windowName "<Alt+$menuKey>"
set menuName "{title='$menuTitle' type='QMenu' unnamed='1' visible='1'}"
waitForObject $menuName
invoke type $menuName $optionKey
}
proc compareTableWithDataFile {tableWidget filename} {
set data [testData dataset $filename]
for {set row 0} {$row < [llength $data]} {incr row} {
set columnNames [testData fieldNames [lindex $data $row]]
for {set column 0} {$column < [llength $columnNames]} {incr column} {
set tableItem [invoke $tableWidget item $row $column]
test compare [testData field [lindex $data $row] $column] [invoke $tableItem text]
}
}
}
The doFileOpen() function begins by opening a file
through the user interface. This is done by using the custom
chooseMenuOptionByKey() function. One point to note about
the chooseMenuOptionByKey() function is that it uses
wildcard matching for the windowTitle property (using
?= instead of equality testing with =; see
Improving Object Identification (Section 16.8) for more details.). This is particularly
useful for windows that show the current filename or other text that can
vary. This function simulates the user clicking
Alt+k
(where k is a character, for example "F" for the
file menu), and then the character that corresponds to the required
action, (for example, "o" for "Open"). Once the file open dialog has
popped up, for each component of the path and file we want, the
doFileOpen() function types in a component followed by
Return, and this leads to the file
being opened.
When the file is opened, the program is expected to load the file's
data. We check that the data has been loaded correctly by comparing the
data shown in the table widget and the data file itself. This comparison
is done by the custom compareTableWithDataFile() function.
This function uses Squish's testData.dataset() function to load in the data so
that it can be accessed through the Squish API. We expect every cell
in the table to match the corresponding item in the data, and we check
that this is the case using the test.compare() function.
Now that we know how to compare a table's data with the data in a file
we can perform some more ambitious tests. We will load in the
before.csv file, delete the first, last, and a middle
row, insert a new row at the beginning and in the middle, and append a
new row at the end. Then we will swap three pairs of columns. At the end
the data should match the after.csv file.
Rather than writing code to do all these things we can simply record a test script that opens the file and performs all the deletions, insertions, and column swaps. Then we can edit the recorded test script to add a few lines of code to compare the actual results with the expected results. The added lines are shown below, in context:
Example 15.16. Extracts from the tst_editing Script
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'}")
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'}");
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'}");
invoke type ":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit" "Regulatory Citation,Standard"
waitForObject ":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit"
invoke type ":Enter the comma-separated names of the two columns to be swapped swapped_QLineEdit" "<Return>"
# Added by hand
source [findFile "scripts" "common.tcl"]
set tableWidget [waitForObject {{type='QTableWidget' unnamed='1' visible='1'}}]
compareTableWithDataFile $tableWidget "after.csv"
# End of added by hand
waitForObject ":CSV Table - before.csv.File_QTableWidget"
invoke type ":CSV Table - before.csv.File_QTableWidget" "<Alt+F>"
waitForObject ":CSV Table - before.csv.File_QMenu"
invoke type ":CSV Table - before.csv.File_QMenu" "q"
waitForObject "{type='QPushButton' unnamed='1' text='No'}"
invoke clickButton "{type='QPushButton' unnamed='1' text='No'}"
As the extract indictates, the added lines are not inserted at the end of the recorded test script, but rather just before the program is terminated.
We can do other tests of course, for example, checking some of the table's properties. Here is an example that checks that the row and column counts are what we expect them to be:
Example 15.17. Testing a Table Widget's Properties
tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}")
test.verify(tableWidget.rowCount == 12)
test.verify(tableWidget.columnCount == 5)
tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
test.verify(tableWidget.rowCount == 12);
test.verify(tableWidget.columnCount == 5);
my $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}");
test::verify($tableWidget->rowCount == 12);
test::verify($tableWidget->columnCount == 5);
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.
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.
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)
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);
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);
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.)
def getAction(widget, text):
actions = widget.actions()
for i in range(actions.count()):
action = actions.at(i)
if action.text == text:
return action
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;
}
}
}
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;
}
}
}
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).
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.

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.
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.
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.
Add a polygon and confirm that it is a triangle, i.e., that its polygon has exactly three points.
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.
Add a text item and confirm that the text entered in the input dialog matches that shown by the text item.
Confirm that the Count LCD shows 4 items and that the Delete button and Z spinbox are enabled.
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.
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:
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)
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);
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);
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).
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)
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);
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);
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.
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)
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);
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);
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.
countLCD = waitForObject(":_QLCDNumber")
test.verify(countLCD.intValue == 2)
test.verify(not findObject(":Delete..._QPushButton").enabled)
test.verify(not findObject(":_QSpinBox").enabled)
var countLCD = waitForObject(":_QLCDNumber");
test.verify(countLCD.intValue == 2);
test.verify(!findObject(":Delete..._QPushButton").enabled);
test.verify(!findObject(":_QSpinBox").enabled);
my $countLCD = waitForObject(":_QLCDNumber");
test::verify($countLCD->intValue == 2);
test::verify(!findObject(":Delete..._QPushButton")->enabled);
test::verify(!findObject(":_QSpinBox")->enabled);
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.
This section describes how to test using script code whether the contents of a list view widget meets the expectations.
The first possibility is to iterate over the items in the list view and check the item text. Assuming we have a list view which has one item with the text "Apple" with two children called "Orange" and "Banana", we could use the following Python code to verify this:
listview = waitForObject("<name of list view>")
item = listview.firstChild()
test.compare(item.text(0), "Apple")
child = item.firstChild()
test.compare(child.text(0), "Orange")
sibling = item.nextSibling()
test.compare(sibling.text(0), "Banana")
In addition we want to check that there are no more toplevel items in the list view, meaning the first item has no siblings, so QListViewItem::nextSibling() returns 0 (in JavaScript):
var item = item.nextSibling(); test.verify(isNull(item));
So, we can use the QListViewItem::firstChild() and QListViewItem::nextSibling() functions to traverse the tree of list view items. To retrieve the item text of an item, we use the QListViewItem::text() function and pass the column whose text we want to get back into this function call.
Another possibility to retrieve an item is to use the QListView::findItem() function. This can be used to check whether an item is there or if we don't want to traverse the whole tree of items but we want to start at a specific one. To check that there exists an item with the text "Orange", we would write in Tcl:
set item [invoke $listview findItem "Orange" 0] test compare [isNull $item] false
The second argument in QListView::findItem() is the column number we search in.
A list view can also contain more sophisticated items like QCheckListItems. Let's assume the "Orange" item is such a check item and we want to verify that this item is checked (in Python):
item = listview.findItem("Orange", 0)
checkitem = cast(item, QCheckListItem)
test.compare(checkitem.state(), QCheckListItem.On)
Since item is of the type QListViewItem,
we need to cast it to a QCheckListItems
using the cast command. If the cast fails, 0
would be returned. Then we can call the QCheckListItem::state()
function on the item and test whether this returns QCheckListItem::On
which means the item is checked.
If you want to check states, etc. of the menu using script code and not use Squish IDE's point & click interface to insert such a verification point, it is also possible to directly access menus from script code.
Similar to QListView, menus contain items which can be retrieved by name and which provide functions to verify the state.
On froglogic's Web site you can find the article Squish: Testing Menus which explains how to test menus using script code in great detail.
Similar to QListView, often the contents of a QTable widget needs to be tested. QTable again consists of items which can be retrieved using the QTable::item() function.
To test whether the cell 5/4 contains the text "Kiwi", the following Python code can be used:
table = waitForObject("<name of table>")
cell = table.item(5, 4)
test.compare(cell.text(), "Kiwi")
Similarly to QListView, it is again possible to cast the cell items to more sophisticated items like e.g. QCheckTableItem (in case such items are used in the table) to query properties on those.
Squish/Qt is designed to support automating tests on Qt widgets in Qt applications. However, on some platforms, Qt applications are built using a mixture of Qt and native widgets—for example, on Windows a Qt application may use native Windows dialogs and embedded ActiveX widgets, in addition to Qt widgets.
Fortunately, Squish supports recording and replaying keyboard and mouse operations on all native Windows controls. And in addition, it is possible to inspect the properties of standard Windows controls using Spy, and to insert verifications regarding these controls, and to access their properties inside test scripts. Note also, that there is a specific Squish/Windows edition that works with standard Windows applications such as those created using the MFC or .NET technologies.
This section illustrates how to test Tk applications using Tcl—and in particular, how to test some of the standard Tk widgets. Although only a few widgets are shown, the same principles and practices apply to all Tk widgets, so by the end of this section you should be able to test any of your AUT's widgets.
The most challenging aspect of implementing test scripts is usually when we want to create test verifications. As shown in the chapter Inserting Verification Points (Section 5.3) in the Tutorial: Starting to Test Tcl/Tk Applications (Chapter 13), this can be done using the Spy and its point & click interface. But in some cases it is actually more convenient—and more flexibile—to implement verification points directly in code.
To test and verify a widget and its properties or contents, we must
first get a reference to the widget in the test script. This can be done
by calling the waitForObject() function with
the object's symbolic or real (multi-property) name, since this function
finds the widget with the given name and returns a reference to
it.
So the key to verifying the state and properties of any widget is to be able to uniquely identify the widget we are interested in so that we can get a reference to it. Squish provides a number of different ways of finding the name of a widget. One approach is to record a “dummy” test where we interact with all the widgets we are interested in. This will result in the Object Map (Section 16.9) becoming populated with the names of the objects we interacted with, and we can then simply copy and paste the relevant names into our code.
An alternative to creating a dummy test is to use Squish's Spy tool. (See How to Use the Spy (Section 15.2.4)). Here are the steps to take:
Start the Squish IDE and open the AUT's test suite.
Start the Spy on the AUT.
Switch the Spy into Pause mode.
Switch to the AUT and navigate through the GUI until the widget we want to test is visible (e.g. open the dialog it is contained in).
Switch back to the Squish IDE and switch the Spy into Pick mode.
Switch back to the AUT and click on the widget you want to test.
Switch back to the Squish IDE. In the Spy object view the selected widget and its tree will be displayed. Right-click onto the object name and choose "Copy to clipboard".
Exit the Spy
In fact, when spying, the entire hierarchy of widgets for the AUT's current window is shown, and we can right-click any of the widgets and choose the 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.
One common requirement is to test the state of a widget, in particular
whether it is enable or disabled. The widget's state
property holds the information we want—here are a couple of
examples that show it in use:
set entry1 [waitForObject ":myapp.entry1"] test compare [property get $entry1 state] "normal" set entry2 [waitForObject ":myapp.entry2"] test compare [property get $entry2 state] "disabled"
This code verifies that the entry1 widget is enabled and that the entry2 widget is disabled.
Although the need to verify whether a standard Tk radiobutton or checkbutton is checked is a common requirement, neither of these widgets has a convenient property that we can use, so we must write a little bit more code than might have been expected.
We will start by verifying that a particular radiobutton is checked.
First we must retrieve the radiobutton's variable and
value properties, and then we must evaluate the variable to
see if it is equal to the value—if it is, then the radiobutton is
checked.
set radiobutton [waitForObject ":myapp.radiobutton"] set variable [property get $radiobutton "variable"] set value [property get $radiobutton "value"] set actual_value [invoke tcleval "return \$$variable"] test compare $actual_value $value
First we retrieve a reference to the radiobutton, then we retrieve the two properties we are interested in. Next we evaluate the variable to get its actual value, and finally we compare the actual value with the property value to see if they're the same.
We must use a similar approach for checkbuttons, only they have
onvalue and offvalue properties that we must
work with.
set checkbutton [waitForObject ":myapp.checkbutton"] set variable [property get $checkbutton "variable"] set onvalue [property get $checkbutton "onvalue"] set actual_value [invoke tcleval "return \$$variable"] test compare $actual_value $onvalue
Here, we retrieve a reference to the checkbutton, and then to the
checkbutton's variable and onvalue properties.
And just like we did for the radiobutton, we evaluate the variable to
get its actual value, and compare this with the onvalue to
see if they are the same.
If we wanted to verify that a checkbutton was not
checked, we would simply retrieve the offvalue property and
compare that with the actual value—if they are the same, then the
checkbutton is not checked.
A standard Tk entry widget's contents can be queried using the
getvalue property.
set entry [waitForObject ":myapp.entry"] test compare [property get $entry getvalue] "Houston"
Here we check that an entry contains the text “Houston”.
Querying the contents of Tk's multiline text widget is a bit
more involved. For that we must call the widget's get
method, giving it the start and end indexes for the text we want to
check.
set text [invoke tcleval ".textfield get 1.0 end"] test compare $text "line 1\nline 2"
Rather than retrieve a reference to the multiline text widget, instead
we have used tcleval to execute the widget's
get method with indexes that span the entire
contents—this will result in all of the widget's text being
returned. We then check that the text contains exactly two lines (with
texts, “line 1” and “line 2”).
Squish isn't limited to Tk's standard widgets—for example, we can test a BWidget Entry widget.
set bentry [waitForObject ":myapp.bentry"] test compare [property get $entry text] "Apollo"
Here we retrieve the BWidget's text using its text
property, and compare it to the text “Apollo”.
One common requirement is to check the text of a Tk listbox's active
item. This is easily done using the listbox's get method.
set active [invoke tcleval ".listbox get active"] test compare $active "Gemini"
Similarly to what we did for the multiline text widget, rather than
retrieve a reference to the listbox, instead we have used
tcleval to execute the listbox's get method
with an argument of active—this will result in the
listbox's active item's text being returned. We then compare the text as
usual, in this case with the literal text “Gemini”.
The iwidget Radiobox is different from the standard Tk radiobutton, in
that it has a getvalue property that holds the text of its
currently checked radiobutton.
set radiobox [waitForObject ":myapp.rbox"] test compare [property get $radiobox getvalue] "Mercury"
If the Radiobox has radiobuttons, “Mercury”,
“Venus”, and “Mars”, we can verify that the
“Mercury” radiobutton is checked by retrieving a reference
to the Radiobox, and then comparing the value of its
getvalue property to see if it matches the text of the
radiobutton that should be checked.
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.
To test and verify an HTML element and its properties or contents, we
must first get a reference to the element in the test script. This can be
done by calling the waitForObject() function
with the elements's symbolic or real (multi-property) name, since this
function finds the element with the given name and returns a reference
to it.
So the key to verifying the state and properties of any HTML element is to be able to uniquely identify the element we are interested in so that we can get a reference to it. Squish provides a number of different ways of finding the name of an element. One approach is to record a “dummy” test where we interact with all the elements we are interested in. This will result in the Object Map (Section 16.9) becoming populated with the names of the objects we interacted with, and we can then simply copy and paste the relevant names into our code.
An alternative to creating a dummy test is to use Squish's Spy tool. (See How to Use the Spy (Section 15.2.4)). Here are the steps to take:
Start the Squish IDE and open the Web application's test suite.
Set a breakpoint in the test script you are working with, at the location where you want to insert a verification.
Run the test until there Squish stops at the breakpoint.
When the Squish IDE pops up, start the Spy.
Switch the Spy into Pick mode.
Switch to the Web browser and click on the element you want to test. Since left clicks on form elements and links will trigger actions in the Web page, click with the right mouse button instead—this will allow you to pick the element you want but without avoid triggering any action in the Web page.
Switch back to the Squish IDE. In the Spy object view the selected element and its tree will be displayed. Right-click on the object name and choose "Copy to clipboard".
Stop the test run.
In fact, when spying, the entire hierarchy of elements for the Web page is shown, and we can right-click any of the elements and choose the 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.
One of the most common test requirements is to verify that a particular
element is enabled or disabled at some point during the test run. This
verification is easily made by checking an element's
disabled property.
entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}")
test.compare(entry.disabled, False)
var entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
test.compare(entry.disabled, false);
my $entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
test::compare($entry->disabled, 0);
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.
To verify that a radiobutton or checkbox is checked, we just need to
query its checked property.
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}")
test.compare(radiobutton.checked, True)
var radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
test.compare(radiobutton.checked, true);
my $radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}");
test::compare($radiobutton->checked, 1);
set radiobutton [waitForObject ":{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}"]
test compare [property get $radiobutton checked] true
The coding pattern shown here—get a reference to an object, then verify the value of one of its properties—is very common and can be applied to any element.
Both the text and textarea form elements have
a text property, so it is easy to check what they
contain.
entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}")
test.compare(entry.text, "Ternary")
var entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
test.compare(entry.text, "Ternary");
my $entry = waitForObject("{tagName='INPUT' id='input' form='myform' type='text'}");
test::compare($entry->text, "Ternary");
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.
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.
selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
test.compare(selection.selectedIndex, 2)
test.compare(selection.selectedOption, "Cavalier")
var selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
test.compare(selection.selectedIndex, 2);
test.compare(selection.selectedOption, "Cavalier");
my $selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
test::compare($selection->selectedIndex, 2);
test::compare($selection->selectedOption, "Cavalier");
set selection [waitForObject ":{tagName='INPUT' id='sel' form='myform' type='select-one'}"]
test compare [property get $selection selectedIndex] 2
test compare [property get $selection selectedOption] "Cavalier"
Here we retrieve the selected item from a single selection list box and verify that the third item (the item at index position 2), is selected, and that it has the text “Cavalier”.
selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select'}")
options = selection.options()
test.compare(options.optionAt(0).selected, True) # item at index position 0 is selected
test.compare(options.optionAt(1).selected, False) # item at index position 1 is not selected
test.compare(options.optionAt(2).selected, True) # item at index position 2 is selected
test.compare(options.optionAt(1).text, "Round Head") # item at index position 1 has the text "Round Head"
var selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select'}");
var options = selection.options();
test.compare(options.optionAt(0).selected, true); // item at index position 0 is selected
test.compare(options.optionAt(1).selected, false); // item at index position 1 is not selected
test.compare(options.optionAt(2).selected, true); // item at index position 2 is selected
test.compare(options.optionAt(1).text, "Round Head"); // item at index position 1 has the text "Round Head"
my $selection = waitForObject(":{tagName='INPUT' id='sel' form='myform' type='select'}");
my $options = $selection->options();
test::compare($options->optionAt(0)->selected, 1); # item at index position 0 is selected
test::compare($options->optionAt(1)->selected, 0); # item at index position 1 is not selected
test::compare($options->optionAt(2)->selected, 1); # item at index position 2 is selected
test::compare($options->optionAt(1)->text, "Round Head"); # item at index position 1 has the text "Round Head"
set selection [waitForObject ":{tagName='INPUT' id='sel' form='myform' type='select'}"]
set options [invoke $selection options]
# item at index position 0 is selected
test compare [property get [invoke options optionAt 0] selected] true
# item at index position 1 is not selected
test compare [property get [invoke options optionAt 1] selected] false
# item at index position 2 is selected
test compare [property get [invoke options optionAt 2] selected] true)
# item at index position 1 has the text "Round Head"
test.compare [property get [invoke options optionAt 1] text] "Round Head"
In this example, we retrieve a reference to a mulitple selection list—normally represented by a listbox—and then retrieve its option items. We then verify that the first and third options are selected, and that the second is not selected. We also verify the second option's text.
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:
def getCellText(tableObject, row, column):
return tableObject.evaluateXPath("TBODY/TR[%d]/TD[%d]" % (row + 1, column + 1)).stringValue
function getCellText(tableObject, row, column)
{
return tableObject.evaluateXPath("TBODY/TR[" + (row + 1) + "]/TD[" + (column + 1) + "]").stringValue;
}
sub getCellText
{
my ($tableObject, $row, $column) = @_;
++$row;
++$column;
return $tableObject->evaluateXPath("TBODY/TR[$row]/TD[$column]")->stringValue;
}
proc getCellText {tableObject row column} {
incr row
incr column
set argument "TBODY/TR[$row]/TD[$column]"
return [property get [invoke $tableObject evaluateXPath $argument] stringValue]
}
An XPath is kind of like a file path in that each component is separated
by a /. The XPath used here says, “find every
TBODY tag, and inside each one find the row-th
TR tag, and inside that find the column-th
TD tag”. The result is always an object of type
HTML_XPathResult Class (Section 16.1.9.21);
here we return the result query as a single string value using the
result's stringValue property. (So if there was more than
one TBODY tag in the document that had a cell at the row
and column we wanted, we'd actually get the text of all of them.) We must
add 1 to the row and to the column because XPath queries use 1-based
indexing, but we prefer our functions to have 0-based indexing since
that is the kind used by all the scripting languages that Squish
supports. The function can be used like this:
table = waitForObject(htmlTableName) text = getCellText(table, 23, 11)
var table = waitForObject(htmlTableName); var text = getCellText(table, 23, 11);
my $table = waitForObject($htmlTableName); my $text = getCellText($table, 23, 11);
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).
Of course it is also possible to verify the states and contents of any other element in a Web application's DOM tree.
For example, we might want to verify that a table
with the ID
result_table contains the text—somewhere in the
table, we don't care where—“Total: 387.92”.
table = waitForObject(":{tagName='TABLE' id='result_table]'}")
contents = table.innerText
test.verify(contents.find("Total: 387.92") != -1)
var table = waitForObject(":{tagName='TABLE' id='result_table]'}");
var contents = table.innerText;
test.verify(contents.indexOf("Total: 387.92") != -1);
my $table = waitForObject(":{tagName='TABLE' id='result_table]'}");
my $contents = $table->innerText;
test::verify(index($contents, "Total: 387.92") != -1);
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.
div = waitForObject(":{tagName='DIV' id='syncDIV'}")
test.compare(div.property("style.display"), "hidden")
var div = waitForObject(":{tagName='DIV' id='syncDIV'}");
test.compare(div.property("style.display"), "hidden");
my $div = waitForObject(":{tagName='DIV' id='syncDIV'}");
test::compare($div->property("style.display"), "hidden");
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.
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')")
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')");
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')");
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.)
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.
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.
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:
clickLink(":Login_A")
waitFor("isBrowserDialogOpen()")
automateLogin(tester_username, tester_password)
clickLink(":Login_A");
waitFor("isBrowserDialogOpen()");
automateLogin(tester_username, tester_password);
clickLink(":Login_A");
waitFor("isBrowserDialogOpen()");
automateLogin($tester_username, $tester_password);
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 |
|---|---|
On Mac OS X you must turn on Universal Access in the System Preferences
when you use the
|
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.
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.
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.
# 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()
// 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();
}
# 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();
}
# 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.
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.
Squish/Web now includes support for testing Java applets embedded in the web browser. This works because Squish/Web includes the necessary functionality from the Squish/Java edition, and means that Web testers can test web pages that include applets that have Java GUIs.
To test a Java applet that is embedded in a web page, simply record the test as normal—Squish/Web will handle all the web page interaction, and under the hood the functionality from the Squish/Java edition will be used when necessary for recording (and later playback) of any interactions with Java applets.
See Tutorial: Starting to Test Java™ AWT/Swing Applications (Chapter 6) for an introduction to Java GUI testing.
It is also possible to test java applets stand-alone as described in the Testing Java Applets (Section 16.4.6) section.
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.
![]() | 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. |
In this section we will see how the Squish API makes it straightforward to check the values and states of individual widgets so that we can test our application's business rules.
As we saw in the tutorial, we can use Squish's recording facility to create tests. However, it is often useful to modify such tests, or create tests entirely from scratch in code, particularly when we want to test business rules that involves multiple widgets.
In general there is no need to test a widget's standard behavior. For example, if an unchecked two-valued checkbox isn't checked after being clicked, that's a bug in the toolkit not in our code. If such a case arose we may need to write a workaround (and write tests for it), but normally we don't write tests just to check that a widget behaves as documented. On the other hand, what we do want to test is whether our application provides the business rules we intended to build into it. Some tests concern individual widgets in isolation—for example, testing that a combobox contains the appropriate items. Other tests concern inter-widget dependencies and interactions. For example, if we have a group of "payment method" radio buttons, we will want to test that if the "cash" radio button is chosen the check and credit card-relevant widgets are all hidden.
Whether we are testing individual widgets or inter-widget dependencies and interactions, we must first be able to get references to the widgets we want to test. Once we have a reference we can then verify that the widget it refers to has the value and is in the states that we expect.
There are a couple of approaches we can take to get a reference to a
Java™ widget. One approach is based on identifying widgets by name.
This can be done in two different ways. We can record a dummy test
making sure we click the widgets we are interested in so that Squish
adds the widgets' names to the object map. Or we can use the How to Use the Spy (Section 15.2.4) tool to pick the widgets we are interested in. In
either of these cases we can then use the waitForObject() function to retrieve a reference
to the widget of interest. Another approach is to obtain a reference to
a container widget as just described, and then use Java™'s
introspection facilities to obtain references to the widgets contained
in the container. We will show both approaches in this section.
The purpose of this section is to explain and show how to access various Java™ widgets and perform common operations using these widgets—such as getting and setting their properites—with the Perl, Python, JavaScript, and Tcl scripting languages.
After completing this section you should be able to access Java™ widgets, gather data from those Java™ widgets, and perform tests against expected values. The principles covered in this chapter apply to all Java™ widgets, so even if you need to test a widget that isn't specifically mentioned here, you should have no problem doing so.
To test and verify a widget and its properties or contents,
first we need access to the widget in the test script. To obtain a
reference to the widget, the waitForObject()
function is used. This function finds the widget with the given name and
returns a reference to it.
For this purpose we need to know the name of the widget we want to test. To find out the name of a widget the Spy tool comes in very handy (for more details about using Spy see How to Use the Spy (Section 15.2.4).
The steps to find out the name are the following:
Start the Squish IDE and make the test suite we are working in active
Start the Spy on the application under test
Switch to the AUT and work through the GUI until the widget we want to test is visible (e.g. open the dialog it is contained in)
Switch back to the Squish IDE and switch the Spy into Pick mode
Switch to the AUT and click on the widget you want to test
Switch back to the Squish IDE. In the Spy object view the selected widget and its tree will be displayed. Right-click onto the object name and choose "Copy to clipboard".
Exit the Spy
Now the object name we were looking for is saved in the
clipboard and we can paste it into the script as the argument to
waitForObject().
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.
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.

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 button must only be enabled if the
correct fields are filled in and have valid values. The business rules
that we must test for are as follows:
In "cash" mode, i.e., when the Cash tab is checked:
The minimum payment is one dollar and the maximum is $2000 or the amount due, whichever is smaller.
In "check" mode, i.e., when the Check tab is checked:
The minimum payment is $10 and the maximum is $250 or the amount due, whichever is smaller.
The check date must be no earlier than 30 days ago and no later than tomorrow.
The bank name, bank number, account name, and account number line edits must all be nonempty.
The check signed checkbox must be checked.
In "card" mode, i.e., when the Credit Card tab is checked:
The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.
For non-Visa cards the issue date must be no earlier than three years ago.
The expiry date must be at least one month later than today.
The account name and account number line edits must be nonempty.
We will write three tests, one for each of the form's modes.
The source code for the payment form is in the directory
SQUISHROOT/examples/java/paymentform, and the test
suites are in subdirectories underneath—for example, the Python
version of the tests is in the directory
SQUISHROOT/examples/java/paymentform/suite_py, and
the JavaScript version of the tests is in
SQUISHROOT/examples/java/paymentform/suite_js.
We will begin by reviewing the test script for testing the form's "cash" mode. First we will show the code, then we will explain it.
Example 15.18. The tst_cash_mode Test Script
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)
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);
}
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);
}
proc main {} {
startApplication "PaymentForm.class"
# Start with the correct tab
set tabWidgetName ":Payment Form.Cash_com.froglogic.squish.awt.TabProxy"
waitForObject $tabWidgetName
invoke clickTab $tabWidgetName
# Business rule #1: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
set amountDueLabelName {{caption?='[$][0-9.,]*' type='javax.swing.JLabel' visible='true' window=':Payment Form_PaymentForm'}}
set amountDueLabel [waitForObject $amountDueLabelName]
set amountText [toString [property get $amountDueLabel text]]
regsub -all {\D} $amountText "" amountText
set amount_due [expr $amountText]
set maximum [expr $amount_due < 2000 ? $amount_due : 2000]
set paymentSpinnerName {{type='javax.swing.JSpinner' visible='true' window=':Payment Form_PaymentForm'}}
set paymentSpinner [waitForObject $paymentSpinnerName]
set model [invoke $paymentSpinner getModel]
set minimumAllowed [invoke [property get $model minimum] intValue]
set maximumAllowed [invoke [property get $model maximum] intValue]
test compare $minimumAllowed 1
test compare $maximumAllowed $maximum
# Business rule #2: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
set payButtonName ":Payment Form.Pay_javax.swing.JButton"
waitForObject $payButtonName
set payButton [findObject $payButtonName]
test verify [property get $payButton enabled]
}
We must start by making sure that the form is in the mode we want to
test. In general, the way we gain access to visible widgets is always
the same: we create a variable holding the widget's name, then we call
waitForObject() to get a reference to the
widget. Once we have the reference we can use it to access the widget's
properties and to call the widget's methods. In this case we don't need
to callwaitForObject() on the tab's name since
we don't need a reference to the tab; instead we just use the clickTab() function to click the tab we are
interested in. How did we know the tab's name? We ran a dummy test and
clicked each of the tabs—as a result Squish put the tab names in
the object map and we copied them from there.
The first business rule to be tested concerns the minimum and maximum
allowed payment amounts. As usual we begin by calling waitForObject() to get a reference to it—in
this case starting with the amount due label. Because the amount due
label's text varies depending on the amount due we cannot have a fixed
name for it. So instead we identify it using a multiproperty name using
wildcards. The wildcard of [$][0-9.,]* matches any text
that starts with a dollar sign and is followed by zero or more digits,
periods and commas. Squish can also do regular expression
matching—see Improving Object Identification (Section 16.8) for more about
matching.
Since the label's text might contain a currency symbol and grouping
markers (for example, $1,700 or €1.700), to convert its text into an
integer we must strip away any non-digit characters first. We do this in
different ways depending on the underlying scripting language. (For
example, in Python, we iterate over each character and join all those
that are digits into a single string and use the cast() function which takes an object and the type
the object should be converted to, and returns an object of the
requested type—or 0 on failure. We use a similar approach in
JavaScript, but for Perl and Tcl we simply replace non-digit characters
using a regular expression.) The resulting integer is the amount due, so
we can now trivially calculate the maximum amount that can be paid in
cash.
With the minimum and maximum amounts known we next get a reference to
the payment spinner. (In this case we didn't get the name from the
object map, but guessed it. We could have used introspection, a
technique we will use shortly.) Once we have a reference to the spinner,
we retrieve its number model. Then we use the test.verify() method to ensure that the model has
the correct minimum and maximum amounts set. (For Tcl we have used the
test.compare() method instead of test.verify() since it is more convenient to do
so.)
Checking the last business rule is easy in this case since if the amount
is in range (and it must be because we have just checked it), then
payment is allowed so the 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
button is enabled.
Although the "cash" mode test works well, there are a few places where
we use essentially the same code. So before creating the test for the
"check" and "card" modes, we will create some common functions that we
can use to refactor our tests with. (The process used to create shared
code is described a little later in How to Create and Use Shared Data and Shared Scripts (Section 15.4)—essentially all we need to do is create
a new script under the Test Suite's shared item's scripts item.) The
Python common code is in common.py, the JavaScript
common code is in common.js, and so on.
Example 15.19. The Shared Code
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)
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);
}
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);
}
proc clickTabbedPane {text} {
waitForObject ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy"
invoke clickTab ":Payment Form.${text}_com.froglogic.squish.awt.TabProxy"
}
proc getAmountDue {} {
set amountDueLabel [waitForObject {{caption?='[$][0-9.,]*' type='javax.swing.JLabel' visible='true' window=':Payment Form_PaymentForm'}}]
set amountText [toString [property get $amountDueLabel text]]
regsub -all {\D} $amountText "" amountText
return [expr $amountText]
}
proc checkPaymentRange {minimum maximum} {
set paymentSpinner [waitForObject {{type='javax.swing.JSpinner' visible='true' window=':Payment Form_PaymentForm'}}]
set model [invoke $paymentSpinner getModel]
set minimumAllowed [invoke [property get $model minimum] intValue]
set maximumAllowed [invoke [property get $model maximum] intValue]
test compare $minimumAllowed $minimum
test compare $maximumAllowed $maximum
}
Now we can write our tests for "check" and "card" modes and put more of
our effort into testing the business rules and less into some of the
basic chores. We've broken the code for "check" mode into a
main() function—this is special to Squish and the
only function Squish will call—and some test-specific supporting
functions, which combined with the shared functions shown above, make
the code more managable. Although the main() function comes
at the end of the test.py (or
test.js and so on) file, we will show it first, and
then show the test-specific supporting functions afterwards.
Example 15.20. The tst_check_mode Test Script's main() function
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)
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);
}
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);
}
proc main {} {
startApplication "PaymentForm.class"
# Import functionality needed by more than one test script
source [findFile "scripts" "common.tcl"]
# Start with the correct tab
clickTabbedPane "Check"
# Business rule #1: the minimum payment is $10 and the maximum is
# $250 or the amount due whichever is smaller
set amount_due [getAmountDue]
set maximum [expr 250 > $amount_due ? $amount_due : 250]
checkPaymentRange 10 $maximum
# Business rule #2: the check date must be no earlier than 30 days
# ago and no later than tomorrow
checkDateRange -30 1
# Business rule #3: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
set payButtonName ":Payment Form.Pay_javax.swing.JButton"
set payButton [findObject $payButtonName]
test compare [property get $payButton enabled] false
# Business rule #4: the check must be signed (and if it isn't we
# will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked
# Business rule #5: the Pay button should be enabled since all the
# previous tests pass, the check is signed and now we have filled in
# the account details
populateCheckFields
set payButton [waitForObject $payButtonName]
test compare [property get $payButton enabled] true
}
The source() function is used to read in a
script and execute it. Normally such a script is used purely to define
things—for example, functions—and these then become
available to the test script.
The first business rule is very similar to before, but the code is much
shorter thanks to the shared checkPaymentRange() function.
The test for the second rule is even simpler since we have put all the
code in a separate checkDateRange() function that we will
look at in a moment.
The third rule checks that the
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 button should be enabled, and the last line tests that it is.
Example 15.21. The tst_check_mode Test Script's other functions
def checkDateRange(daysBeforeToday, daysAfterToday):
calendar = java_util_Calendar.getInstance()
calendar.add(java_util_Calendar.DAY_OF_MONTH, daysBeforeToday)
earliest = calendar.getTime()
calendar.setTime(java_util_Date())
calendar.add(java_util_Calendar.DAY_OF_MONTH, daysAfterToday)
latest = calendar.getTime()
checkDateSpinner = waitForObject(
"{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' "
"type='javax.swing.JSpinner' visible='true'}")
model = checkDateSpinner.getModel()
formatter = java_text_SimpleDateFormat("yyyy-MM-dd")
test.verify(formatter.format(model.getStart()) == formatter.format(earliest))
test.verify(formatter.format(model.getEnd()) == formatter.format(latest))
def ensureSignedCheckBoxIsChecked():
checkSignedCheckBox = waitForObject(
":Check.Check Signed_javax.swing.JCheckBox")
if not checkSignedCheckBox.isSelected():
clickButton(checkSignedCheckBox)
test.verify(checkSignedCheckBox.isSelected())
def populateCheckFields():
bankNameLineEdit = waitForObject(":Check.Bank Name:_javax.swing.JTextField")
type(bankNameLineEdit, "A Bank")
bankNumberLineEdit = waitForObject(":Check.Bank Number:_javax.swing.JTextField")
type(bankNumberLineEdit, "88-91-33X")
accountNameLineEdit = waitForObject(":Check.Account Name:_javax.swing.JTextField")
type(accountNameLineEdit, "An Account")
accountNumberLineEdit = waitForObject(":Check.Account Number:_javax.swing.JTextField")
type(accountNumberLineEdit, "932745395")
function checkDateRange(daysBeforeToday, daysAfterToday)
{
var checkDateSpinner = waitForObject("{container=':Payment Form." +
"Check_com.froglogic.squish.awt.TabProxy' " +
"type='javax.swing.JSpinner' visible='true'}");
var model = checkDateSpinner.getModel();
var calendar = java_util_Calendar.getInstance();
calendar.add(java_util_Calendar.DAY_OF_MONTH, daysBeforeToday);
var earliest = calendar.getTime();
calendar.setTime(new java_util_Date());
calendar.add(java_util_Calendar.DAY_OF_MONTH, daysAfterToday);
var latest = calendar.getTime();
var formatter = new java_text_SimpleDateFormat("yyyy-MM-dd");
test.verify(formatter.format(model.getStart()) ==
formatter.format(earliest));
test.verify(formatter.format(model.getEnd()) ==
formatter.format(latest));
}
function ensureSignedCheckBoxIsChecked()
{
var checkSignedCheckBox = waitForObject(
":Check.Check Signed_javax.swing.JCheckBox");
if (!checkSignedCheckBox.isSelected()) {
clickButton(checkSignedCheckBox);
}
test.verify(checkSignedCheckBox.isSelected());
}
function populateCheckFields()
{
var bankNameLineEdit = waitForObject(":Check.Bank Name:_javax.swing.JTextField");
type(bankNameLineEdit, "A Bank");
var bankNumberLineEdit = waitForObject(":Check.Bank Number:_javax.swing.JTextField");
type(bankNumberLineEdit, "88-91-33X");
var accountNameLineEdit = waitForObject(":Check.Account Name:_javax.swing.JTextField");
type(accountNameLineEdit, "An Account");
var accountNumberLineEdit = waitForObject(":Check.Account Number:_javax.swing.JTextField");
type(accountNumberLineEdit, "932745395");
}
sub checkDateRange
{
my ($daysBeforeToday, $daysAfterToday) = @_;
my $calendar = java_util_Calendar::getInstance();
$calendar->add(java_util_Calendar::DAY_OF_MONTH, $daysBeforeToday);
my $earliest = $calendar->getTime();
$calendar->setTime(java_util_Date->new());
$calendar->add(java_util_Calendar::DAY_OF_MONTH, $daysAfterToday);
my $latest = $calendar->getTime();
my $checkDateSpinner = waitForObject(
"{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' " .
"type='javax.swing.JSpinner' visible='true'}");
my $model = $checkDateSpinner->getModel();
my $formatter = java_text_SimpleDateFormat->new("yyyy-MM-dd");
test::verify($formatter->format($model->getStart()) eq $formatter->format($earliest));
test::verify($formatter->format($model->getEnd()) eq $formatter->format($latest));
}
sub ensureSignedCheckBoxIsChecked
{
my $checkSignedCheckBox = waitForObject(
":Check.Check Signed_javax.swing.JCheckBox");
if (!$checkSignedCheckBox->isSelected()) {
clickButton($checkSignedCheckBox);
}
test::verify($checkSignedCheckBox->isSelected());
}
sub populateCheckFields
{
my $bankNameLineEdit = waitForObject(":Check.Bank Name:_javax.swing.JTextField");
type($bankNameLineEdit, "A Bank");
my $bankNumberLineEdit = waitForObject(":Check.Bank Number:_javax.swing.JTextField");
type($bankNumberLineEdit, "88-91-33X");
my $accountNameLineEdit = waitForObject(":Check.Account Name:_javax.swing.JTextField");
type($accountNameLineEdit, "An Account");
my $accountNumberLineEdit = waitForObject(":Check.Account Number:_javax.swing.JTextField");
type($accountNumberLineEdit, "932745395");
}
proc checkDateRange {daysBeforeToday daysAfterToday} {
set checkDateSpinner [waitForObject {{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' type='javax.swing.JSpinner' visible='true'}}]
set formatter [construct java_text_SimpleDateFormat "yyyy-MM-dd"]
set model [invoke $checkDateSpinner getModel]
set minimumAllowed [invoke $formatter format [invoke $model getStart]]
set maximumAllowed [invoke $formatter format [invoke $model getEnd]]
set calendar [invoke java_util_Calendar getInstance]
invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] $daysBeforeToday
set minimumDate [invoke $formatter format [invoke $calendar getTime]]
invoke $calendar setTime [construct java_util_Date]
invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] $daysAfterToday
set maximumDate [invoke $formatter format [invoke $calendar getTime]]
test compare $minimumAllowed $minimumDate
test compare $maximumAllowed $maximumDate
}
proc ensureSignedCheckBoxIsChecked {} {
set checkSignedCheckBox [waitForObject ":Check.Check Signed_javax.swing.JCheckBox"]
if {![invoke $checkSignedCheckBox isSelected]} {
invoke clickButton $checkSignedCheckBox
}
test compare [invoke $checkSignedCheckBox isSelected] true
}
proc populateCheckFields {} {
set bankNameLineEdit [waitForObject ":Check.Bank Name:_javax.swing.JTextField"]
invoke type $bankNameLineEdit "A Bank"
set bankNumberLineEdit [waitForObject ":Check.Bank Number:_javax.swing.JTextField"]
invoke type $bankNumberLineEdit "88-91-33X"
set accountNameLineEdit [waitForObject ":Check.Account Name:_javax.swing.JTextField"]
invoke type $accountNameLineEdit "An Account"
set accountNumberLineEdit [waitForObject ":Check.Account Number:_javax.swing.JTextField"]
invoke type $accountNumberLineEdit "932745395"
}
In the checkDateRange() function we test the properties of
a JSpinner's SpinnerDateModel. Notice that we
compare the dates as strings using a uniform format.
While Squish provides access to most of the Java™ API automatically,
in some cases we need to access classes that are not available by
default. In this function we need a couple of classes that are not
available, java.util.Calendar and
java.text.SimpleDateFormat. This isn't a problem in
practice since we can always register additional classes (whether
standard or our own custom classes) with the squishserver—see
Wrapping custom Java™ classes (Section 16.4.8) for details. In this case we added
several extra classes in java.ini which has just
two lines:
[general] AutClasses="java.util.Calendar","java.util.Date","java.text.DateFormat","java.text.SimpleDateFormat"
The ensureSignedCheckBoxIsChecked() function checks the
state of a JCheckBox and if it is not checked, checks it by
clicking it. The function then verifies that the checkbox is indeed
checked.
The populateCheckFields() function fills in some
JTextFields—this, along with setting the date and
checking the checkbox earlier—should ensure that the
is enabled, something that is checked in the
main() function after the populateCheckFields() function is called.
Notice that we used the type() function to
simulate the user entering text. It is almost always better to simulate
user interaction than to set widget properties directly—after all,
it is the application's behavior as experienced by the user that we
normally need to test.
We are now ready to look at the last test of the form's business logic—the test of "card" mode. The code is a bit longer because there are a few more things to test given the test specification, but everything works on the same principles as the tests we have already seen.
Example 15.22. The tst_card_mode Test Script's main() function
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)
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);
}
sub main
{
startApplication("PaymentForm.class");
source(findFile("scripts", "common.pl"));
# Start with the correct tab
clickTabbedPane("Credit Card");
# Business rule #1: the minimum payment is $10 or 5% of the amount due
# whichever is larger and the maximum is $5000 or the amount due
# whichever is smaller
my $amount_due = getAmountDue();
my $minimum = $amount_due / 20.0 > 10 ? $amount_due / 20.0 : 10;
my $maximum = $amount_due < 5000 ? $amount_due : 5000;
checkPaymentRange($minimum, $maximum);
# Business rule #2: for non-Visa cards the issue date must be no
# earlier than 3 years ago
# Business rule #3: the expiry date must be at least a month later
# than today---we make sure that this is the case for the later tests
checkCardDateEdits;
# Business rule #4: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
my $payButtonName = ":Payment Form.Pay_javax.swing.JButton";
my $payButton = findObject($payButtonName);
test::compare($payButton->enabled, 0);
# Business rule #5: the Pay button should be enabled since all the
# previous tests pass, and now we have filled in the account details
populateCardFields;
my $payButton = findObject($payButtonName);
test::verify($payButton->enabled);
}
proc main {} {
startApplication "PaymentForm.class"
# Import functionality needed by more than one test script
source [findFile "scripts" "common.tcl"]
# Start with the correct tab
clickTabbedPane "Credit Card"
# Business rule #1: the minimum payment is $10 or 5% of the amount due
# whichever is larger and the maximum is $5000 or the amount due
# whichever is smaller
set amount_due [getAmountDue]
set five_percent [expr $amount_due / 20]
set minimum [expr 10 > $five_percent ? 10 : $five_percent]
set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
checkPaymentRange $minimum $maximum
# Business rule #2: for non-Visa cards the issue date must be no
# earlier than 3 years ago
# Business rule #3: the expiry date must be at least a month later
# than today---we make sure that this is the case for the later tests
checkCardDateEdits
# Business rule #4: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
set payButtonName ":Payment Form.Pay_javax.swing.JButton"
set payButton [findObject $payButtonName]
test compare [property get $payButton enabled] false
# Business rule #5: the Pay button should be enabled since all the
# previous tests pass, and now we have filled in the account details
populateCardFields
set payButton [waitForObject $payButtonName]
test compare [property get $payButton enabled] true
}
Just as we did for the "check" mode's main() function, we
have encapsulated almost every test in a separate function. This makes
the main() function simpler and clearer and makes it easier
to develop and test tests individually.
Example 15.23. The tst_card_mode Test Script's other functions
def checkCardDateEdits():
# (1) set the card type to any non-Visa card
cardTypeComboBox = waitForObject(":Credit Card.Card Type:_javax.swing.JComboBox")
for index in range(cardTypeComboBox.getItemCount()):
if cardTypeComboBox.getItemAt(index).toString() != "Visa":
cardTypeComboBox.setSelectedIndex(index)
break
# (2) find the two date spinners
creditCardTabPane = waitForObject(
":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy").component
spinners = []
for i in range(creditCardTabPane.getComponentCount()):
component = creditCardTabPane.getComponent(i)
if component.getClass().toString() == "class javax.swing.JSpinner":
spinners.append(component)
test.verify(len(spinners) == 2)
# (3) check the issue date spinner's minimum date
calendar = java_util_Calendar.getInstance()
calendar.add(java_util_Calendar.YEAR, -3)
date = calendar.getTime()
issueDateSpinner = spinners[0]
model = issueDateSpinner.getModel()
formatter = java_text_SimpleDateFormat("yyyy-MM-dd")
test.verify(formatter.format(model.getStart()) == formatter.format(date))
# (4) set the expiry date more than a month later than now for later tests
calendar.setTime(java_util_Date())
calendar.add(java_util_Calendar.DAY_OF_MONTH, 35)
expiryDateSpinner = spinners[1]
expiryDateSpinner.setValue(calendar.getTime())
def populateCardFields():
cardAccountNameLineEdit = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField")
type(cardAccountNameLineEdit, "An Account")
cardAccountNumberLineEdit = waitForObject(":Credit Card.Account Number:_javax.swing.JTextField")
type(cardAccountNumberLineEdit, "1343 876 326 1323 32")
function checkCardDateEdits()
{
// (1) set the card type to any non-Visa card
var cardTypeComboBox = waitForObject(":Credit Card.Card Type:_javax.swing.JComboBox");
for (var index = 0; index < cardTypeComboBox.getItemCount(); ++index) {
if (cardTypeComboBox.getItemAt(index).toString() != "Visa") {
cardTypeComboBox.setSelectedIndex(index);
break;
}
}
// (2) find the two date spinners
var creditCardTabPane = waitForObject(":Payment Form.Credit Card_com." +
"froglogic.squish.awt.TabProxy").component;
var spinners = [];
for (var i = 0; i < creditCardTabPane.getComponentCount(); ++i) {
var component = creditCardTabPane.getComponent(i);
if (component.getClass().toString() ==
"class javax.swing.JSpinner") {
spinners.push(component);
}
}
test.verify(spinners.length == 2);
// (3) check the issue date spinner's minimum date
var calendar = java_util_Calendar.getInstance();
calendar.add(java_util_Calendar.YEAR, -3);
var threeYearsAgo = calendar.getTime();
var issueDateSpinner = spinners[0];
var model = issueDateSpinner.getModel();
var formatter = new java_text_SimpleDateFormat("yyyy-MM-dd");
test.verify(formatter.format(model.getStart()) ==
formatter.format(threeYearsAgo));
// (4) set the expiry date more than a month later than now for later tests
calendar.setTime(new java_util_Date());
calendar.add(java_util_Calendar.DAY_OF_MONTH, 35);
var expiryDateSpinner = spinners[1];
expiryDateSpinner.setValue(calendar.getTime());
}
function populateCardFields()
{
var cardAccountNameLineEdit = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField");
type(cardAccountNameLineEdit, "An Account");
var cardAccountNumberLineEdit = waitForObject(":Credit Card.Account Number:_javax.swing.JTextField");
type(cardAccountNumberLineEdit, "1343 876 326 1323 32");
}
sub checkCardDateEdits
{
# (1) set the card type to any non-Visa card
my $cardTypeComboBox = waitForObject(":Credit Card.Card Type:_javax.swing.JComboBox");
for (my $index = 0; $index < $cardTypeComboBox->getItemCount(); ++$index) {
if ($cardTypeComboBox->getItemAt($index)->toString() != "Visa") {
$cardTypeComboBox->setSelectedIndex($index);
break;
}
}
# (2) find the two date spinners
my $creditCardTabPane = waitForObject(
":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy")->component;
my @spinners = ();
for (my $i = 0; $i < $creditCardTabPane->getComponentCount(); ++$i) {
my $component = $creditCardTabPane->getComponent($i);
if ($component->getClass()->toString() eq "class javax.swing.JSpinner") {
push @spinners, $component;
}
}
test::verify(@spinners == 2);
# (3) check the issue date spinner's minimum date
my $calendar = java_util_Calendar::getInstance();
$calendar->add(java_util_Calendar::YEAR, -3);
my $date = $calendar->getTime();
my $issueDateSpinner = $spinners[0];
my $model = $issueDateSpinner->getModel();
my $formatter = java_text_SimpleDateFormat->new("yyyy-MM-dd");
test::verify($formatter->format($model->getStart()) eq $formatter->format($date));
# (4) set the expiry date more than a month later than now for later tests
$calendar->setTime(java_util_Date->new());
$calendar->add(java_util_Calendar::DAY_OF_MONTH, 35);
my $expiryDateSpinner = $spinners[1];
$expiryDateSpinner->setValue($calendar->getTime());
}
sub populateCardFields
{
my $cardAccountNameLineEdit = waitForObject(":Credit Card.Account Name:_javax.swing.JTextField");
type($cardAccountNameLineEdit, "An Account");
my $cardAccountNumberLineEdit = waitForObject(":Credit Card.Account Number:_javax.swing.JTextField");
type($cardAccountNumberLineEdit, "1343 876 326 1323 32");
}
proc checkCardDateEdits {} {
# (1) set the card type to any non-Visa card
set cardTypeComboBox [waitForObject ":Credit Card.Card Type:_javax.swing.JComboBox"]
set count [invoke $cardTypeComboBox getItemCount]
for {set index 0} {$index < $count} {incr index} {
if {[invoke $cardTypeComboBox getItemAt $index] != "Visa"} {
invoke $cardTypeComboBox setSelectedIndex $index
break
}
}
# (2) find the two date spinners
set creditCardTabPaneProxy [waitForObject ":Payment Form.Credit Card_com.froglogic.squish.awt.TabProxy"]
set creditCardTabPane [property get $creditCardTabPaneProxy component]
set spinners {}
set count [invoke $creditCardTabPane getComponentCount]
for {set index 0} {$index < $count} {incr index} {
set component [invoke $creditCardTabPane getComponent $index]
set classname [invoke [invoke $component getClass] toString]
if {$classname == "class javax.swing.JSpinner"} {
lappend spinners $component
}
}
test compare [llength $spinners] 2
# (3) check the issue date spinner's minimum date
set calendar [invoke java_util_Calendar getInstance]
invoke $calendar add [property get java_util_Calendar YEAR] -3
set issueDateSpinner [lindex $spinners 0]
set model [invoke $issueDateSpinner getModel]
set formatter [construct java_text_SimpleDateFormat "yyyy-MM-dd"]
set minimumAllowed [invoke $formatter format [invoke $model getStart]]
set minimumDate [invoke $formatter format [invoke $calendar getTime]]
test compare $minimumAllowed $minimumDate
# (4) set the expiry date more than a month later than now for later tests
invoke $calendar setTime [construct java_util_Date]
invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] 35
set expiryDateSpinner [lindex $spinners 1]
invoke $expiryDateSpinner setValue [invoke $calendar getTime]
}
proc populateCardFields {} {
set cardAccountNameLineEdit [waitForObject ":Credit Card.Account Name:_javax.swing.JTextField"]
invoke type $cardAccountNameLineEdit "An Account"
set cardAccountNumberLineEdit [waitForObject ":Credit Card.Account Number:_javax.swing.JTextField"]
invoke type $cardAccountNumberLineEdit "1343 876 326 1323 32"
}
The second and third business rules are handled by the test-specific
checkCardDateEdits() function.
For the second business rule we need the card type combobox to be on any
card type except Visa, so we iterate over the combobox's items and set
the current item to be the first non-Visa item we find. Now we must
check that the card's issue date is not allowed to be too long ago.
This form has two JSpinners, one used for the card's issue
date and the other for the card's expiry date. We can't use names to
distingish between the spinners so we must obtain references to them by
using introspection. To do this we begin by finding the innermost
component that contains the spinners—in this case the
JPane that is shown by the JTabbedPane's
current tab. (Squish uses "proxy"s for some widgets, in this case a
TabProxy; but we can always access the relevant component using the
component property as we do here.) Once we have the
JPane, we iterate over its components, making a list of
those that are JSpinners. We then check that there are
exactly two spinners as expected and then we are ready to check
that the minimum issue date has been correctly set to three years ago.
The third business rule says that the expiry date must be at least a month ahead. We explictly set the expiry to be 35 days ahead so that the button will be enabled later on.
Initially the 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
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.
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.
It is very easy to iterate over all the items in a JList and retrieve their texts and selected status, as the following test example shows:
Example 15.24. The tst_jlist Test Script
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))
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);
}
}
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");
}
}
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.
It is also very easy to iterate over all the items in a JTable and retrieve their texts and selected status, as the following test example shows:
Example 15.25. The tst_jtable Test Script
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))
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);
}
}
}
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");
}
}
}
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.
It is slightly more tricky to iterate over all the items in a JTree and retrieve their texts and selected status—since a tree is a recursive structure. Nonetheless, it is perfectly possible, as the following test example shows:
Example 15.26. The tst_jtree Test Script
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)
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);
}
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);
}
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.
In this section we will see how to test the
CsvTable.java program shown below. This program
uses a JTable to present the contents of a .csv
(comma-separated values) file, and provides some basic functionality for
manipulating the data—inserting and deleting rows and swapping
columns.
[5]
As we review the tests we will learn how to import test data,
manipulate the data, and compare what the JTable
shows with what we expect its contents to be. And since the
CSV Table program is a main-window-style
application, we will also learn how to test that menu options behave as
expected.

The source code for this example is in the directory
SQUISHROOT/examples/java/csvtable, and the test
suites are in subdirectories underneath—for example, the Python
version of the tests is in the directory
SQUISHROOT/examples/java/csvtable/suite_py, and
the JavaScript version of the tests is in
SQUISHROOT/examples/java/csvtable/suite_js.
The first test we will look at is deceptively simple and consists of just four executable statements. This simplicity is achieved by putting almost all the functionality into a shared script, to avoid code duplication. Here is the code:
Example 15.27. The tst_loading Test Script
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)
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);
}
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);
}
proc main {} {
startApplication "CsvTable.class"
source [findFile "scripts" "common.tcl"]
set filename "before.csv"
doFileOpen $filename
set jtable [waitForObject {{type='javax.swing.JTable' visible='true'}}]
compareTableWithDataFile $jtable $filename
}
We begin by loading in the script that contains common functionality,
just as we did in an earlier section. Then we call a custom
doFileOpen() function that tells the program to open
the given file—and this is done through the user interface as we
will see. Next we get a reference to the JTable using the
waitForObject() function, and finally we
check that the JTable's contents match the contents of the data file
held amongst the test suite's test data. Note that both the CSV Table
program and Squish load and parse the data file using their own
completely independent code. (See How to Create and Use Shared Data and Shared Scripts (Section 15.4) for
how to import test data into Squish.)
Now we will look at the custom functions we have used in the above test.
Example 15.28. Extracts from the Shared Scripts
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)
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);
}
}
}
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));
}
}
}
proc doFileOpen {filename} {
chooseMenuOptionByKey "F" "o"
set paneName {{type='javax.swing.JRootPane' visible='true'}}
waitForObject $paneName
# Platform-specific name
set fileDialogEntryName {{leftWidget=':Open.File Name:_javax.swing.plaf.metal.MetalFileChooserUI$AlignedLabel' type='javax.swing.plaf.metal.MetalFileChooserUI$3' visible='true' window=':Open_javax.swing.JDialog'}}
waitForObject $fileDialogEntryName
invoke type $fileDialogEntryName $filename
waitForObject $fileDialogEntryName
invoke type $fileDialogEntryName "<Return>"
}
proc chooseMenuOptionByKey {menuKey optionKey} {
set paneName {{type='javax.swing.JRootPane' visible='true'}}
waitForObject $paneName
invoke type $paneName "<Alt+$menuKey>"
waitForObject $paneName
invoke type $paneName $optionKey
}
proc compareTableWithDataFile {jtable filename} {
set data [testData dataset $filename]
set tableModel [invoke $jtable getModel]
for {set row 0} {$row < [llength $data]} {incr row} {
set columnNames [testData fieldNames [lindex $data $row]]
for {set column 0} {$column < [llength $columnNames]} {incr column} {
set item [invoke $tableModel getValueAt $row $column]
test compare [testData field [lindex $data $row] $column] [invoke $item toString]
}
}
}
The doFileOpen() function begins by opening a file
through the user interface. This is done by using the custom
chooseMenuOptionByKey() function. The file dialog used may
not be the same on all platforms so the name of the text entry (in this
case of type AlignedLabel) may vary, so we have added a note that the
name is platform-specific. Apart from that using the dialog itself is
straightforward—we simply type in the filename into the text entry
and the type Return to confirm the
choice.
The
chooseMenuOptionByKey() function simulates the user
clicking
Alt+k
(where k is a character, for example "F" for the
file menu), and then the character that corresponds to the required
action, (for example, "o" for "Open").
When the file is opened, the program is expected to load the file's
data. We check that the data has been loaded correctly by comparing the
data shown in the JTable and the data file itself. This comparison
is done by the custom compareTableWithDataFile() function.
This function uses Squish's testData.dataset() function to load in the data so
that it can be accessed through the Squish API. We expect every cell
in the table to match the corresponding item in the data, and we check
that this is the case using the test.compare() function.
Now that we know how to compare a table's data with the data in a file
we can perform some more ambitious tests. We will load in the
before.csv file, delete a few rows, insert a new
row in the middle, and append a new row at the end. Then we will swap a
few pairs of columns. At the end the data should match the
after.csv file.
Rather than writing code to do all these things we can simply record a test script that opens the file and performs all the deletions, insertions, and column swaps. Then we can edit the recorded test script to add a few lines of code near the end to compare the actual results with the expected results. Shown below is an extract from the test script starting one line above the hand written code and continuing to the end of the script:
Example 15.29. Extracts from the tst_editing Script
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>")
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>");
}
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>");
}
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.
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.
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.

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 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 |
|---|---|
Java™/SWT's DateTime control for Eclipse 3.4 does not support the setting of date ranges, so although the application does constrain the date ranges using listeners, we cannot easily test this in hand written code. One simple solution is to record a script where an attempt is made to change to an out of range date, and either run that as a separate test or copy and paste the relevant lines into a hand written script. |
We will write three tests, one for each of the form's modes.
The source code for the payment form is in the directory
SQUISHROOT/examples/java/paymentform_swt, and the test
suites are in subdirectories underneath—for example, the Python
version of the tests is in the directory
SQUISHROOT/examples/java/paymentform_swt/suite_py, and
the JavaScript version of the tests is in
SQUISHROOT/examples/java/paymentform_swt/suite_js.
We will begin by reviewing the test script for testing the form's "cash" mode. First we will show the code, then we will explain it.
Example 15.30. The tst_cash_mode Test Script
def main():
startApplication("PaymentFormSWT.class")
# Start with the correct tab
tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder"
tabFolder = waitForObject(tabFolderName)
clickTab(tabFolder, "C&ash")
# Business rule #1: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
amountDueLabelName = ("{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' "
"visible='true' window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
amountDueLabel = waitForObject(amountDueLabelName)
chars = []
for char in unicode(amountDueLabel.getText()):
if char.isdigit():
chars.append(char)
amount_due = cast("".join(chars), int)
maximum = min(2000, amount_due)
paymentSpinnerName = ("{isvisible='true' type='org.eclipse.swt.widgets.Spinner' "
"window=':Payment Form_org.eclipse.swt.widgets.Shell'}")
paymentSpinner = waitForObject(paymentSpinnerName)
test.verify(paymentSpinner.getMinimum() == 1)
test.verify(paymentSpinner.getMaximum() == maximum)
# Business rule #2: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
payButton = waitForObject(payButtonName)
test.verify(payButton.isEnabled())
function main()
{
startApplication("PaymentFormSWT.class");
// Start with the correct tab
var tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
var tabFolder = waitForObject(tabFolderName);
clickTab(tabFolder, "C&ash");
// Business rule #1: the minimum payment is $1 and the maximum is
// $2000 or the amount due whichever is smaller
var amountDueLabelName = "{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' " +
"window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
var amountDueLabel = waitForObject(amountDueLabelName);
var chars = [];
var amountDueText = new String(amountDueLabel.text);
for (var i = 0; i < amountDueText.length; ++i) {
var ch = amountDueText.charAt(i);
if ("0123456789".indexOf(ch) > -1) {
chars.push(ch);
}
}
var amount_due = parseFloat(chars.join(""));
var maximum = Math.min(2000, amount_due);
var paymentSpinnerName = "{isvisible='true' type='org.eclipse.swt.widgets.Spinner' " +
"window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
var paymentSpinner = waitForObject(paymentSpinnerName);
test.verify(paymentSpinner.getMinimum() == 1);
test.verify(paymentSpinner.getMaximum() == maximum);
// Business rule #2: the Pay button is enabled (since the above tests
// ensure that the payment amount is in range)
var payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
var payButton = waitForObject(payButtonName);
test.verify(payButton.isEnabled());
}
sub main
{
startApplication("PaymentFormSWT.class");
# Start with the correct tab
my $tabFolderName = ":Payment Form_org.eclipse.swt.widgets.TabFolder";
my $tabFolder = waitForObject($tabFolderName);
clickTab($tabFolder, "C&ash");
# Business rule #1: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
my $amountDueLabelName = "{caption?='[\$][0-9.,]*' type='org.eclipse.swt.widgets.Label' " .
"visible='true' window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
my $amountDueLabel = waitForObject($amountDueLabelName);
my $amount_due = $amountDueLabel->text;
$amount_due =~ s/\D//g; # remove non-digits
my $maximum = 2000 < $amount_due ? 2000 : $amount_due;
my $paymentSpinnerName = "{isvisible='true' type='org.eclipse.swt.widgets.Spinner' " .
"window=':Payment Form_org.eclipse.swt.widgets.Shell'}";
my $paymentSpinner = waitForObject($paymentSpinnerName);
test::verify($paymentSpinner->getMinimum() == 1);
test::verify($paymentSpinner->getMaximum() == $maximum);
# Business rule #2: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
my $payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
my $payButton = waitForObject($payButtonName);
test::verify($payButton->isEnabled());
}
proc main {} {
startApplication "PaymentFormSWT.class"
# Start with the correct tab
set tabFolderName ":Payment Form_org.eclipse.swt.widgets.TabFolder"
set tabFolder [waitForObject $tabFolderName]
invoke clickTab $tabFolder "C&ash"
# Business rule #1: the minimum payment is $1 and the maximum is
# $2000 or the amount due whichever is smaller
set amountDueLabelName {{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
set amountDueLabel [waitForObject $amountDueLabelName]
set amountText [toString [property get $amountDueLabel text]]
regsub -all {\D} $amountText "" amountText
set amount_due [expr $amountText]
set maximum [expr $amount_due < 2000 ? $amount_due : 2000]
set paymentSpinnerName {{isvisible='true' type='org.eclipse.swt.widgets.Spinner' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
set paymentSpinner [waitForObject $paymentSpinnerName]
test compare [invoke $paymentSpinner getMinimum] 1
test compare [invoke $paymentSpinner getMaximum] $maximum
# Business rule #2: the Pay button is enabled (since the above tests
# ensure that the payment amount is in range)
set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
set payButton [waitForObject $payButtonName]
test verify [invoke $payButton isEnabled]
}
We must start by making sure that the form is in the mode we want to
test. In general, the way we gain access to visible widgets is always
the same: we create a variable holding the widget's name, then we call
waitForObject() to get a reference to the
widget. Once we have the reference we can use it to access the widget's
properties and to call the widget's methods. In this case we use
waitForObject() to get a reference to the
TabFolder widget and then use the clickTab() function to click the tab we are
interested in. How did we know the tab folder's name? We used the How to Use the Spy (Section 15.2.4) facility.
The first business rule to be tested concerns the minimum and maximum
allowed payment amounts. As usual we begin calling
waitForObject() to get
references to the widgets we are interested in—in this case
starting with the amount due label. Because the amount due label's text
varies depending on the amount due we cannot have a fixed name for it.
So instead we identify it using a multiproperty name using wildcards.
The wildcard of [$][0-9.,]* matches any text that starts
with a dollar sign and is followed by zero or more digits, periods and
commas. Squish can also do regular expression matching—see Improving Object Identification (Section 16.8) for more about matching.
Since the label's text might contain a currency symbol and grouping
markers (for example, $1,700 or €1.700), to convert its text into an
integer we must strip away any non-digit characters first. We do this in
different ways depending on the underlying scripting language. (For
example, in Python, we iterate over each character and join all those
that are digits into a single string and use the cast() function which takes an object and the type
the object should be converted to, and returns an object of the
requested type—or 0 on failure. We use a similar approach in
JavaScript, but for Perl and Tcl we simply replace non-digit characters
using a regular expression.) The resulting integer is the amount due, so
we can now trivially calculate the maximum amount that can be paid in
cash.
With the minimum and maximum amounts known we next get a reference to
the payment Spinner, again using Spy to find out the
spinner's name. Once we have a reference to the spinner, we use the
test.verify() method to ensure that is has
the correct minimum and maximum amounts set. (For Tcl we have used the
test.compare() method instead of test.verify() since it is more convenient to do
so.)
Checking the last business rule is easy in this case since if the amount
is in range (and it must be because we have just checked it), then
payment is allowed so the 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
button is enabled.
Although the "cash" mode test works well, there are a few places where
we use essentially the same code. So before creating the test for the
"check" and "card" modes, we will create some common functions that we
can use to refactor our tests with. (The process used to create shared
code is described a little later in How to Create and Use Shared Data and Shared Scripts (Section 15.4)—essentially all we need to do is create
a new script under the Test Suite's shared item's scripts item.) The
Python common code is in common.py, the JavaScript
common code is in common.js, and so on.
Example 15.31. The Shared Code
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)
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);
}
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);
}
proc clickTabItem {name} {
set tabFolderName {{isvisible='true' type='org.eclipse.swt.widgets.TabFolder' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
set tabFolder [waitForObject $tabFolderName]
invoke clickTab $tabFolder $name
}
proc getAmountDue {} {
set amountDueLabelName {{caption?='[$][0-9.,]*' type='org.eclipse.swt.widgets.Label' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}
set amountDueLabel [waitForObject $amountDueLabelName]
set amountText [toString [property get $amountDueLabel text]]
regsub -all {\D} $amountText "" amountText
return [expr $amountText]
}
proc dateTimeEqualsDate {dateTime date} {
set yearsMatch [expr [invoke $dateTime getYear] == [invoke $date get [property get java_util_Calendar YEAR]]]
set monthsMatch [expr [invoke $dateTime getMonth] == [invoke $date get [property get java_util_Calendar MONTH]]]
set daysMatch [expr [invoke $dateTime getDay] == [invoke $date get [property get java_util_Calendar DAY_OF_MONTH]]]
if {$yearsMatch && $monthsMatch && $daysMatch} {
return true
}
return false
}
proc checkPaymentRange {minimum maximum} {
set paymentSpinner [waitForObject {{isvisible='true' type='org.eclipse.swt.widgets.Spinner' window=':Payment Form_org.eclipse.swt.widgets.Shell'}}]
test compare [invoke $paymentSpinner getMinimum] $minimum
test compare [invoke $paymentSpinner getMaximum] $maximum
}
Now we can write our tests for "check" and "card" modes and put more of
our effort into testing the business rules and less into some of the
basic chores. The code for "check" mode is quite long, but we have
broken it down into a main() function—the only
function that Squish will call—and a couple of test-specific
supporting functions that help keep the main() function
short and clear, in addition to making use of the common functions we
saw above.
Example 15.32. The tst_check_mode Test Script's main() function
def main():
startApplication("PaymentFormSWT.class")
# Import functionality needed by more than one test script
source(findFile("scripts", "common.py"))
# Start with the correct tab
clickTabItem("Chec&k")
# Business rule #1: the minimum payment is $10 and the maximum is
# $250 or the amount due whichever is smaller
amount_due = getAmountDue()
checkPaymentRange(10, min(250, amount_due))
# Business rule #2: the check date must be no earlier than 30 days
# ago and no later than tomorrow, and must initially be set to
# today. Here we just check its initial value.
checkDateTime = waitForObject(":Check.Check Date:_DateTime")
today = java_util_Calendar.getInstance()
test.verify(dateTimeEqualsDate(checkDateTime, today))
# Business rule #3: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
payButton = findObject(payButtonName)
test.verify(not payButton.isEnabled())
# Business rule #4: the check must be signed (and if it isn't we
# will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked()
# Business rule #5: the Pay button should be enabled since all the
# previous tests pass, the check is signed and now we have filled in
# the account details
populateCheckFields()
payButton = waitForObject(payButtonName)
test.verify(payButton.isEnabled())
function main()
{
startApplication("PaymentFormSWT.class");
// Import functionality needed by more than one test script
source(findFile("scripts", "common.js"));
// Start with the correct tab
clickTabItem("Chec&k");
// Business rule #1: the minimum payment is $10 and the maximum is
// $250 or the amount due whichever is smaller
var amount_due = getAmountDue();
checkPaymentRange(10, Math.min(250, amount_due));
// Business rule #2: the check date must be no earlier than 30 days
// ago and no later than tomorrow, and must initially be set to
// today. Here we just check its initial value.
var checkDateTime = waitForObject(":Check.Check Date:_DateTime");
var today = java_util_Calendar.getInstance();
test.verify(dateTimeEqualsDate(checkDateTime, today));
// Business rule #3: the Pay button is disabled (since the form's data
// isn't yet valid), so we use findObject() without waiting
var payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
var payButton = findObject(payButtonName);
test.verify(!payButton.isEnabled());
// Business rule #4: the check must be signed (and if it isn't we
// will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked();
// Business rule #5: the Pay button should be enabled since all the
// previous tests pass, the check is signed and now we have filled in
// the account details
populateCheckFields();
var payButton = waitForObject(payButtonName);
test.verify(payButton.isEnabled());
}
sub main
{
startApplication("PaymentFormSWT.class");
# Import functionality needed by more than one test script
source(findFile("scripts", "common.pl"));
# Start with the correct tab
clickTabItem("Chec&k");
# Business rule #1: the minimum payment is $10 and the maximum is
# $250 or the amount due whichever is smaller
my $amount_due = getAmountDue();
checkPaymentRange(10, $amount_due < 250 ? $amount_due : 250);
# Business rule #2: the check date must be no earlier than 30 days
# ago and no later than tomorrow, and must initially be set to
# today. Here we just check its initial value.
my $checkDateTime = waitForObject(":Check.Check Date:_DateTime");
my $today = java_util_Calendar::getInstance();
test::verify(dateTimeEqualsDate($checkDateTime, $today));
# Business rule #3: the Pay button is disabled (since the form's data
# isn't yet valid), so we use findObject() without waiting
my $payButtonName = ":Payment Form.Pay_org.eclipse.swt.widgets.Button";
my $payButton = findObject($payButtonName);
test::verify(!$payButton->isEnabled());
# Business rule #4: the check must be signed (and if it isn't we
# will check the check box ready to test the next rule)
ensureSignedCheckBoxIsChecked();
# Business rule #5: the Pay button should be enabled since all the
# previous tests pass, the check is signed and now we have filled in
# the account details
populateCheckFields();
my $payButton = waitForObject($payButtonName);
test::verify($payButton->isEnabled());
}
proc main {} {
startApplication "PaymentFormSWT.class"
# Import functionality needed by more than one test script
source [findFile "scripts" "common.tcl"]
# Start with the correct tab
clickTabItem "Chec&k"
# Business rule #1: the minimum payment is $10 and the maximum is
# $250 or the amount due whichever is smaller
set amount_due [getAmountDue]
checkPaymentRange 10 [expr 250 > $amount_due ? $amount_due : 250]
# Business rule #2: the check date must be no earlier than 30 days
# ago and no later than tomorrow, and must initially be set to
# today. Here we just check its initial value.
set checkDateTime [waitForObject ":Check.Check Date:_DateTime"]
set today [invoke java_util_Calendar getInstance]
test verify [dateTimeEqualsDate $checkDateTime $today]
# Business rule #3: the Pay button is disabled (since the form's data