How to Test Java Applications

Squish's Java-specific API enables you to find and query objects, as well as to access properties and methods:

In addition, the Java Convenience API provides functions for executing common actions on GUI applications, such as clicking a button or entering some text.

For examples of how to use the scripting Java API to access and test complex Java GUI elements, including list, table, and tree widgets, see How to Test Applications. Separate examples are provided for AWT/Swing and for SWT applications, although the same principles apply to both of them.

Java Properties

In this context, properties means fields in Java. That is, classes with methods that 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 orisXyz, 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. In the example above, (assuming only one of getSomething or isSomething is defined), Squish will create a property called something.

How to Find and Query Java Objects

The Object waitForObject(objectOrName) function 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 Object findObject(objectName) 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 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 maintenance easier. (See Editing an Object Map for more about handling object hierarchy changes.)
  • Multiple property (real) name—a list of property-name=value pairs in curly braces that uniquely identifies the object. Squish will search the GUI parent–child hierarchy until it finds a matching object. Here is an example of such a name: {caption='Pay' type='javax.swing.JButton' visible='true' window=':Payment Form_PaymentForm'}. To be valid for most GUI toolkits a multi-property name (also called a "real name") must include a type property. However, Squish for Web and Squish for Windows don't need a type property specified—but will use it if it is present of course. Notice also that in this example another object was referred to (the window); and in this case the reference used a symbolic name. In general using symbolic names is more robust, but if we need to identify an object with a variable property (for example, a caption that might change), then we must use a multi-property name, since this naming scheme supports wildcards. (See Improving Object Identification 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 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 Object waitForObject(objectOrName) 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 Object waitForObject(objectOrName) 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:

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");
};
begin
    textField = waitForObject(
        ":Credit Card.Account Name:_javax.swing.JTextField")
    # here we can use the textField object reference
rescue LookupError => err
    Test.fail("Could not find the account name text field")
end
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:

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

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

If we expect an object to be hidden but nonetheless, present (for example, on a Tab page that isn't the current one), we can still access it, but this time we cannot use the Object waitForObject(objectOrName) function—which only works for visible objects—but instead must use the Boolean object.exists(objectName) function in conjunction with the Object findObject(objectName) 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")
    textField = findObject(
        ":Credit Card.Account Name:_javax.swing.JTextField")
    if textField
    Test.pass("Correctly found the hidden object")
    end
end
if {[object exists ":Credit Card.Account Name:_javax.swing.JTextField"]} {
    set textField [findObject \
        ":Credit Card.Account Name:_javax.swing.JTextField"]
    if {![isNull $textField]} {
    test pass "Correctly found the hidden object"
    }
}

Using these techniques it is possible to query and access every object in the AUT's object hierarchy, regardless of whether they are visible.

How to Call Functions on Java Objects

Squish makes it possible to call any public function on any Java object. (See How to Find and Query Objects 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
s = java_lang_String.new("A string")
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. See also, Wrapping custom classes.

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");
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) function:

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

Passing a null value

A Java null can be created as follows. Assume a Java method call that needs a java.lang.Object as an argument.

obj.doFoo(object.createNull(java_lang_Object))
obj.doFoo(object.createNull(java_lang_Object));
$obj->doFoo(object::createNull(java_lang_Object));
obj.doFoo(Object.createNull(LC::Java_lang_Object))
invoke $obj doFoo [object createNull java_lang_Object]

How to Access Java Object Properties

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

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);
point = java_awt_Point.new(5, 8)
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 API 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);
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: Java Synthetic properties, that is, properties created by Squish based on function signatures, make it easier to add more verification points to test scripts. See How to Create and Use Verification Points for more details.

How to Use the Java Convenience API

This section describes the script API Squish offers on top of Java's API to make it easy to perform common user actions such as clicking a button, entering text, etc. A complete list of the API is available in the Java Convenience API section in the Tools Reference. 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");
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 clicked a button, typed in some text, and activated a menu and a menu item. For more examples that 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 Applications.

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.

How to Create and Access Java Arrays

Accessing Java Arrays

Some of the methods in the Java API return Arrays (called JavaArrays in Squish) rather than single objects. The number of elements in such an array is accessible using the JavaArray.length property, and individual elements can be accessed using the Object JavaArray.at(index) method parametrized 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");
}
tabPane = waitForObject(":Payment Form_javax.swing.JTabbedPane")
components = tabPane.getComponents()
for i in 0...components.length
    Test.log("Component ##{i}: #{components.at(i)}")
end
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 How to Test Tree's tst_tree test script.

Creating and Using JavaArrays

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

    variousObjects = JavaArray(5) # creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(names.payment_Form_Cancel_javax_swing_JButton))
    variousObjects.set(1, java_lang_Object())
    variousObjects.set(2, 4)           # converted to java.lang.Integer
    variousObjects.set(3, "some text") # converted to java.lang.String
    test.compare(variousObjects.at(0).text, "Cancel")
    test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object")
    test.compare(variousObjects.at(2), 4)
    test.compare(variousObjects.at(3), "some text")
    test.verify(variousObjects.length == 5)

    integers = JavaArray(42, "int") # creates an int[42] array
    integers.set(23, -71)
    test.compare(integers.at(23), -71)
    test.verify(integers.length == 42)

    strings = JavaArray(10, "java.lang.String") # java.lang.String[10]
    strings.set(4, "more text")
    test.compare(strings.at(4), "more text")
    test.verify(strings.length == 10)
    var variousObjects = new JavaArray(5); // creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(names.paymentFormCancelJavaxSwingJButton));
    variousObjects.set(1, new java_lang_Object());
    variousObjects.set(2, 4);           // converted to java.lang.Integer
    variousObjects.set(3, "some text"); // converted to java.lang.String
    test.compare(variousObjects.at(0).text, "Cancel");
    test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object");
    test.compare(variousObjects.at(2), 4);
    test.compare(variousObjects.at(3), "some text");
    test.verify(variousObjects.length == 5);

    var integers = new JavaArray(42, "int"); // creates an int[42] array
    integers.set(23, -71);
    test.compare(integers.at(23), -71);
    test.verify(integers.length == 42);

    var strings = new JavaArray(10, "java.lang.String"); // java.lang.String[10]
    strings.set(4, "more text");
    test.compare(strings.at(4), "more text");
    test.verify(strings.length == 10);
    my $variousObjects = JavaArray->new(5); # creates a java.lang.Object[5] array
    $variousObjects->set(0, waitForObject($Names::payment_form_cancel_javax_swing_jbutton));
    $variousObjects->set(1, java_lang_Object->new());
    $variousObjects->set(2, 4);           # converted to java.lang.Integer
    $variousObjects->set(3, "some text"); # converted to java.lang.String
    #test::compare($variousObjects->at(0)->text, "Cancel");
    test::compare($variousObjects->at(1)->getClass()->getName(), "java.lang.Object");
    test::compare($variousObjects->at(2), 4);
    test::compare($variousObjects->at(3), "some text");
    test::verify($variousObjects->length == 5);

    my $integers = JavaArray->new(42, "int"); # creates an int[42] array
    $integers->set(23, -71);
    test::compare($integers->at(23), -71);
    test::verify($integers->length == 42);

    my $strings = JavaArray->new(10, "java.lang.String"); # java.lang.String[10]
    $strings->set(4, "more text");
    test::compare($strings->at(4), "more text");
    test::verify($strings->length == 10);
    variousObjects = JavaArray.new(5) # creates a java.lang.Object[5] array
    variousObjects.set(0, waitForObject(Names::Payment_Form_Cancel_javax_swing_JButton))
    variousObjects.set(1, LC::Java_lang_Object.new)
    variousObjects.set(2, 4)                                     # converted to java.lang.Integer
    variousObjects.set(3, "some text") # converted to java.lang.String
    Test.compare(variousObjects.at(0).text, "Cancel")
    Test.compare(variousObjects.at(1).getClass().getName(), "java.lang.Object")
    Test.compare(variousObjects.at(2), 4)
    Test.compare(variousObjects.at(3), "some text")
    Test.verify(variousObjects.length == 5)

    integers = JavaArray.new(42, "int") # creates an int[42] array
    integers.set(23, -71)
    Test.compare(integers.at(23), -71)
    Test.verify(integers.length == 42)

    strings = JavaArray.new(10, "java.lang.String") # java.lang.String[10]
    strings.set(4, "more text")
    Test.compare(strings.at(4), "more text")
    Test.verify(strings.length == 10)
    set variousObjects [construct JavaArray 5]
    invoke $variousObjects set 0 [waitForObject ":Payment Form.Cancel_javax.swing.JButton"]
    invoke $variousObjects set 1 [construct java_lang_Object]
    # converted to java.lang.Integer
    invoke $variousObjects set 2 4
    # converted to java.lang.String
    invoke $variousObjects set 3 "some text"
    test compare [property get [invoke $variousObjects at 0] text] "Cancel"
    test compare [invoke [invoke [invoke $variousObjects at 1] getClass] getName] "java.lang.Object"
    test compare [invoke $variousObjects at 2] 4
    test compare [invoke $variousObjects at 3] "some text"
    test compare [property get $variousObjects length] 5

    # creates an int[42] array
    set integers [construct JavaArray 42 "int"]
    invoke $integers set 23 [expr -71]
    test compare [invoke $integers at 23] -71
    test compare [property get $integers length] 42

    # java.lang.String[10]
    set strings [construct JavaArray 10 "java.lang.String"]
    invoke $strings set 4 "more text"
    test compare [invoke $strings at 4] "more text"
    test compare [property get $strings length] 10

The JavaArray API is documented in Native Java Arrays.

How to Test Java Applications

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

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

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

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 properties—with the JavaScript, Perl, Python, Ruby, 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.

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 state that we expect.

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

How to Test Java AWT/Swing Applications

In the subsections that follow we will focus on testing Java AWT/Swing widgets, both single-valued widgets like buttons and spinners, and multi-valued widgets such as lists, tables, and trees. We will also cover testing using external data files. (If you are using Java SWT, skip ahead to How to Test SWT Applications.)

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

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

