Replay System for Java Swing Objects

Jan Newmarch

Introduction

In automated testing of an application built using windowing objects, one important aspect is to simulate a user's interaction with the application. This system is a very early version of a system to drive applications through a sequence of activities by their user interface elements.

Requirements

This system is designed to work with JDK 1.1 or JDK 1.2. It is not designed to work with JDK 1.0 applications. I have no idea if it will work with applications using the JDK 1.0 event model, and don't want to know about them.

This package is only designed to work on applications that use the Swing or JFC components. While some parts will work with AWT components, there are still enough problems with the AWT that require much special case code - and I am not interested in supplying that. AWT problems can only be solved by much C code in a platform-dependent manner, and we only have a short time to live. I am currently developing with Swing 0.6.1 (used in JDK 1.2 beta 1) and Swing 0.7.

The replay language used is tcl for all sorts of excellent reasons. The package is being developed using the tcl-in-Java package JACL jacl1.0b1. See http://sunscript.sun.com for more information on JACL.

Finally, the system uses its own files. These are wrapped in a jar file. I have done something very naughty here: I have a custom version of java.awt.Frame which adds a couple of methods that are either in JDK 1.2 or are promised to be in later versions than JDK 1.2 beta 1. The additional methods are getOwnedWindows() and getFrames(). When JDK 1.2 stabilises this file will be removed.

The CLASSPATH variable needs to be set to cover all these packages. For example, under Unix it may need to be

export CLASSPATH=$REPLAY/replay.jar:$JACL/jacl.jar:$SWING/swing.jar:$SWING/motif.jar:$JDK_HOME/lib/classes.zip
The replay.jar needs to be before the classes.zip in order to override the java.awt.Frame class.

Status

This is version 1.0 alpha, working with Swing 0.7 beta and JACL 1.0b1. Lots of things don't work, partly because of problems in my code, but also because of deficiencies in Swing. Known problems are listed at the end.

Using JDK instead of Swing

The development of the Swing components by Sun is done by a different group for JDK 1.2. Right now, JDK 1.2 beta 1 has been released which includes Swing 0.6. In the meantime, the Swing group has released Swing 0.7.

This would not matter, except that when Swing is folded into the JDK, the package name changes. In Swing, it is com.sun.java.swing but in JDK it is java.awt.swing. Right now, I am building to the latest Swing release, so I use the Swing package name. If you want to use this stuff against JDK 1.2 you will need to edit various files to change the package name, and rebuild the classes.

The files that need to have package name changes are

Copyright

This system is issued under a BSD style copyright: basically, you can do what you want with the code, documentation and examples, but you need to acknowledge me as the original author. Also, whatever you do with this package, I am not responsible for any problems that happen to you as the result. The full notice is in the file COPYRIGHT.

Basic concepts

A Java application will typically be designed to run from a graphical user interface. This will present a screen of GUI objects, which can be selected, typed into, etc. As a result of this user interaction, the application will perform various tasks, which will involve changes of state, changes in the local file system, changes in appearance, etc. Some of these changes will be expected, but some may not.

The purpose of a replay system is simulate a user interaction. This may be done for demonstrations or for application testing: after a sequence of user actions, is the state of various components what it should be?

This system allows an interaction to be specified via a script file. The script file primarily consists of a set of simulated user actions, which will cause the application to respond to these (faked) user interactions. The application will respond in a normal way to these, and at various stages state may be tested to see if the application is doing what it should be doing.

tcl

A replay system needs a language to specify the sequence of actions to be performed. If the system is used for testing, then it also needs access to the objects of the application under test, and to the state of these objects. It may need to perform arbitrarily complex calculations to verify correctness.

tcl is a general purpose scripting language which has found uses in a large variety of situations. tcl is an interpreted language with a simple syntax, which is run by an interpreter. The original tcl interpreters were written in C, but the JACL project has written a tcl interpreter in Java. Even better, the JACL interpreter knows all about Java reflection methods, and is able to call directly into Java code - any Java code.

