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.zipThe
replay.jar
needs to be before the
classes.zip
in order to override
the java.awt.Frame
class.
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
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
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
doClick()
of AbstractButton
java tcl.lang.ShellThis 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 $argswill
String
array with zero elements
main()
method of
the Java application
SwingButton
with an empty command line list
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.
tcl
interpreter:
sleep
frame
actionPerformed
adjustmentValueChanged
focusGained
focusLost
itemStateChanged
keyPressed
keyReleased
keyTyped
mouseClicked
mousePressed
mouseReleased
mouseDragged
mouseMoved
componentHidden
componentMoved
componentResized
componentShown
windowClosed
windowClosing
windowDeiconified
windowIconified
windowOpened
key...
and mouse...
.
sleep
msecs
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.
command component ?args?
actionPerformed component
[actionCommand command]
adjustmentValueChanged component
[-value n] [-type type]
componentHidden component
componentMoved component
componentResized component
componentShown component
focusGained component
[-temporary boolean]
focusLost component
[-temporary boolean]
itemStateChanged component
keyPressed component
[-keyChar char
[-modifiers modifiers]
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
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()
ActionEvent
and delivers it.
This works for all subclasses of
AbstractButton
, such as JButton,
JCheckBox, JRadioButton, JToggleButton,
JMenuItem
.
JScrollBar.setValue(int)
JList.setSelectedIndex(int)
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
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.
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
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 187This 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
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
The Java home directory has a subdirectory lib
containing a file awt.properties
.
If you add the line
AWT.EventQueueClass=jnewmarch.replay.TracedEventQueueto 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.
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.
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
.
java.lang.Shell
to make loading easier
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!