{}

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

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

  • In "cash" mode, i.e., when the Cash tab is checked:
    • The minimum payment is one dollar and the maximum is $2000 or the amount due, whichever is smaller.
  • In "check" mode, i.e., when the Check tab is checked:
    • The minimum payment is $10 and the maximum is $250 or the amount due, whichever is smaller.
    • The check date must be no earlier than 30 days ago and no later than tomorrow.
    • The bank name, bank number, account name, and account number line edits must all be nonempty.
    • The check signed checkbox must be checked.
  • In "card" mode, i.e., when the Credit Card tab is checked:
    • The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.
    • For non-Visa cards the issue date must be no earlier than three years ago.
    • The expiry date must be at least one month later than today.
    • The account name and account number line edits must be nonempty.

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

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

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.

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform/PaymentFormSwing.jar"')
    # 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 str(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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform/PaymentFormSwing.jar"');
    // 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 amount_due = 0 + amountDueLabel.text.replace(/\D/g, "");
    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("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform/PaymentFormSwing.jar\"");
    # 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);
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform/PaymentFormSwing.jar\"")
    # 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)
    amount_due = String(amountDueLabel.text).gsub(/\D/, "").to_f
    maximum = 2000 < amount_due ? 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)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform/PaymentFormSwing.jar\""
    # 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 the Object waitForObject(objectOrName) function 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 call the Object waitForObject(objectOrName) function on the tab's name since we don't need a reference to the tab; instead we just use the clickTab(objectOrName, tabText) 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 Object waitForObject(objectOrName) to get references to the objects we need to access—in this case starting with the amount due label. Because the amount due label's text varies depending on the amount we cannot have a fixed name for it. So instead we identify it using a multi-property (real) 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 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 Object cast(object, type) 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 strip out 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. Alternatively, 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 Boolean test.verify(condition) function to ensure that the model has the correct minimum and maximum amounts set. (For Tcl we have used the Boolean test.compare(value1, value2) function instead of Boolean test.verify(condition) since it is more convenient to do so.)

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

Although the "cash" mode test works well, there are a few places where we use essentially the same code. So before creating the test for the "check" and "card" modes, we will create some common functions that we can use to refactor our tests with. (The process used to create shared code is described a little later in How to Create and Use Shared Data and Shared Scripts—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.

    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 str(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'}")
        return 0 + amountDueLabel.text.replace(/\D/g, "");
    }


    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);
    }
    # encoding: UTF-8
    require 'squish'
    include Squish

    def clickTabbedPane(text)
        waitForObject(":Payment Form.#{text}_com.froglogic.squish.awt.TabProxy")
        clickTab(":Payment Form.#{text}_com.froglogic.squish.awt.TabProxy")
    end


def getAmountDue
        amountDueLabel = waitForObject("{caption?='[$][0-9.,]*' " +
                "type='javax.swing.JLabel' visible='true' " +
                "window=':Payment Form_PaymentForm'}")
        String(amountDueLabel.text).gsub(/\D/, "").to_f
    end

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

    def min(a, b)
        a < b ? a : b
    end

    def max(a, b)
        a < b ? b : a
    end
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 manageable. 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.

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform/PaymentFormSwing.jar"')
    # 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 = names.payment_Form_Pay_javax_swing_JButton
    payButton = findObject(payButtonName)
    test.verify(not payButton.enabled)

    # 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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform/PaymentFormSwing.jar"');
    // 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 = names.paymentFormPayJavaxSwingJButton;
    var payButton = findObject(payButtonName);
    test.verify(!payButton.enabled);

    // 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("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform/PaymentFormSwing.jar\"");
    # 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
    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
    my $payButtonName = $Names::payment_form_pay_javax_swing_jbutton;
    my $payButton = findObject($payButtonName);
    test::verify(!$payButton->enabled);

    # 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 = findObject($payButtonName);
    test::verify($payButton->enabled);
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform/PaymentFormSwing.jar\"")
    # Import functionality needed by more than one test script
    require findFile("scripts", "common.rb")

    # 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 = Names::Payment_Form_Pay_javax_swing_JButton
    payButton = findObject(payButtonName)
    Test.verify(!payButton.enabled)

    # 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)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform/PaymentFormSwing.jar\""
    # 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 verify [expr ![property get $payButton enabled]]

    # Business rule #4: the check must be signed (and if it isn't we
    # will check the check box ready to test the next rule)
    ensureSignedCheckBoxIsChecked

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, the check is signed and now we have filled in
    # the account details
    populateCheckFields
    set payButton [waitForObject $payButtonName]
    test verify [property get $payButton enabled]
}

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

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

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

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

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

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(names.check_JPanel)
    model = checkDateSpinner.getModel()
    AnyClass = calendar.getClass()
    SimpleDateFormatClass = AnyClass.forName("java.text.SimpleDateFormat")
    formatter = SimpleDateFormatClass.newInstance()
    formatter.applyPattern("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(earliest))
    test.verify(formatter.format(model.getEnd()) ==
function checkDateRange(daysBeforeToday, daysAfterToday)
{
    var checkDateSpinner = waitForObject({"container": names.paymentFormCheckComFroglogicSquishAwtTabProxy, "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 AnyClass = calendar.getClass();
    var SimpleDateFormatClass = AnyClass.forName(
        "java.text.SimpleDateFormat");
    var formatter = SimpleDateFormatClass.newInstance();
    formatter.applyPattern("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(earliest));
    test.verify(formatter.format(model.getEnd()) ==
                formatter.format(latest));
}
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($Names::check_check_date_jspinner);
    my $model = $checkDateSpinner->getModel();
    my $simpleDateFormatClass = java_lang_Class::forName("java.text.SimpleDateFormat");
    my $formatter = $simpleDateFormatClass->newInstance();
    $formatter->applyPattern("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq
                 $formatter->format($earliest));
    test::verify($formatter->format($model->getEnd()) eq
                 $formatter->format($latest));
}
def checkDateRange(daysBeforeToday, daysAfterToday)
    calendar = LC::Java_util_Calendar.getInstance()
    calendar.add(LC::Java_util_Calendar.DAY_OF_MONTH, daysBeforeToday)
    earliest = calendar.getTime()
    calendar.setTime(LC::Java_util_Date.new)
    calendar.add(LC::Java_util_Calendar.DAY_OF_MONTH, daysAfterToday)
    latest = calendar.getTime()
    checkDateSpinner = waitForObject({:basetype => "javax.swing.JPanel", :container => Names::Payment_Form_Check_com_froglogic_squish_awt_TabProxy, :occurrence => 1, :visible => true})
    model = checkDateSpinner.getModel()
    simpleDateFormatClass = LC::Java_lang_Class.forName("java.text.SimpleDateFormat")
    formatter = simpleDateFormatClass.newInstance()
    formatter.applyPattern("yyyy-MM-dd")
    Test.verify(formatter.format(model.getStart()) ==
    formatter.format(earliest))
    Test.verify(formatter.format(model.getEnd()) ==
    formatter.format(latest))
end
proc checkDateRange {daysBeforeToday daysAfterToday} {
    set checkDateSpinner [waitForObject \
        {{container=':Payment Form.Check_com.froglogic.squish.awt.TabProxy' \
            type='javax.swing.JSpinner' visible='true'}}]
    set AnyClass [invoke $checkDateSpinner getClass]
    set SimpleDateFormatClass [invoke $AnyClass forName \
        "java.text.SimpleDateFormat"]
    set formatter [invoke $SimpleDateFormatClass newInstance]
    invoke $formatter applyPattern "yyyy-MM-dd"
    set model [invoke $checkDateSpinner getModel]
    set minimumAllowed [invoke $formatter format [invoke $model getStart]]
    set maximumAllowed [invoke $formatter format [invoke $model getEnd]]
    set calendar [invoke java_util_Calendar getInstance]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] \
        $daysBeforeToday
    set minimumDate [invoke $formatter format [invoke $calendar getTime]]
    invoke $calendar setTime [construct java_util_Date]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] \
        $daysAfterToday
    set maximumDate [invoke $formatter format [invoke $calendar getTime]]
    test compare $minimumAllowed $minimumDate
    test compare $maximumAllowed $maximumDate
}

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

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

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

In fact, we only used this approach for the Perl version; for the others we used Java's introspection facilities to create instances of the java.text.DateFormat and java.text.SimpleDateFormat classes.

def ensureSignedCheckBoxIsChecked():
    checkSignedCheckBox = waitForObject(
            names.check_Check_Signed_javax_swing_JCheckBox)
    if not checkSignedCheckBox.isSelected():
        clickButton(checkSignedCheckBox)
    test.verify(checkSignedCheckBox.isSelected())
function ensureSignedCheckBoxIsChecked()
{
    var checkSignedCheckBox = waitForObject(
            names.checkCheckSignedJavaxSwingJCheckBox);
    if (!checkSignedCheckBox.isSelected()) {
        clickButton(checkSignedCheckBox);
        }
    test.verify(checkSignedCheckBox.isSelected());
}
sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedCheckBox = waitForObject(
            $Names::check_check_signed_javax_swing_jcheckbox);
    if (!$checkSignedCheckBox->isSelected()) {
        clickButton($checkSignedCheckBox);
    }
    test::verify($checkSignedCheckBox->isSelected());
}
def ensureSignedCheckBoxIsChecked
    checkSignedCheckBox = waitForObject(
    Names::Check_Check_Signed_javax_swing_JCheckBox)
    if !checkSignedCheckBox.isSelected()
        clickButton(checkSignedCheckBox)
    end
    Test.verify(checkSignedCheckBox.isSelected())
end
proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedCheckBox [waitForObject \
        ":Check.Check Signed_javax.swing.JCheckBox"]
    if {![invoke $checkSignedCheckBox isSelected]} {
        invoke clickButton $checkSignedCheckBox
    }
    test verify [invoke $checkSignedCheckBox isSelected]
}

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

def populateCheckFields():
    bankNameLineEdit = waitForObject(
        names.check_Bank_Name_javax_swing_JTextField)
    type(bankNameLineEdit, "A Bank")
    bankNumberLineEdit = waitForObject(
        names.check_Bank_Number_javax_swing_JTextField)
    type(bankNumberLineEdit, "88-91-33X")
    accountNameLineEdit = waitForObject(
        names.check_Account_Name_javax_swing_JTextField)
    type(accountNameLineEdit, "An Account")
    accountNumberLineEdit = waitForObject(
        names.check_Account_Number_javax_swing_JTextField)
    type(accountNumberLineEdit, "932745395")