This allows tcl to be used as a control language to run an application. It also allows tcl to be used to test the state of Java objects. Since it is a full programming language, it can be used to perform arbitrarily complex calculations.

This package extends the basic JACL interpreter by adding in some new commands. These allow applications to be driven in two ways

Invoking Replay

To start the tcl interpreter from Java, set the CLASSPATH variable first. Then type
java tcl.lang.Shell
This will bring up the tcl prompt
%
Whenever this prompt appears, tcl commands can be typed.

The tcl commands

java::load jnewmarch.replay.PlayTcl

set args [java::new {java.lang.String[]} {0} {}]
java::call SwingButton main $args
will
  1. load the replay package
  2. create a tcl reference to a String array with zero elements
  3. call the main() method of the Java application SwingButton with an empty command line list
For a GUI application, this will start it running from the main() method. It will run in its own thread, and the tcl prompt will reappear.

Further tcl commands may be issued to the application from the prompt. In particular, a script may be run by source'ing into the interpreter.

Additional tcl commands

This package adds the following commands to the standard tcl interpreter:
  • sleep
  • frame
  • Event commands
    • actionPerformed
    • adjustmentValueChanged
    • focusGained
    • focusLost
    • itemStateChanged
    • keyPressed
    • keyReleased
    • keyTyped
    • mouseClicked
    • mousePressed
    • mouseReleased
    • mouseDragged
    • mouseMoved
    • componentHidden
    • componentMoved
    • componentResized
    • componentShown
    • windowClosed
    • windowClosing
    • windowDeiconified
    • windowIconified
    • windowOpened
So far I have only found a use for the input methods key... and mouse....

sleep

sleep msecs
Sleep for the specified number of milliseconds

frame

frame frameName
This returns the tcl reference to the Java object with the name frameName. If no name has been set for Frames of an application, then they are named frame0, frame1, etc.

The purpose of this command is to find the toplevel windows of an application. From there, any other GUI Component of the application can be found.

Event methods