function populateCheckFields()
{
    var bankNameLineEdit = waitForObject(
        names.checkBankNameJavaxSwingJTextField);
    type(bankNameLineEdit, "A Bank");
    var bankNumberLineEdit = waitForObject(
        names.checkBankNumberJavaxSwingJTextField);
    type(bankNumberLineEdit, "88-91-33X");
    var accountNameLineEdit = waitForObject(
        names.checkAccountNameJavaxSwingJTextField);
    type(accountNameLineEdit, "An Account");
    var accountNumberLineEdit = waitForObject(
        names.checkAccountNumberJavaxSwingJTextField);
    type(accountNumberLineEdit, "932745395");
}
sub populateCheckFields
{
    my $bankNameLineEdit = waitForObject(
        $Names::check_bank_name_javax_swing_jtextfield);
    type($bankNameLineEdit, "A Bank");
    my $bankNumberLineEdit = waitForObject(
        $Names::check_bank_number_javax_swing_jtextfield);
    type($bankNumberLineEdit, "88-91-33X");
    my $accountNameLineEdit = waitForObject(
        $Names::check_account_name_javax_swing_jtextfield);
    type($accountNameLineEdit, "An Account");
    my $accountNumberLineEdit = waitForObject(
        $Names::check_account_number_javax_swing_jtextfield);
    type($accountNumberLineEdit, "932745395");
}
def populateCheckFields
    bankNameLineEdit = waitForObject(
    Names::Check_Bank_Name_javax_swing_JTextField)
    type(bankNameLineEdit, "A Bank")
    bankNumberLineEdit = waitForObject(
    Names::Check_Bank_Number_javax_swing_JTextField)
    type(bankNumberLineEdit, "88-91-33X")
    accountNameLineEdit = waitForObject(
    Names::Check_Account_Name_javax_swing_JTextField)
    type(accountNameLineEdit, "An Account")
    accountNumberLineEdit = waitForObject(
    Names::Check_Account_Number_javax_swing_JTextField)
    type(accountNumberLineEdit, "932745395")
end
proc populateCheckFields {} {
    set bankNameLineEdit [waitForObject \
        ":Check.Bank Name:_javax.swing.JTextField"]
    invoke type $bankNameLineEdit "A Bank"
    set bankNumberLineEdit [waitForObject \
        ":Check.Bank Number:_javax.swing.JTextField"]
    invoke type $bankNumberLineEdit "88-91-33X"
    set accountNameLineEdit [waitForObject \
        ":Check.Account Name:_javax.swing.JTextField"]
    invoke type $accountNameLineEdit "An Account"
    set accountNumberLineEdit [waitForObject \
        ":Check.Account Number:_javax.swing.JTextField"]
    invoke type $accountNumberLineEdit "932745395"
}

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

Notice that we used the type(objectOrName, text) 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.

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform/PaymentFormSwing.jar"')
    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 = names.payment_Form_Pay_javax_swing_JButton
    payButton = findObject(payButtonName)
    test.verify(not payButton.enabled)

    # 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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform/PaymentFormSwing.jar"');
    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 = names.paymentFormPayJavaxSwingJButton;
    var payButton = findObject(payButtonName);
    test.verify(!payButton.enabled);

    // 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("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform/PaymentFormSwing.jar\"");
    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 = $Names::payment_form_pay_javax_swing_jbutton;
    my $payButton = findObject($payButtonName);
    test::verify(!$payButton->enabled);

    # 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);
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform/PaymentFormSwing.jar\"")
    require findFile("scripts", "common.rb")

    # 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 = Names::Payment_Form_Pay_javax_swing_JButton
    payButton = findObject(payButtonName)
    Test.verify(!payButton.enabled)

    # 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)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform/PaymentFormSwing.jar\""
    # 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 verify [expr ![property get $payButton enabled]]

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    set payButton [waitForObject $payButtonName]
    test verify [property get $payButton enabled]
}

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

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

def checkCardDateEdits():
    # (1) set the card type to any non-Visa card
    cardTypeComboBox = waitForObject(
        names.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(names.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()
    java_lang_Class.forName("java.text.SimpleDateFormat")
    formatter = java_text_SimpleDateFormat()
    formatter.applyPattern("yyyy-MM-dd")
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(date))
    # (4) set the expiry date more than a month later
    # than now for later tests
    calendar.setTime(java_util_Date())
    calendar.add(java_util_Calendar.DAY_OF_MONTH, 35)
    expiryDateSpinner = spinners[1]
    expiryDateSpinner.setValue(calendar.getTime())
function checkCardDateEdits()
{
    // (1) set the card type to any non-Visa card
    var cardTypeComboBox = waitForObject(
        names.creditCardCardTypeJavaxSwingJComboBox);
    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(names.paymentFormCreditCardComFroglogicSquishAwtTabProxy).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();
    java_lang_Class.forName("java.text.SimpleDateFormat");
    var formatter = new java_text_SimpleDateFormat();
    formatter.applyPattern("yyyy-MM-dd");
    test.verify(formatter.format(model.getStart()) ==
                formatter.format(threeYearsAgo));
    // (4) set the expiry date more than a month later
    // than now for later tests
    calendar.setTime(new java_util_Date());
    calendar.add(java_util_Calendar.DAY_OF_MONTH, 35);
    var expiryDateSpinner = spinners[1];
    expiryDateSpinner.setValue(calendar.getTime());
}
sub checkCardDateEdits
{
    # (1) set the card type to any non-Visa card
    my $cardTypeComboBox = waitForObject(
        $Names::credit_card_card_type_javax_swing_jcombobox);
    for (my $index = 0; $index < $cardTypeComboBox->getItemCount();
        ++$index) {
        if ($cardTypeComboBox->getItemAt($index)->toString() != "Visa") {
            $cardTypeComboBox->setSelectedIndex($index);
            last;
        }
    }
    # (2) find the two date spinners
    my $creditCardTabPane = waitForObject($Names::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();
    java_lang_Class::forName("java.text.SimpleDateFormat");
    my $formatter = new java_text_SimpleDateFormat();
    $formatter->applyPattern("yyyy-MM-dd");
    test::verify($formatter->format($model->getStart()) eq
                 $formatter->format($date));
    # (4) set the expiry date more than a month later than
    # now for later tests
    $calendar->setTime(java_util_Date->new());
    $calendar->add(java_util_Calendar->DAY_OF_MONTH, 35);
    my $expiryDateSpinner = $spinners[1];
    $expiryDateSpinner->setValue($calendar->getTime());
}
def checkCardDateEdits
    # (1) set the card type to any non-Visa card
    cardTypeComboBox = waitForObject(
    Names::Credit_Card_Card_Type_javax_swing_JComboBox)
    for index in 0...cardTypeComboBox.getItemCount()
        if cardTypeComboBox.getItemAt(index).toString() != "Visa"
            cardTypeComboBox.setSelectedIndex(index)
            break
        end
    end
    # (2) find the two date spinners
    creditCardTabPane = waitForObject({:caption => "Credit Card", :type => "com.froglogic.squish.awt.TabProxy", :window => Names::Payment_Form_PaymentForm}).component
    spinners = []
    for i in 0...creditCardTabPane.getComponentCount()
        component = creditCardTabPane.getComponent(i)
        if (component.getClass().toString() ==
        "class javax.swing.JSpinner")
            spinners << component
        end
    end
    Test.verify(spinners.length == 2)
    # (3) check the issue date spinner's minimum date
    calendar = LC::Java_util_Calendar.getInstance()
    calendar.add(LC::Java_util_Calendar.YEAR, -3)
    date = calendar.getTime()
    issueDateSpinner = spinners[0]
    model = issueDateSpinner.getModel()
    LC::Java_lang_Class.forName("java.text.SimpleDateFormat")
    formatter = LC::Java_text_SimpleDateFormat.new
    formatter.applyPattern("yyyy-MM-dd")
    Test.verify(formatter.format(model.getStart()) ==
    formatter.format(date))
    # (4) set the expiry date more than a month later
    # than now for later tests
    calendar.setTime(LC::Java_util_Date.new())
    calendar.add(LC::Java_util_Calendar.DAY_OF_MONTH, 35)
    expiryDateSpinner = spinners[1]
    expiryDateSpinner.setValue(calendar.getTime())
end
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]
    invoke java_lang_Class forName "java.text.SimpleDateFormat"
    set formatter [construct java_text_SimpleDateFormat]
    invoke $formatter applyPattern "yyyy-MM-dd"
    set minimumAllowed [invoke $formatter format [invoke $model getStart]]
    set minimumDate [invoke $formatter format [invoke $calendar getTime]]
    test compare $minimumAllowed $minimumDate
    # (4) set the expiry date more than a month later than now for
    # later tests
    invoke $calendar setTime [construct java_util_Date]
    invoke $calendar add [property get java_util_Calendar DAY_OF_MONTH] 35
    set expiryDateSpinner [lindex $spinners 1]
    invoke $expiryDateSpinner setValue [invoke $calendar getTime]
}

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

This form has two JSpinners, one used for the card's issue date and the other for the card's expiry date. We can't use names to distinguish 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 explicitly set the expiry to be 35 days ahead so that the Pay button will be enabled later on.

def populateCardFields():
    cardAccountNameLineEdit = waitForObject(
        names.credit_Card_Account_Name_javax_swing_JTextField)
    type(cardAccountNameLineEdit, "An Account")
    cardAccountNumberLineEdit = waitForObject(
        names.credit_Card_Account_Number_javax_swing_JTextField)
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32")
function populateCardFields()
{
    var cardAccountNameLineEdit = waitForObject(
        names.creditCardAccountNameJavaxSwingJTextField);
    type(cardAccountNameLineEdit, "An Account");
    var cardAccountNumberLineEdit = waitForObject(
        names.creditCardAccountNumberJavaxSwingJTextField);
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32");
}
sub populateCardFields
{
    my $cardAccountNameLineEdit = waitForObject(
        $Names::credit_card_account_name_javax_swing_jtextfield);
    type($cardAccountNameLineEdit, "An Account");
    my $cardAccountNumberLineEdit = waitForObject(
        $Names::credit_card_account_number_javax_swing_jtextfield);
    type($cardAccountNumberLineEdit, "1343 876 326 1323 32");
}
def populateCardFields
    cardAccountNameLineEdit = waitForObject(
    Names::Credit_Card_Account_Name_javax_swing_JTextField)
    type(cardAccountNameLineEdit, "An Account")
    cardAccountNumberLineEdit = waitForObject(
    Names::Credit_Card_Account_Number_javax_swing_JTextField)
    type(cardAccountNumberLineEdit, "1343 876 326 1323 32")
end
proc populateCardFields {} {
    set cardAccountNameLineEdit [waitForObject \
        ":Credit Card.Account Name:_javax.swing.JTextField"]
    invoke type $cardAccountNameLineEdit "An Account"
    set cardAccountNumberLineEdit [waitForObject \
        ":Credit Card.Account Number:_javax.swing.JTextField"]
    invoke type $cardAccountNumberLineEdit "1343 876 326 1323 32"
}

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

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

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

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

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

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

How to Test JList

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

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews/ItemViewsSwing.jar"')
    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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews/ItemViewsSwing.jar"');
    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("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews/ItemViewsSwing.jar\"");
    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");
    }
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews/ItemViewsSwing.jar\"")
    listWidgetName = ":Item Views_javax.swing.JList"
    listWidget = waitForObject(listWidgetName)
    model = listWidget.getModel()
    for row in 0...model.getSize()
        item = model.getElementAt(row)
        selected = ""
        if listWidget.isSelectedIndex(row)
            selected = " +selected"
        end
        Test.log("(#{row}) '#{item.toString()}'#{selected}")
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews/ItemViewsSwing.jar\""
    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.

How to Test JTable

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

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews/ItemViewsSwing.jar"')
    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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews/ItemViewsSwing.jar"');
    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("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews/ItemViewsSwing.jar\"");
    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");
        }
    }
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews/ItemViewsSwing.jar\"")
    tableWidgetName = ":Item Views_javax.swing.JTable"
    tableWidget = waitForObject(tableWidgetName)
    model = tableWidget.getModel()
    for row in 0...model.getRowCount()
        for column in 0...model.getColumnCount()
            item = model.getValueAt(row, column)
            selected = ""
            if tableWidget.isCellSelected(row, column)
                selected = " +selected"
            end
            Test.log("(#{row}, #{column}) '#{item}'#{selected}")
        end
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews/ItemViewsSwing.jar\""
    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.

Note: You can use table verification points to check an entire table, as instructed in How to Create and Use Table Verifications.

How to Test JTree

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

    import os
    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('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews/ItemViewsSwing.jar"')
        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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews/ItemViewsSwing.jar"');
        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("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews/ItemViewsSwing.jar\"");
    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);
}
# encoding: UTF-8
require 'squish'
include Squish

def checkAnItem(indent, model, item, selectionModel, treePath)
    if indent > -1
        selected = ""
        if selectionModel.isPathSelected(treePath)
            selected = " +selected"
        end
        Test.log("|%s'%s'%s" % [" " * indent, item, selected])
    else
        indent = -4
    end
    for row in 0...model.getChildCount(item)
        child = model.getChild(item, row)
        childTreePath = treePath.pathByAddingChild(child)
        checkAnItem(indent + 4, model, child, selectionModel, childTreePath)
    end
end

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews/ItemViewsSwing.jar\"")
    treeWidgetName = ":Item Views_javax.swing.JTree"
    treeWidget = waitForObject(treeWidgetName)
    model = treeWidget.getModel()
    selectionModel = treeWidget.getSelectionModel()
    treePath = LC::Javax_swing_tree_TreePath.new(model.getRoot())
    checkAnItem(-1, model, model.getRoot(), selectionModel, treePath)
end
    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 "\"$::env(SQUISH_PREFIX)/examples/java/itemviews/ItemViewsSwing.jar\""
        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.

How to Test JTable and Use External Data Files (Java—AWT/Swing)

In this section we will see how to test the CsvTable.java program shown below. This program uses a JTable to present the contents of a .csv (comma-separated values) file, and provides some basic functionality for manipulating the data—inserting and deleting rows and swapping columns. As we review the tests we will learn how to import test data, manipulate the data, and compare what the JTable shows with what we expect its contents to be. And since the CSV Table program is a main-window-style application, we will also learn how to test that menu options behave as expected.

{}

The CSV Table program.

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

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 complete main function:

import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/csvtable/CsvTableSwing.jar"')
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename)
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    compareTableWithDataFile(jtable, filename)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/csvtable/CsvTableSwing.jar"');
    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("\"$ENV{'SQUISH_PREFIX'}/examples/java/csvtable/CsvTableSwing.jar\"");
    source(findFile("scripts", "common.pl"));
    my $filename = "before.csv";
    doFileOpen($filename);
    my $jtable = waitForObject("{type='javax.swing.JTable' visible='true'}");
    compareTableWithDataFile($jtable, $filename);
}
# encoding: UTF-8
require 'squish'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/csvtable/CsvTableSwing.jar\"")
    require findFile("scripts", "common.rb")
    filename = "before.csv"
    doFileOpen(filename)
    jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    compareTableWithDataFile(jtable, filename)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/csvtable/CsvTableSwing.jar\""
    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 Object waitForObject(objectOrName) 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 for how to import test data into Squish.)

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

def doFileOpen(filename):
    chooseMenuOptionByKey("F", "o")
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    # Platform-specific name
    fileDialogEntryName = names.open_File_Name_JTextField
    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 = names.openFileNameJTextField;
    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));
        }
    }
}
def doFileOpen(filename)
    chooseMenuOptionByKey("F", "o")
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    # Platform-specific name
    fileDialogEntryName = Names::Open_File_Name_JTextField
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, filename)
    waitForObject(fileDialogEntryName)
    type(fileDialogEntryName, "<Return>")
end

def chooseMenuOptionByKey(menuKey, optionKey)
    paneName = "{type='javax.swing.JRootPane' visible='true'}"
    waitForObject(paneName)
    type(paneName, "<Alt+#{menuKey}>")
    waitForObject(paneName)
    type(paneName, optionKey)
end

def compareTableWithDataFile(jtable, filename)
    tableModel = jtable.getModel()
    TestData.dataset(filename).each_with_index do
        |record, row|
        for column in 0...TestData.fieldNames(record).length
            text = tableModel.getValueAt(row, column).toString()
            Test.compare(TestData.field(record, column), text)
        end
    end
end
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 with the data in the file. This comparison is done by the custom compareTableWithDataFile function. This function uses Squish's Dataset testData.dataset(filename) 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 Boolean test.compare(value1, value2) 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:

    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() == 10)
    compareTableWithDataFile(jtable, "after.csv")
# End of Added by Hand
    type(waitForObject(names.csv_Table_before_csv_JRootPane), "<Alt+F>")
    type(waitForObject(names.csv_Table_before_csv_JRootPane), "q")
    type(waitForObject(names.csv_Table_Yes_JButton), "<Alt+N>")
    source(findFile("scripts", "common.js"))
    var jtable = waitForObject("{type='javax.swing.JTable' visible='true'}")
    var tableModel = jtable.getModel()
    test.verify(tableModel.getColumnCount() == 5)
    test.verify(tableModel.getRowCount() == 10)
    compareTableWithDataFile(jtable, "after.csv")
// End of Added by Hand
    type(waitForObject(names.csvTableBeforeCsvJRootPane), "<Alt+F>");
    type(waitForObject(names.csvTableBeforeCsvJRootPane), "q");
    type(waitForObject(names.csvTableYesJButton), "<Alt+N>");
}
    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() == 10);
    compareTableWithDataFile($jtable, "after.csv");
# End of Added by Hand
    type(waitForObject(":CSV Table - before.csv_JRootPane"), "<Alt+F>");
    type(waitForObject(":CSV Table - before.csv_JRootPane"), "q");
    type(waitForObject(":CSV Table.Yes_JButton"), "<Alt+N>");
}
    require findFile("scripts", "common.rb")
    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(waitForObject(Names::CSV_Table_before_csv_JRootPane), "<Alt+F>")
    type(waitForObject(Names::CSV_Table_before_csv_JRootPane), "q")
    type(waitForObject(Names::CSV_Table_Yes_JButton), "<Alt+N>")
end
    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] 10
    compareTableWithDataFile $jtable "after.csv"
# End of Added by Hand
    invoke type [waitForObject ":CSV Table - before.csv_JRootPane"] "<Alt+F>"
    invoke type [waitForObject ":CSV Table - before.csv_JRootPane"] "q"
    invoke type [waitForObject ":CSV Table.Yes_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. Or we could record a test snippet directly into an existing test.

How to Test Java SWT Applications

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

How to Test Stateful and Single-Valued Widgets (Java/SWT)

In this section we will see how to test the examples/java/paymentform_swt/PaymentFormSWT.java example program. This program uses many basic Java/SWT widgets including Button, Combo, DateTime, TabFolder, and Text. As part of our coverage of the example we will show how to check the values and state of individual widgets. We will also demonstrate how to test a form's business rules.

{}

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

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

  • In "cash" mode, i.e., when the Cash tab is checked:
    • The minimum payment is one dollar and the maximum is $2000 or the amount due, whichever is smaller.
  • In "check" mode, i.e., when the Check tab is checked:
    • The minimum payment is $10 and the maximum is $250 or the amount due, whichever is smaller.
    • The check date must be no earlier than 30 days ago and no later than tomorrow, and must initially be set to today's date.
    • The bank name, bank number, account name, and account number line edits must all be nonempty.
    • The check signed checkbox must be checked.
  • In "card" mode, i.e., when the Credit Card tab is checked:
    • The minimum payment is $10 or 5% of the amount due whichever is larger, and the maximum is $5000 or the amount due, whichever is smaller.
    • The issue date must be no earlier than three years ago, and must be initially set to the earliest possible date.
    • The expiry date must be at least one month later than today, and must be initially set to the earliest possible date.
    • The account name and account number line edits must be nonempty.

Note: 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 SQUISHDIR/examples/java/paymentform_swt, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHDIR/examples/java/paymentform_swt/suite_py, and the JavaScript version of the tests is in SQUISHDIR/examples/java/paymentform_swt/suite_js, and so on.

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.

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform_swt/PaymentFormSWT.jar"')
    # 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 str(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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform_swt/PaymentFormSWT.jar"');
    // 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 amount_due = 0 + amountDueLabel.text.replace(/\D/g, "");
    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("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform_swt/PaymentFormSWT.jar\"");
    # 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());
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform_swt/PaymentFormSWT.jar\"")
    # 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)
    amount_due = String(amountDueLabel.text).gsub(/\D/, "").to_f
    maximum = 2000 < amount_due ? 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())
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform_swt/PaymentFormSWT.jar\""
    # 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 Object waitForObject(objectOrName) 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 the Object waitForObject(objectOrName) function to get a reference to the TabFolder widget and then use the clickTab(objectOrName, tabText) function to click the tab we are interested in. How did we know the tab folder's name? We used the Spy (see How to Use the Spy).

The first business rule to be tested concerns the minimum and maximum allowed payment amounts. As usual we begin calling Object waitForObject(objectOrName) 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 we cannot have a fixed name for it. So instead we identify it using a real (multi-property) 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 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 Object cast(object, type) 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 strip out 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 the Spy to find out the spinner's name. Once we have a reference to the spinner, we use the Boolean test.verify(condition) method to ensure that is has the correct minimum and maximum amounts set. (For Tcl we have used the Boolean test.compare(value1, value2) method instead of Boolean test.verify(condition) since it is more convenient to do so.)

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

Although the "cash" mode test works well, there are a few places where we use essentially the same code. So before creating the test for the "check" and "card" modes, we will create some common functions that we can use to refactor our tests with. (The process used to create shared code is described a little later in How to Create and Use Shared Data and Shared Scripts—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.

    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 str(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'}");
        return 0 + amountDueLabel.text.replace(/\D/g, "");
    }


    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);
    }