Events of different types may be synthesized and added to the event queue for individual Components. In general the syntax is
command component ?args? 
  • actionPerformed component [actionCommand command]
  • adjustmentValueChanged component [-value n] [-type type]
    where the type is one of unitIncrement, unitDecrement, blockIncrement, blockDecrement
  • componentHidden component
  • componentMoved component
  • componentResized component
  • componentShown component
  • focusGained component [-temporary boolean]
  • focusLost component [-temporary boolean]
  • itemStateChanged component
  • keyPressed component [-keyChar char [-modifiers modifiers]
    where the modifiers is a tcl list with possible elements Alt, Ctrl, Meta, Shift
  • keyReleased component [-keyChar char] [-modifiers modifiers]
  • keyTyped component [-keyChar char] [-modifiers modifiers]
  • mouseClicked component [-x x] [-y y] [-clickCount count] [-modifiers modifiers]
  • mousePressed component [-x x] [-y y] [-clickCount count] [-modifiers modifiers]
  • mouseReleased component [-x x] [-y y] [-clickCount count] [-modifiers modifiers]
  • mouseDragged component
  • mouseMoved component
  • windowClosed component
  • windowClosing component
  • windowDeiconified component
  • windowIconified component
  • windowOpened component

Replaying an application

Semantic actions

Users generate input events such as mouse presses and key presses. Some of the Swing components take these input events and turn them into semantic events. For example, clicking the mouse within a JButton generates an ActionEvent in the button. Swing takes care of generating this event, and passes it to the component. This semantic event is not placed in the event queue.

This sequence can be performed within some SwingComponents by methods which perform semantic actions. These include

  • AbstractButton.doClick()
    This draws the button pressed and then released, generates an ActionEvent and delivers it. This works for all subclasses of AbstractButton, such as JButton, JCheckBox, JRadioButton, JToggleButton, JMenuItem.
  • JScrollBar.setValue(int)
    This sets the value and moves the ScrollBar.
  • JList.setSelectedIndex(int)
    This sets the selection and highlights it.

Generating input events

Any event type may be generated and placed in the event queue. Usually you would only do this for events that a user would generate, such as key and mouse presses.

Care must be taken when generating events: for example, a MouseEvent needs to have the x, y coordinates specified. If the component is a different size to what is expected, then these coordinates may be outside of the component. This can often happen when a recording is made of an application and a replay is made under different circumstances.

The Swing event delivery model ensures that an event delivered to a Container will be forwarded to a child if the child is at the right coordinates for the event. So events can be sent directly to the component that will receive them, or to a parent container

Mouse motion

There is no support in Java for moving the mouse around, to better simulate a user interaction. So on the face of it, there would appear to be no reason to include mouseMoved events.

However, Java needs to be able to find the component in which a mouse event occurs, in order to be able to deliver it to that component. To avoid unnecessary calculations, the component is cached until the mouse is moved. So if you want to click on one component, move the mouse and click on another, then you have to have at least one mouseMoved event to invalidate the cache.

For example, in a scrolled list, to move between the list and the scrollBar, you need at least one mouseMoved event.

Examples

JButton

The following Java code gives a JButton in a JFrame
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;

public class SwingButton extends JFrame 
                         implements ActionListener{

    public static void main(String argv[]) {
	new SwingButton().setVisible(true);
    }

    public SwingButton() {
	JButton btn = new JButton("Press me");
	btn.addActionListener(this);
	getContentPane().add("Center", btn);
	pack();
    }

    public void actionPerformed(ActionEvent e) {
	System.out.println("Button pressed: " + e.toString());
    }
}

The following tcl script will press and release the button in various ways:

java::load jnewmarch.replay.PlayTcl

set args [java::new {java.lang.String[]} {0} {}]
java::call SwingButton main $args

sleep 3000

# get the toplevel JFrame, frame0
set frame [frame frame0]

# find the contentsPane container
set pane [$frame getContentPane]

# get the Components it contains
set components [$pane getComponents]

# find the first object, which should be the JButton
set btn [$components get 0]

# the above steps could have been collapsed into
# set btn [[[[frame frame0] getContentsPane] getComponents] get 0]

# press and release the button with delays between
# press and release, by creating events
mousePressed $btn -x 47 -y 38
sleep 1000
mouseReleased $btn -x 47 -y 38
mouseClicked $btn -x 47 -y 38

sleep 3000
# a more robust way ensures that we are
# in middle of the button
set width [$btn getWidth]
set midWidth [expr $width / 2]
set height [$btn getHeight]
set midHeight [expr $height / 2]
mousePressed $btn -x $midWidth -y $midHeight
sleep 1000
mouseReleased $btn -x $midWidth -y $midHeight
mouseClicked $btn -x $midWidth -y $midHeight

sleep 3000
# now do the same action semantically
$btn doClick

JList

The following Java code makes a scrolling list in a Frame
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import com.sun.java.swing.event.*;

public class SwingList extends JFrame 
                       implements ListSelectionListener{

    public static void main(String argv[]) {
	new SwingList().setVisible(true);
    }

    public SwingList() {
	String[] elmts = {"one", "two", "three", "four",
			  "five", "six", "seven", "eight",
	                  "nine", "ten", "eleven", "twelve",
	                  "thirteen", "fourteen", "fifteen",
	                  "sixteen", "seventeen", "eighteen",
	                  "nineteen", "twenty"};
	JScrollPane scrollPane = new JScrollPane();
	JList list = new JList(elmts);
	scrollPane.getViewport().setView(list);
	list.addListSelectionListener(this);
	getContentPane().add(scrollPane, "Center");
	setSize(100, 200);
    }

    public void valueChanged(ListSelectionEvent e) {
	System.out.println("Item selected: " + e.toString());
    }
}
This application looks like

Using input events to select the first item, click the scrollbar to page down and then select the last item can be done by

java::load jnewmarch.replay.PlayTcl

set args [java::new {java.lang.String[]} {0} {}]
java::call SwingList main $args

# get the toplevel JFrame
set frame [frame frame0]

# select the first item
mousePressed $frame -x 18 -y 34
sleep 1000
mouseReleased $frame -x 18 -y 34
mouseClicked $frame -x 18 -y 34

sleep 3000
# move down one block in the scrollbar
mouseMoved $frame -x 87 -y 179
mousePressed $frame -x 87 -y 179
sleep 1000
mouseReleased $frame -x 87 -y 179
mouseClicked $frame -x 87 -y 179

sleep 3000
# select the last item
mouseMoved $frame -x 44 -y 187
mousePressed $frame -x 44 -y 187
sleep 1000
mouseReleased $frame -x 44 -y 187
mouseClicked $frame -x 44 -y 187
This can be improved by getting values of objects and calculating x, y values.

This can also be driven using semantic methods. Not all of the desired methods are part of the Swing set, but in this case we can write tcl code to fill the gap.

# find the contentsPane container
set pane [$frame getContentPane]

# get the Components it contains
set components [$pane getComponents]

# find the first object, which should be the ScrollPane
set scrollPane [$components get 0]

# find the JList
set list [[$scrollPane getViewport] getView]

# find the JScrollBar
set scrollBar [$scrollPane getVerticalScrollBar]


# first let's move back up a block increment
# there isn't a blockIncrement method, so we define 
# one in tcl

proc blockDecrement {scrollBar} {
    set currValue [$scrollBar getValue]

    # get the value "1" for vertical vs horizontal
    # can't ignore direction since this method is
    # overridden in ScrollPane
    set direction [java::field com.sun.java.swing.SwingConstants VERTICAL]
    set blockIncr [$scrollBar getBlockIncrement $direction]

    set newValue [expr $currValue - $blockIncr]
    set minValue [$scrollBar getMinimum]
    if {$newValue < $minValue} {
	set newValue $minValue
    }
    $scrollBar setValue $newValue
}

blockDecrement $scrollBar
sleep 1000

# choose the sixth item
$list setSelectedIndex 5

JTextArea

The following Java code shows a TextArea in a Frame
import java.awt.*;
import com.sun.java.swing.*;

public class SwingText extends JFrame {

    public static void main(String argv[]) {
	new SwingText().setVisible(true);
    }

    public SwingText() {
	JTextArea text = new JTextArea();
	getContentPane().add("Center", text);
	setSize(300, 100);
    }
}

Using input events we can enter text using keyTyped and select text using mouseDragged

java::load jnewmarch.replay.PlayTcl

set args [java::new {java.lang.String[]} {0} {}]
java::call SwingText main $args

set frame [frame frame0]

set pane [$frame getContentPane]

# get the Components it contains
set components [$pane getComponents]

# find the first object, which should be the JTextArea
set text [$components get 0]

sleep 3000
# set focus to the text
mouseMoved $text -x 20 -y 40
mousePressed $text -x 20 -y 40
mouseReleased $text -x 20 -y 40
mouseClicked $text -x 20 -y 40

# add some chars "abcDEF"
sleep 2000
keyTyped $text -keyChar a
sleep 500
keyTyped $text -keyChar b
sleep 500
keyTyped $text -keyChar c

# we don't need to set Shift for keyTyped
sleep 500
keyTyped $text -keyChar D
sleep 500
keyTyped $text -keyChar E
sleep 500
keyTyped $text -keyChar F

sleep 1000
# select a piece of text - slowly
mousePressed $text -x 8 -y 5
mouseDragged $text -x 14 -y 5
sleep 100
mouseDragged $text -x 20 -y 5
sleep 100
mouseDragged $text -x 25 -y 5
mouseReleased $text -x 25 -y 5

# and replace it
sleep 500
keyTyped $text -keyChar x
sleep 500
keyTyped $text -keyChar y
sleep 500
keyTyped $text -keyChar z

Recording events

Recording events in a suitable format will be supported in a later version of this product, once the replay side has stabilised. In the meantime, you can get event information in an undocumented way.

The Java home directory has a subdirectory lib containing a file awt.properties. If you add the line

AWT.EventQueueClass=jnewmarch.replay.TracedEventQueue
to this, then all events that enter the event queue will be printed to standard output.

The event recording is very simplistic right now, and calls toString() for each event that enters the queue. Nevertheless, it can tell you what events are generated and what their fields are (e.g. mouse coordinates).

To turn off event printing, just comment out the line in awt.properties with a `#' at the start of the line.

Applets

There is no direct support for testing applets in this version. I guess you could drive appletviewer using these techniques (since it is a Java application), but I haven't tried it.

I am thinking of ways to test applets, and have some ideas I want to work on - see a later version of this.

Java code versus tcl code

The JACL interpreter has been designed to make full use of the reflection methods of Java. What this amounts to is the following: if you can write it in Java, then you can write it in tcl. This is really, really cool.

This implies that this package could have been written in tcl. That is correct. For example, the Java code to implement the frame command is

        Component frames[] = Frame.getFrames();
        String compName = argv[1].toString();
        for (int n = 0; n < frames.length; n++) {
     
            if (compName.equals(frames[n].getName())) {
                interp.setResult(
                          ReflectObject.newInstance(interp,
                                                    frames[n]));
                return;
            }
        }

This can be done by the following tcl:

proc frame {compName} {
    set frames [java::call java.awt.Frame getFrames]
    set length [$frames length]
    for {set n 0} {$n < $length} {incr n} {
        if {[[$frames get $n] getName] == $compName} {
            return [$frames get $n]
        }
    }
}
There isn't really much difference.

So why isn't the whole of this package in tcl? Well, firstly because I didn't think of it, but also because different parts are easier in different languages. Mainly, I coded the fixed part in Java because it was the "fixed part", and left the rest to tcl. Future versions may change this around.

This version is oriented to testing GUI applications. However, it can be used for any application: simply call the main() method for the application.

Things like testing image equality (or differences) are not in this version. I guess they could be coded in either Java or tcl.

To Do

  • When the replay side is completely stable, add record mechanisms
  • Add a set of tcl functions to encapsulate common sequences of actions
  • Add image comparisons
  • Build a customised version of java.lang.Shell to make loading easier

Known bugs

  • JOptionPane
    There are no semantic actions for pushing the buttons of a JOptionPane. These include the OK, Cancel, Yes, etc buttons, as well as any user defined ones. This is an incompleteness in Swing.
  • JFileChooser
    The components that make up the internal structure cannot be found programmatically. That means you have to aim MouseEvents at locations within JFileChooser and hope they get to the right subcomponent. This is an incompleteness in Swing.
  • Input events generated for AWT components get thrown away when they get back into the peer level. So a mouse click in an AWT Button will not generate an ActionEvent, and a key press in an AWT TextArea will not enter a character. This is an AWT problem that is unlikely to be fixed, which is why I won't be supporting AWT components, only Swing.

Feedback

I have spent time on this - not so much as on other projects such as replayXt, but it shows. (Java is so much easier to work with than Motif!) If people find this useful, I will spend more time on it. So send me feedback - you liked it/thought it useless/ couldn't get it to work; it wouldn't do this/would do that, etc.

Thanks - and enjoy!

Internationalisation help

I really don't understand internationalisation (i18n) and how it works in Java and tcl. But I think it is very important, particularly in a multi-cultural place like Australia. If any expert can point to holes in my code related to this area I would be very grateful. Better yet, if only someone could tell me what the hell is going on in terms that I can understand, then I will ...

Jan Newmarch (http://jan.newmarch.name)
jan@newmarch.name
Last modified: Wed Feb 11 23:12:31 EST 1998
Copyright ©Jan Newmarch