# encoding: UTF-8
require 'squish'

include Squish

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

def dateTimeEqualsDate(dateTime, date)
    dateTime.getYear() == date.get(LC::Java_util_Calendar.YEAR) &&
    dateTime.getMonth() == date.get(LC::Java_util_Calendar.MONTH) &&
    dateTime.getDay() == date.get(LC::Java_util_Calendar.DAY_OF_MONTH)
end

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)
    String(amountDueLabel.text).gsub(/\D/, "").to_f
end

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

def min(a, b)
    a < b ? a : b
end

def max(a, b)
    a < b ? b : a
end
    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.

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform_swt/PaymentFormSWT.jar"')
    # 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(names.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 = names.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('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform_swt/PaymentFormSWT.jar"');
    // 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(names.checkCheckDateDateTime);
    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 = names.paymentFormPayOrgEclipseSwtWidgetsButton;
    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("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform_swt/PaymentFormSWT.jar\"");
    # 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($Names::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 = $Names::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();
    $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform_swt/PaymentFormSWT.jar\"")
    # Import functionality needed by more than one test script
    require findFile("scripts", "common.rb")

    # 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(Names::Check_Check_Date_DateTime)
    today = LC::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 = Names::Payment_Form_Pay_org_eclipse_swt_widgets_Button
    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()
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.isEnabled())
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform_swt/PaymentFormSWT.jar\""
    # Import functionality needed by more than one test script
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabItem "Chec&k"

    # Business rule #1: the minimum payment is $10 and the maximum is
    # $250 or the amount due whichever is smaller
    set amount_due [getAmountDue]
    checkPaymentRange 10 [expr 250 > $amount_due ? $amount_due : 250]

    # Business rule #2: the check date must be no earlier than 30 days
    # ago and no later than tomorrow, and must initially be set to
    # today. Here we just check its initial value.
    set checkDateTime [waitForObject ":Check.Check Date:_DateTime"]
    set today [invoke java_util_Calendar getInstance]
    test verify [dateTimeEqualsDate $checkDateTime $today]

    # Business rule #3: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [findObject $payButtonName]
    test verify [expr ![invoke $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
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}

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

The first business rule is very similar to before, except that this time we use the common checkPaymentRange function. The second rule shows how we can test the properties of a DateTime using the common dateTimeEqualsDate function.

While Squish provides access to most of the Java API automatically, in some cases we need to access classes that are not available by default. In this test we need a class that is not available by default, java.util.Calendar. This isn't a problem in practice since we can always register additional classes (whether standard or our own custom classes) with the squishserver—see Wrapping custom classes 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"

An alternative to registering additional classes is to create instances of them using Java's introspection facilities.

The third rule checks that the Pay button is disabled since at this stage the form's data isn't valid. (For example, the check hasn't been signed and there are no account details filled in.) The fourth rule is used to ensure and confirm that the check is signed—functionality that we have wholly encapsulated in the ensureSignedCheckBoxIsChecked function. For the fifth rule we populate the account line edits with fake data using the populateCheckFields function. At the end all the widgets have valid values so the Pay button should be enabled, and the last line tests that it is.

def ensureSignedCheckBoxIsChecked():
    checkSignedButton = waitForObject(names.check_Check_Signed_Button)
    if not checkSignedButton.getSelection():
        clickButton(checkSignedButton)
    snooze(1)
    test.verify(checkSignedButton.getSelection())

def populateCheckFields():
    bankNameText = waitForObject(names.check_Bank_Name_Text)
    type(bankNameText, "A Bank")
    bankNumberText = waitForObject(names.check_Bank_Number_Text)
    type(bankNumberText, "88-91-33X")
    accountNameText = waitForObject(names.check_Account_Name_Text)
    type(accountNameText, "An Account")
    accountNumberText = waitForObject(names.check_Account_Number_Text)
    type(accountNumberText, "932745395")
function ensureSignedCheckBoxIsChecked()
{
    var checkSignedButton = waitForObject(names.checkCheckSignedButton);
    if (!checkSignedButton.getSelection()) {
        clickButton(checkSignedButton);
    }
    snooze(1);
    test.verify(checkSignedButton.getSelection());
}

function populateCheckFields()
{
    var bankNameText = waitForObject(names.checkBankNameText);
    type(bankNameText, "A Bank");
    var bankNumberText = waitForObject(names.checkBankNumberText);
    type(bankNumberText, "88-91-33X");
    var accountNameText = waitForObject(names.checkAccountNameText);
    type(accountNameText, "An Account");
    var accountNumberText = waitForObject(names.checkAccountNumberText);
    type(accountNumberText, "932745395");
}
sub ensureSignedCheckBoxIsChecked
{
    my $checkSignedButton = waitForObject($Names::check_check_signed_button);
    if (!$checkSignedButton->getSelection()) {
        clickButton($checkSignedButton);
    }
    test::verify($checkSignedButton->getSelection());
}

sub populateCheckFields
{
    my $bankNameText = waitForObject($Names::check_bank_name_text);
    type($bankNameText, "A Bank");
    my $bankNumberText = waitForObject($Names::check_bank_number_text);
    type($bankNumberText, "88-91-33X");
    my $accountNameText = waitForObject($Names::check_account_name_text);
    type($accountNameText, "An Account");
    my $accountNumberText = waitForObject($Names::check_account_number_text);
    type($accountNumberText, "932745395");
}
def ensureSignedCheckBoxIsChecked
    checkSignedButton = waitForObject(Names::Check_Check_Signed_Button)
    if !checkSignedButton.getSelection()
        clickButton(checkSignedButton)
    end
    snooze(1)
    Test.verify(checkSignedButton.getSelection())
end

def populateCheckFields
    bankNameText = waitForObject(Names::Check_Bank_Name_Text)
    type(bankNameText, "A Bank")
    bankNumberText = waitForObject(Names::Check_Bank_Number_Text)
    type(bankNumberText, "88-91-33X")
    accountNameText = waitForObject(Names::Check_Account_Name_Text)
    type(accountNameText, "An Account")
    accountNumberText = waitForObject(Names::Check_Account_Number_Text)
    type(accountNumberText, "932745395")
end
proc ensureSignedCheckBoxIsChecked {} {
    set checkSignedButton [waitForObject ":Check.Check Signed_Button"]
    if {![invoke $checkSignedButton getSelection]} {
        invoke clickButton $checkSignedButton
    }
    snooze 1
    test verify [invoke $checkSignedButton getSelection]
}

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

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

We are now ready to look at the last test of the form's business logic—the test of "card" mode. Just as with "check" mode we have refactored the main function to keep it short and understandable, relying on common functions and test-specific functions in the test.py (or test.js etc.) file to encapsulate particular tests.

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/paymentform_swt/PaymentFormSWT.jar"')
    source(findFile("scripts", "common.py"))

    # Start with the correct tab
    clickTabItem("&Card")

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    cardTypeCombo = waitForObject(names.card_Card_Type_Combo)
    for index in range(cardTypeCombo.getItemCount()):
        if cardTypeCombo.getItem(index) != "Master":
            cardTypeCombo.select(index)
            break

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails()

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButtonName = names.payment_Form_Pay_org_eclipse_swt_widgets_Button
    payButton = findObject(payButtonName)
    test.verify(not payButton.isEnabled())

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields()
    payButton = waitForObject(payButtonName)
    test.verify(payButton.isEnabled())
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/paymentform_swt/PaymentFormSWT.jar"');
    source(findFile("scripts", "common.js"));

    // Start with the correct tab
    clickTabItem("&Card");

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

    // Make sure the card type is not Master, i.e., not the default,
    // just to show how to manipulate a Combo.
    var cardTypeCombo = waitForObject(names.cardCardTypeCombo);
    for (var index = 0; index < cardTypeCombo.getItemCount(); ++index) {
        if (cardTypeCombo.getItem(index) != "Master") {
            cardTypeCombo.select(index);
            break;
        }
    }

    // Business rules #2 and #3: the issue date must be no
    // earlier than 3 years ago, and must start out at that date, and the
    // expiry date must be at least a month later than today.
    // All we check here is that both DateTimes are set to their earliest
    // valid date.
    checkCardDateDetails();

    // Business rule #4: the Pay button is disabled (since the form's data
    // isn't yet valid), so we use findObject() without waiting
    var payButtonName = names.paymentFormPayOrgEclipseSwtWidgetsButton;
    var payButton = findObject(payButtonName);
    test.verify(!payButton.isEnabled());

    // Business rule #5: the Pay button should be enabled since all the
    // previous tests pass, and now we have filled in the account details
    populateCardFields();
    var payButton = waitForObject(payButtonName);
    test.verify(payButton.isEnabled());
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/paymentform_swt/PaymentFormSWT.jar\"");
    source(findFile("scripts", "common.pl"));

    # Start with the correct tab
    clickTabItem("&Card");

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

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    my $cardTypeCombo = waitForObject($Names::card_card_type_combo);
    for (my $index = 0; $index < $cardTypeCombo->getItemCount(); ++$index) {
        if ($cardTypeCombo->getItem($index) ne "Master") {
            $cardTypeCombo->select($index);
            last;
        }
    }

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails();

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    my $payButtonName = $Names::payment_form_pay_org_eclipse_swt_widgets_button;
    my $payButton = findObject($payButtonName);
    test::verify(!$payButton->isEnabled());

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields();
    $payButton = waitForObject($payButtonName);
    test::verify($payButton->isEnabled());
}
def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/paymentform_swt/PaymentFormSWT.jar\"")
    require findFile("scripts", "common.rb")

    # Start with the correct tab
    clickTabItem("&Card")

    # Business rule #1: the minimum payment is $10 or 5% of the amount due
    # whichever is larger and the maximum is $5000 or the amount due
    # whichever is smaller
    amount_due = getAmountDue()
    checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due))

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    cardTypeCombo = waitForObject(Names::Card_Card_Type_Combo)
    for index in 0...cardTypeCombo.getItemCount()
        if cardTypeCombo.getItem(index) != "Master"
            cardTypeCombo.select(index)
            break
        end
    end

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    payButtonName = Names::Payment_Form_Pay_org_eclipse_swt_widgets_Button
    payButton = findObject(payButtonName)
    Test.verify(!payButton.isEnabled())

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    payButton = waitForObject(payButtonName)
    Test.verify(payButton.isEnabled())
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/paymentform_swt/PaymentFormSWT.jar\""
    source [findFile "scripts" "common.tcl"]

    # Start with the correct tab
    clickTabItem "&Card"

    # Business rule #1: the minimum payment is $10 or 5% of the amount
    # due whichever is larger and the maximum is $5000 or the amount
    # due whichever is smaller
    set amount_due [getAmountDue]
    set five_percent [expr $amount_due / 20.0]
    set minimum [expr 10 > $five_percent ? 10 : $five_percent]
    set maximum [expr 5000 > $amount_due ? $amount_due : 5000]
    checkPaymentRange $minimum $maximum

    # Make sure the card type is not Master, i.e., not the default, just
    # to show how to manipulate a Combo.
    set cardTypeCombo [waitForObject ":Card.Card Type:_Combo"]
    set count [invoke $cardTypeCombo getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        if {[invoke $cardTypeCombo getItem $index] != "Master"} {
            invoke $cardTypeCombo select $index
            break
        }
    }

    # Business rules #2 and 3: the issue date must be no
    # earlier than 3 years ago, and must start out at that date, and the
    # expiry date must be at least a month later than today.
    # All we check here is that both DateTimes are set to their earliest
    # valid date.
    checkCardDateDetails

    # Business rule #4: the Pay button is disabled (since the form's data
    # isn't yet valid), so we use findObject() without waiting
    set payButtonName ":Payment Form.Pay_org.eclipse.swt.widgets.Button"
    set payButton [findObject $payButtonName]
    test verify [expr ![invoke $payButton isEnabled]]

    # Business rule #5: the Pay button should be enabled since all the
    # previous tests pass, and now we have filled in the account details
    populateCardFields
    set payButton [waitForObject $payButtonName]
    test verify [invoke $payButton isEnabled]
}

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

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

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

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

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

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

def populateCardFields():
    cardAccountNameText = waitForObject(names.card_Account_Name_Text)
    mouseClick(cardAccountNameText, 10, 10, 0, Button.Button1)
    type(cardAccountNameText, "An Account")
    cardAccountNumberText = waitForObject(names.card_Account_Number_Text)
    type(cardAccountNumberText, "1343 876 326 1323 32")
function checkCardDateDetails()
{
    var tabFolderName = names.paymentFormOrgEclipseSwtWidgetsTabFolder;
    var tabFolder = waitForObject(tabFolderName);
    for (var index = 0; index < tabFolder.getItemCount(); ++index) {
        var tabItem = tabFolder.getItem(index);
        if (tabItem.getText() == "&Card") {
            break;
        }
    }
    var dateTimes = [];
    var control = tabItem.getControl();
    var children = control.getChildren();
    for (var index = 0; index <  children.length; ++index) {
        var child = children.at(index);
        if (child.getClass().toString() ==
            "class org.eclipse.swt.widgets.DateTime") {
            dateTimes.push(child);
        }
    }
    test.verify(dateTimes.length == 2);

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

function populateCardFields()
{
    var cardAccountNameText = waitForObject(names.cardAccountNameText);
    mouseClick(cardAccountNameText, 10, 10, 0, Button.Button1);
    type(cardAccountNameText, "An Account");
    var cardAccountNumberText = waitForObject(names.cardAccountNumberText);
    type(cardAccountNumberText, "1343 876 326 1323 32");
}
sub checkCardDateDetails
{
    my $tabFolderName = $Names::payment_form_org_eclipse_swt_widgets_tabfolder;
    my $tabFolder = waitForObject($tabFolderName);
    my $tabItem;
    for (my $index = 0; $index < $tabFolder->getItemCount(); ++$index) {
        $tabItem = $tabFolder->getItem($index);
        if ($tabItem->getText() eq "&Card") {
            last;
        }
    }
    my @dateTimes = ();
    my $control = $tabItem->getControl();
    my $children = $control->getChildren();
    for (my $index = 0; $index < $children->length(); ++$index) {
        my $child = $children->at($index);
        if ($child->getClass()->toString() eq
            "class org.eclipse.swt.widgets.DateTime") {
            push @dateTimes, $child;
        }
    }
    test::verify(@dateTimes == 2);

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

sub populateCardFields
{
    my $cardAccountNameText = waitForObject($Names::card_account_name_text);
    mouseClick($cardAccountNameText, 10, 10, 0, Button::Button1);
    type($cardAccountNameText, "An Account");
    my $cardAccountNumberText = waitForObject(
        $Names::card_account_number_text);
    type($cardAccountNumberText, "1343 876 326 1323 32");
}
def checkCardDateDetails
    tabFolderName = Names::Payment_Form_org_eclipse_swt_widgets_TabFolder
    tabFolder = waitForObject(tabFolderName)
    for index in 0...tabFolder.getItemCount()
        tabItem = tabFolder.getItem(index)
        if tabItem.getText() == "&Card"
            break
        end
    end
    dateTimes = []
    control = tabItem.getControl()
    children = control.getChildren()
    for index in 0...children.length
        child = children.at(index)
        if (child.getClass().toString() ==
        "class org.eclipse.swt.widgets.DateTime")
            dateTimes << child
        end
    end
    Test.verify(dateTimes.length == 2)

    earliestIssueDate = LC::Java_util_Calendar.getInstance()
    earliestIssueDate.add(LC::Java_util_Calendar.YEAR, -3)
    Test.verify(dateTimeEqualsDate(dateTimes[0], earliestIssueDate))
    earliestExpiryDate = LC::Java_util_Calendar.getInstance()
    earliestExpiryDate.add(LC::Java_util_Calendar.MONTH, 1)
    Test.verify(dateTimeEqualsDate(dateTimes[1], earliestExpiryDate))
end

def populateCardFields
    cardAccountNameText = waitForObject(Names::Card_Account_Name_Text)
    mouseClick(cardAccountNameText, 10, 10, 0, Button::BUTTON1)
    type(cardAccountNameText, "An Account")
    cardAccountNumberText = waitForObject(Names::Card_Account_Number_Text)
    type(cardAccountNumberText, "1343 876 326 1323 32")
end
proc checkCardDateDetails {} {
    set tabFolderName ":Payment Form_org.eclipse.swt.widgets.TabFolder"
    set tabFolder [waitForObject $tabFolderName]
    set count [invoke $tabFolder getItemCount]
    for {set index 0} {$index < $count} {incr index} {
        set tabItem [invoke $tabFolder getItem $index]
        if {[invoke $tabItem getText] == "&Card"} {
            break
        }
    }
    set dateTimes {}
    set control [invoke $tabItem getControl]
    set children [invoke $control getChildren]
    set count [property get $children length]
    for {set index 0} {$index < $count} {incr index} {
        set child [invoke $children at $index]
        set className [invoke [invoke $child getClass] toString]
        if {$className == "class org.eclipse.swt.widgets.DateTime"} {
            lappend dateTimes $child
        }
    }
    test compare [llength $dateTimes] 2

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

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

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

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

For the populateCardFields function we use the type(objectOrName, text) function to enter the fake data.

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

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

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

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

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

How to Test List

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

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews_swt/ItemViewsSWT.jar"')
    listWidgetName = ":Item Views_org.eclipse.swt.widgets.List"
    listWidget = waitForObject(listWidgetName)
    for row in range(listWidget.getItemCount()):
        text = listWidget.getItem(row)
        selected = ""
        if listWidget.isSelected(row):
            selected = " +selected"
        test.log("(%d) '%s'%s" % (row, text, selected))
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews_swt/ItemViewsSWT.jar"');
    var listWidgetName = ":Item Views_org.eclipse.swt.widgets.List";
    var listWidget = waitForObject(listWidgetName);
    for (var row = 0; row < listWidget.getItemCount(); ++row) {
        var text = listWidget.getItem(row);
        var selected = "";
        if (listWidget.isSelected(row)) {
            selected = " +selected";
        }
        test.log("(" + String(row) + ") '" + text + "'" + selected);
    }
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews_swt/ItemViewsSWT.jar\"");
    my $listWidgetName = ":Item Views_org.eclipse.swt.widgets.List";
    my $listWidget = waitForObject($listWidgetName);
    for (my $row = 0; $row < $listWidget->getItemCount(); ++$row) {
        my $text = $listWidget->getItem($row);
        my $selected = "";
        if ($listWidget->isSelected($row)) {
            $selected = " +selected";
        }
        test::log("($row) '$text'$selected");
    }
}
# encoding: UTF-8
require 'squish'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews_swt/ItemViewsSWT.jar\"")
    listWidgetName = ":Item Views_org.eclipse.swt.widgets.List"
    listWidget = waitForObject(listWidgetName)
    for row in 0...listWidget.getItemCount()
        text = listWidget.getItem(row)
        selected = ""
        if listWidget.isSelected(row)
            selected = " +selected"
        end
        Test.log("(#{row}) '#{text}'#{selected}")
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews_swt/ItemViewsSWT.jar\""
    set listWidgetName ":Item Views_org.eclipse.swt.widgets.List"
    set listWidget [waitForObject $listWidgetName]
    for {set row 0} {$row < [invoke $listWidget getItemCount]} \
        {incr row} {
        set text [invoke $listWidget getItem $row]
        set selected ""
        if {[invoke $listWidget isSelected $row]} {
            set selected " +selected"
        }
        test log "($row) '$text'$selected"
    }
}

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

How to Test Table

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

import os
def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/itemviews_swt/ItemViewsSWT.jar"')
    tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table"
    tableWidget = waitForObject(tableWidgetName)
    for row in range(tableWidget.getItemCount()):
        item = tableWidget.getItem(row)
        checked = ""
        if item.getChecked():
            checked = " +checked"
        selected = ""
        if tableWidget.isSelected(row):
            selected = " +selected"
        texts = []
        for column in range(tableWidget.getColumnCount()):
            texts.append(item.getText(column))
        test.log("(%d, 0) '%s'%s%s" % (row, "|".join(texts), checked,
            selected))
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/itemviews_swt/ItemViewsSWT.jar"');
    var tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table";
    var tableWidget = waitForObject(tableWidgetName);
    for (var row = 0; row < tableWidget.getItemCount(); ++row) {
        var item = tableWidget.getItem(row);
        var checked = "";
        if (item.getChecked()) {
            checked = " +checked";
        }
        var selected = "";
        if (tableWidget.isSelected(row)) {
            selected = " +selected";
        }
        var text = "";
        for (var column = 0; column < tableWidget.getColumnCount();
            ++column) {
            if (text != "") {
                text += "|";
            }
            text += item.getText(column);
        }
        test.log("(" + String(row) + ", 0) '" + text + "'" + checked +
            selected);
    }
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews_swt/ItemViewsSWT.jar\"");
    my $tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table";
    my $tableWidget = waitForObject($tableWidgetName);
    for (my $row = 0; $row < $tableWidget->getItemCount(); ++$row) {
        my $item = $tableWidget->getItem($row);
        my $checked = "";
        if ($item->getChecked()) {
            $checked = " +checked";
        }
        my $selected = "";
        if ($tableWidget->isSelected($row)) {
            $selected = " +selected";
        }
        my @texts = ();
        for (my $column = 0; $column < $tableWidget->getColumnCount();
            ++$column) {
            push @texts, $item->getText($column);
        }
        $text = join("|", @texts);
        test::log("($row, 0) '$text'$checked$selected");
    }
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/itemviews_swt/ItemViewsSWT.jar\"")
    tableWidgetName = ":Item Views_org.eclipse.swt.widgets.Table"
    tableWidget = waitForObject(tableWidgetName)
    for row in 0...tableWidget.getItemCount()
        item = tableWidget.getItem(row)
        checked = ""
        if item.getChecked()
            checked = " +checked"
        end
        selected = ""
        if tableWidget.isSelected(row)
            selected = " +selected"
        end
        texts = []
        for column in 0...tableWidget.getColumnCount()
            texts << item.getText(column)
        end
        Test.log("(#{row}, 0) '%s'#{checked}#{selected}" % texts.join("|"))
    end
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews_swt/ItemViewsSWT.jar\""
    set tableWidgetName ":Item Views_org.eclipse.swt.widgets.Table"
    set tableWidget [waitForObject $tableWidgetName]
    for {set row 0} {$row < [invoke $tableWidget getItemCount]} \
        {incr row} {
        set item [invoke $tableWidget getItem $row]
        set checked ""
        if {[invoke $item getChecked]} {
            set checked " +checked"
        }
        set selected ""
        if {[invoke $tableWidget isSelected $row]} {
            set selected " +selected"
        }
        set text {}
        for {set column 0} {$column < [invoke $tableWidget getColumnCount]} \
            {incr column} {
            lappend text [invoke $item getText $column]
        }
        set text [join $text "|"]
        test log "($row, 0) '$text'$checked$selected"
    }
}

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

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

Note: You can use table verification points to check an entire table, as instructed in How to Create and Use Table Verifications.

How to Test Tree

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

    import os
    def checkAnItem(indent, item, selection):
        if indent > -1:
            checked = selected = ""
            if item.getChecked():
                checked = " +checked"
            for i in range(selection.length):
                if selection.at(i) == item:
                    selected = " +selected"
                    break
            test.log("|%s'%s'%s%s" % (" " * indent, item.getText(), checked,
                selected))
        else:
            indent = -4
        for row in range(item.getItemCount()):
            child = item.getItem(row)
            checkAnItem(indent + 4, child, selection)

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

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

sub main()
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/itemviews_swt/ItemViewsSWT.jar\"");
    my $treeWidgetName = ":Item Views_org.eclipse.swt.widgets.Tree";
    my $treeWidget = waitForObject($treeWidgetName);
    checkAnItem(-1, $treeWidget, $treeWidget->getSelection());
}
# encoding: UTF-8
require 'squish'
include Squish

def checkAnItem(indent, item, selection)
    if indent > -1
        checked = selected = ""
        if item.getChecked()
            checked = " +checked"
        end
        for i in 0...selection.length
            if selection.at(i) == item
                selected = " +selected"
                break
            end
        end
        Test.log("|%s'%s'%s%s" % [" " * indent, item.getText(), checked,
            selected])
    else
        indent = -4
    end
    for row in 0...item.getItemCount()
        child = item.getItem(row)
        checkAnItem(indent + 4, child, selection)
    end
end

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

proc main {} {
        startApplication "\"$::env(SQUISH_PREFIX)/examples/java/itemviews_swt/ItemViewsSWT.jar\""
        set treeWidgetName ":Item Views_org.eclipse.swt.widgets.Tree"
        set treeWidget [waitForObject $treeWidgetName]
        checkAnItem -1 $treeWidget [invoke $treeWidget getSelection]
    }

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

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.

How to Test the Table Widget and Use External Data Files (Java/SWT)

In this section we will see how to test the CsvTableSWT.java program shown below. This program uses a Table to present the contents of a .csv (comma-separated values) file, and provides some basic functionality for manipulating the data—inserting and deleting rows and swapping columns. As we review the tests we will learn how to import test data, manipulate the data, and compare what the Table shows with what we expect its contents to be. And since the CSV Table program is a main-window-style application, we will also learn how to test that menu options behave as expected.

{}

The CSV Table program.

The source code for this example is in the directory SQUISHDIR/examples/java/csvtable_swt, and the test suites are in subdirectories underneath—for example, the Python version of the tests is in the directory SQUISHDIR/examples/java/csvtable_swt/suite_py, and the JavaScript version of the tests is in SQUISHDIR/examples/java/csvtable_swt/suite_js, and so on.

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

import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/csvtable_swt/CsvTableSWT.jar"')
    source(findFile("scripts", "common.py"))
    filename = "before.csv"
    doFileOpen(filename, "")
    table = waitForObject("{isvisible='true' "
        "type='org.eclipse.swt.widgets.Table'}")
    compareTableWithDataFile(table, filename)
function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/java/csvtable_swt/CsvTableSWT.jar"');
    source(findFile("scripts", "common.js"));
    var filename = "before.csv";
    doFileOpen(filename, "");
    var table = waitForObject("{isvisible='true' " +
        "type='org.eclipse.swt.widgets.Table'}");
    compareTableWithDataFile(table, filename);
}
sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/csvtable_swt/CsvTableSWT.jar\"");
    source(findFile("scripts", "common.pl"));
    my $filename = "before.csv";
    doFileOpen($filename);
    my $table = waitForObject("{isvisible='true' " .
        "type='org.eclipse.swt.widgets.Table'}");
    compareTableWithDataFile($table, $filename);
}
# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/csvtable_swt/CsvTableSWT.jar\"")
    require findFile("scripts", "common.rb")
    filename = "before.csv"
    doFileOpen(filename, "")
    table = waitForObject("{isvisible='true' " +
    "type='org.eclipse.swt.widgets.Table'}")
    compareTableWithDataFile(table, filename)
end
proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/csvtable_swt/CsvTableSWT.jar\""
    source [findFile "scripts" "common.tcl"]
    set filename "before.csv"
    doFileOpen $filename ""
    set table [waitForObject "{isvisible='true' \
        type='org.eclipse.swt.widgets.Table'}"]
    compareTableWithDataFile $table $filename
}

We begin by starting the application—in the case of Java/SWT programs this is a shell script or .bat file that sets up the correct environment and executes the AUT. Once the AUT is running we next load the script that contains common functionality, just as we did in an earlier section. Then we call a custom doFileOpen function that tells the program to open the given file—and this is done through the user interface as we will see. (The first argument is the name of the file to open and the second argument is the name of the current file, which in this case is an empty string since we are starting the AUT from scratch.) Next we get a reference to the Table using the Object waitForObject(objectOrName) function, and finally we check that the Table's contents match the contents of the data file held amongst the test suite's test data. Note that both the CSV Table program and Squish load and parse the data file using their own completely independent code. (See How to Create and Use Shared Data and Shared Scripts for how to import test data into Squish.)

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

def doFileOpen(fileToOpen, currentFile):
    invokeMenuItem("File", "Open...", currentFile)
    chooseFile(waitForObject(names.sWT), fileToOpen)

def invokeMenuItem(menu, item, filename):
    if filename:
        filename = " - " + filename
    menuName = {"caption": menu, "container": names.csv_Table_org_eclipse_swt_widgets_Menu, "type": "org.eclipse.swt.widgets.MenuItem"}
    waitForObject(menuName)
    activateItem(menuName)
    menuItemName = ("{caption='%s' type='org.eclipse.swt.widgets.MenuItem'}" % item)
    waitForObject(menuItemName)
    activateItem(menuItemName)

def compareTableWithDataFile(table, filename):
    for row, record in enumerate(testData.dataset(filename)):
        item = table.getItem(row)
        for column, name in enumerate(testData.fieldNames(record)):
            test.compare(testData.field(record, name), item.getText(column))
function doFileOpen(fileToOpen, currentFile)
{
    invokeMenuItem("File", "Open...", currentFile);
    chooseFile(waitForObject(names.swt), fileToOpen);
}

function invokeMenuItem(menu, item, filename)
{
    if (filename) {
        filename = " - " + filename;
    }
    var menuName = {"caption": menu, "container": names.csvTableOrgEclipseSwtWidgetsMenu, "type": "org.eclipse.swt.widgets.MenuItem"};
    waitForObject(menuName);
    activateItem(menuName);
    var menuItemName = "{caption='" + item + "'" +
            " type='org.eclipse.swt.widgets.MenuItem'}";
    waitForObject(menuItemName);
    activateItem(menuItemName);
}

function compareTableWithDataFile(table, filename)
{
    var records = testData.dataset(filename);
    for (var row = 0; row < records.length; ++row) {
        var item = table.getItem(row);
        columnNames = testData.fieldNames(records[row]);
        for (var column = 0; column < columnNames.length; ++column) {
            test.compare(testData.field(records[row], column),
                item.getText(column));
        }
    }
}
sub doFileOpen
{
    my ($fileToOpen, $currentFile) = @_;
    invokeMenuItem("File", "Open...", $currentFile);
    chooseFile(waitForObject($Names::swt), $fileToOpen);
}

sub invokeMenuItem
{
    my ($menu, $item, $filename) = @_;
    if ($filename) {
        $filename = " - " . $filename;
    }
    my $menuName = {"caption" => $menu, "container" => $Names::csv_table_org_eclipse_swt_widgets_menu, "type" => "org.eclipse.swt.widgets.MenuItem"};
    waitForObject($menuName);
    activateItem($menuName);
    my $menuItemName = {"caption"=>$item, "type"=>'org.eclipse.swt.widgets.MenuItem'};
    waitForObject($menuItemName);
    activateItem($menuItemName);
}

sub compareTableWithDataFile
{
    my ($table, $filename) = @_;
    my @records = testData::dataset($filename);
    for (my $row = 0; $row < scalar(@records); $row++) {
        my $item = $table->getItem($row);
        my @columnNames = testData::fieldNames($records[$row]);
        for (my $column = 0; $column < scalar(@columnNames); $column++) {
            test::compare(testData::field($records[$row], $column),
                $item->getText($column));
        }
    }
}
def doFileOpen(fileToOpen, currentFile)
    invokeMenuItem("File", "Open...", currentFile)
    chooseFile(waitForObject(Names::SWT), fileToOpen)
end

def invokeMenuItem(menu, item, filename)
    if filename and filename != ""
        filename = " - " + filename
    end
    menuName = {:caption => menu, :container => Names::CSV_Table_org_eclipse_swt_widgets_Menu, :type => "org.eclipse.swt.widgets.MenuItem"}
    waitForObject(menuName)
    activateItem(menuName)
    menuItemName = {:caption=> item, :type =>'org.eclipse.swt.widgets.MenuItem'}
    waitForObject(menuItemName)
    activateItem(menuItemName)
end

def compareTableWithDataFile(table, filename)
    TestData.dataset(filename).each_with_index do
        |record, row|
        item = table.getItem(row)
        for column in 0...TestData.fieldNames(record).length
            Test.compare(TestData.field(record, column), item.getText(column))
        end
    end
end
proc doFileOpen {fileToOpen currentFile} {
    invokeMenuItem "File" "Open..." $currentFile
    invoke chooseFile [waitForObject ":SWT"] $fileToOpen
}

proc invokeMenuItem {menu item filename} {
    if {$filename != ""} {
        set filename " - $filename"
    }
    set menuName "{caption='$menu' \
        container=':CSV Table${filename}_org.eclipse.swt.widgets.Menu' \
        type='org.eclipse.swt.widgets.MenuItem'}"
    waitForObject $menuName
    invoke activateItem $menuName
    set menuItemName "{caption='$item' \
        type='org.eclipse.swt.widgets.MenuItem'}"
    waitForObject $menuItemName
    invoke activateItem $menuItemName
}

proc compareTableWithDataFile {table filename} {
    set data [testData dataset $filename]
    for {set row 0} {$row < [llength $data]} {incr row} {
        set item [invoke $table getItem $row]
        set columnNames [testData fieldNames [lindex $data $row]]
        for {set column 0} {$column < [llength $columnNames]} \
            {incr column} {
            test compare [testData field [lindex $data $row] $column] \
                [invoke $item getText $column]
           }
    }
}

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

The invokeMenuItem function in effect simulates the user clicking Alt+k (where k is a character, for example "F" for the file menu), and then the character that corresponds to the required action, (for example, "o" for "Open").

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

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

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

When the file is opened, the program is expected to load the file's data. We check that the data has been loaded correctly by comparing the data shown in the Table and the data in the file. This comparison is done by the custom compareTableWithDataFile function. This function uses Squish's Dataset testData.dataset(filename) function to load in the data so that it can be accessed through the Squish API. We expect the text of every cell in the table to match the corresponding item in the data, and we check that this is the case using the Boolean test.compare(value1, value2) 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:

    source(findFile("scripts", "common.py"))
    table = waitForObject("{isvisible='true' "
        "type='org.eclipse.swt.widgets.Table'}")
    test.verify(table.getColumnCount() == 5)
    test.verify(table.getItemCount() == 12)
    compareTableWithDataFile(table, "after.csv")
    # End of Added by Hand
    waitForObject(names.file_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(names.file_org_eclipse_swt_widgets_MenuItem_2)
    waitForObject(names.quit_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(names.quit_org_eclipse_swt_widgets_MenuItem_2)
    waitForObject(names.sWT)
    closeMessageBox(names.sWT, SWT.NO)
    source(findFile("scripts", "common.js"));
    var table = waitForObject("{isvisible='true' " +
        "type='org.eclipse.swt.widgets.Table'}");
    test.verify(table.getColumnCount() == 5);
    test.verify(table.getItemCount() == 12);
    compareTableWithDataFile(table, "after.csv");
    // End of Added by Hand
    waitForObject(names.fileOrgEclipseSwtWidgetsMenuItem2);
    activateItem(names.fileOrgEclipseSwtWidgetsMenuItem2);
    waitForObject(names.quitOrgEclipseSwtWidgetsMenuItem);
    activateItem(names.quitOrgEclipseSwtWidgetsMenuItem);
    waitForObject(names.swt);
    closeMessageBox(names.swt, SWT.NO);
}
    source(findFile("scripts", "common.pl"));
    my $table = waitForObject("{isvisible='true' " .
        "type='org.eclipse.swt.widgets.Table'}");
    test::verify($table->getColumnCount() == 5);
    test::verify($table->getItemCount() == 12);
    compareTableWithDataFile($table, "after.csv");
    # End of Added by Hand
    waitForObject($Names::file_org_eclipse_swt_widgets_menuitem_2);
    activateItem($Names::file_org_eclipse_swt_widgets_menuitem_2);
    waitForObject($Names::quit_org_eclipse_swt_widgets_menuitem_2);
    activateItem($Names::quit_org_eclipse_swt_widgets_menuitem_2);
    waitForObject($Names::swt);
    closeMessageBox($Names::swt, SWT::NO);
}
    require findFile("scripts", "common.rb")
    table = waitForObject("{isvisible='true' " +
    "type='org.eclipse.swt.widgets.Table'}")
    Test.verify(table.getColumnCount() == 5)
    Test.verify(table.getItemCount() == 12)
    compareTableWithDataFile(table, "after.csv")
    # End of Added by Hand
    waitForObject(Names::File_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(Names::File_org_eclipse_swt_widgets_MenuItem_2)
    waitForObject(Names::Quit_org_eclipse_swt_widgets_MenuItem_2)
    activateItem(Names::Quit_org_eclipse_swt_widgets_MenuItem_2)
    closeMessageBox(waitForObject(Names::SWT), SWT::NO)
end
    source [findFile "scripts" "common.tcl"]
    set table [waitForObject "{isvisible='true' \
        type='org.eclipse.swt.widgets.Table'}"]
    test compare [invoke $table getColumnCount] 5
    test compare [invoke $table getItemCount] 12
    compareTableWithDataFile $table "after.csv"
    # End of Added by Hand
    waitForObject ":File_org.eclipse.swt.widgets.MenuItem_2"
    invoke activateItem ":File_org.eclipse.swt.widgets.MenuItem_2"
    waitForObject ":Quit_org.eclipse.swt.widgets.MenuItem_2"
    invoke activateItem ":Quit_org.eclipse.swt.widgets.MenuItem_2"
    waitForObject ":SWT"
    invoke closeMessageBox ":SWT" [enum SWT NO]
}

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

As the extract indictates, the added lines are not inserted at the end of the recorded test script, but rather just before the program is terminated—after all, we need the program to be running to query its Table. (The reason that the row counts differ is that slightly different interactions were recorded for each scripting language.)

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

How to Test List and ComboBoxes Using Proxies

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

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

{}

In the spy you get for each item an ItemProxy derived object.

In SWT, the List and Combo widget have the method getSelectionIndex. Squish uses this method to generate selectionindex property calls. So for the SWT toolkit, these two tests are equivalent:

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

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

How to Test GEF applications

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

item = waitForObject(":First_FigureItem")
test.verify(item.visible);
test.verify(item.enabled);

How to Use the GestureBuilder class

Note: This sections only applies to JavaFX running on Windows 8 or higher.

An instance of this class is returned by readGesture(gesture-file). When however the recorded gesture doesn't fit on the window, a scaling and/or translation can be done.

It might be useful to get the scene sizes. Here an example how to get these in pixels, using the Java script bindings.

var fx_node = findObject(":A_JavaFX_Node");
var scene = fx_node.getScene();
var w = scene.getWidth();
var h = scene.getHeight();

Suppose the gesture was recorded on a 4:3 display full-screen. And when replayed on a wide monitor, the gesture might be too large and too much to the bottom-left. Then a Object GestureBuilder.scale(scaleX, scaleY, originX, originY) and Object GestureBuilder.translate(x, y) towards the top-right is a possible solution.

{}

The effect of a rotation, scale and translate transformation.

For instance, scale it 3/4 in size and 50 pixels to the right and 10 pixels upwards.

Note: When using the Squish IDE, use the Console view when at a breakpoint in your script, to experiment with gesture transformations.

gesture(waitForObject(":some_object"), readGesture("Gesture_1").scale(0.75).translate(50,-10));

Another approach could be to only scale with an origin in the top-right corner.

var gst = readGesture("Gesture_1");
gesture(waitForObject(":some_object"), gst.scale(0.75, 0.75, gst.areaWidth, 0));

In some cases dynamic created gestures are required, e.g. for more accurate control or dependency on runtime state information. Then the Gesture creation methods can be used.

Here an example of a pitch gesture, two finger gesture making a curved counter clockwise movement on a 800x1200 pixel screen in one second.

var tb = new GestureBuilder(800, 1280, GestureBuilder.Pixel);
tb.addStroke( 600, 400 );
tb.curveTo(1000, 500, 300, 300, 300, 200, 400 );
tb.addStroke( 200, 800 );
tb.curveTo(1000, 300, 900, 500, 900, 600, 800);
tb.build();
gesture(waitForObject(":some_object"), tb);

And here an example of a zoom gesture, two finger gesture moving away from each other, also in one second. This time written as one statement.

gesture(waitForObject(":some_object"),
        new GestureBuilder(800, 1280, GestureBuilder.Pixel)
           .addStroke( 500, 400 )
           .lineTo(1000, 700, 100 )
           .addStroke( 300, 700 )
           .lineTo(1000, 100, 1000)
           .build());

In the above two examples, the coordinate values are based on the area size of 800x1280. For different scene sizes or different size or position of the widget on which the gesture should replay, some calculations is needed to get these values. Next, a strategy that can help to keep the complexity under control when having to deal with that.

  • Create a gesture given the screen dimensions, within the boundary of x-axis [-0.5,0.5] and y-axis [-0.5,0.5] and a duration of 1s.
  • Translate it to the center of the target widget.
  • Scale it with a maximum of the widget size, using the center of this widget as origin.
  • Adjust the duration.

{}

Here a listing of this, in this case an S shaped figure.

var fx_node = findObject(":A_JavaFX_Node");
var scene = fx_node.getScene();

var tb = new GestureBuilder(scene.width, scene.height, GestureBuilder.Pixel)
             .addStroke(0, 0.5)
             .curveTo(500, -0.5, 0.5, -0.5, 0, 0, 0)
             .curveTo(500, 0.5, 0, 0.5, -0.5, 0, -0.5)
             .build();

var widget = findObject(":Some widget");
var scale = widget.width > widget.height ? widget.height : widget.width;
var centerX = widget.screenX + widget.width/2;
var centerY = widget.screenY + widget.height/2;
gesture(widget,
        tb.translate(centerX, centerY)
          .scale(scale, -scale, centerX, centerY)
          .accelerate(1/2))

Note that this example defines the figure with the positive y-axis upwards. In order to not get the figure up-side-down, a mirror in the x-axis is needed. The trick is to use a negative scale factor in the vertical direction.

To keep the defined gesture within the -0.5 to 0.5 boundary has the advantage that the total size is 1. Thus it can be scaled with the widget sizes without being scaled outside the screen boundaries. Having (0, 0) in the center, makes the translation simple, just to the center of the widget.

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