Jemmy 3 design
Preface
This document has features of both development plan and design
paper, but it is no one of them.
It is not a
development plan, 'cause it does not provide any data on when
and who will be implementing it. In fact, I do not even know if it
going to bee implemented, or what part of it is going to be
implemented.
It is no design document either, because it does
not provide enough technical details such as interfaces and so
on.
The way I see it, it is a description of next possible
Jemmy extension. If something is going to be changed, it needs to be
done this way.
"Life cycle" idea
Software "life cycle" could be interpreted somewhat
wider than just a time between birth and death - it's rather a time
between "reincarnations". Reincarnations usually happen
when an amount of found design errors become too big and/or when new
features could not be implemented in current design.
Ideally,
during the redesign, it would be perfect to have an ability to change
things inside, simply to make them better. Unfortunately, schedule
is, regularly, too tight for that, so, inside, code stays pretty much
the same for centuries. :)
Current Jemmy design have some
heavy drawbacks, which do not allow to implement some of the fixes
requested even by already filed RFEs, and, more importantly, it does
not allow to implement some new features need to be implemented.
Things could not be done in current design
Native GUI support
One of the new features is a support of native GUI. Almost every
window manager provides some information which could be gathered by
different hacks, so, "almost black box" GUI testing is
possible almost everywhere.
Details of native GUI testing
implementation lays aside of this document, this document only
intended to show how Jemmy could be redesigned in order to make
native support possible.
However, one thing need to be
mentioned right away: we going to allow writing native GUI tests in
Java. There are two good reasons for that:
we would like to use as much of
existing Jemmy functionality as possible.
in part, native GUI we are going to test, is displayed from
java, so, java GUI testing code needs to live with native GUI
testing code.
Another reason for writing native GUI tests in Java, could be that
this way, tests could be made really platform independent. It would
require a higher level of abstraction in test libraries, which is not
describer in this document either.
Naturally, native GUI
testing will require writing some C code, which will be used through
JNI. That C code may be used separately (i.e. without java) - it's up
to that code developer what API to provide, but, from Jemmy point of
view, only small atomic functions with JNI interface is interesting.
Driver registration schema improvement.
I will mention it several times in the document, here I just want
to say that no improvement could not be done in current design.
Things done wrong in current design
Initialization/environment
I keep receiving questions asking: "Why is it so
complicated?".
It is too complicated, that's right. :(
Let me show some examples of this:
System properties
jemmy.robot_dispatching,
jemmy.event_dispatching,
jemmy.shortcut_events are used to specify dispatching
mode during initialization, but after that, mode is specified by
JemmyProperties.getDispatchingModel().
Practically, no class could be
used without complete Jemmy initialization (there are a couple on
issues opened on this subject).
Driver registration schema is not convenient. Besides, it
does not completely match the idea of "operator environment".
More info here: "Environment/initializing".
Driver registration
Besides dynamic linking, which is not good by itself, it supposes
all the drivers to be loaded right during the initialization, which
leads to several problems.
You can find more on the subject in
"Driver registration procedures".
Finding of items in compound components
Under "compound components" I mean components which
contains multiple data occurrences, such as
lists, tables, trees, ...
To demonstrate the problem, let me
just tell that JTableOperator has 38 (!) different
methods of finding cell, row or column.
I show how it could
be done in a more efficient way, in "Operators
for compound types".
Mapping methods.
Although they are very useful and , sometimes, even necessary, the
fact that all operators have them, makes operators API almost
useless.
Other way of having the methods discussed in "Mapping
methods".
Compatibility
That's, probably, the truth, that no redesign could be 100%
compatible, unless old API is supported for compatibility reasons.
That's what gonna happen this time.
Jemmy 3 will be almost
100% compatible with current version. Several places where
compatibility may be broken, marked by c11y mark below.
That
could be done by developing of a new version in some source trees
different from existing. Namely, we will need to have next source
trees:
common: to keep
sources common for all component systems - Jemmy 3 core.
awt and swing:
these two may be combined together.
native: some
shortcuts to JNI functionality.
xwindows: X Windows
and X Toolkit.
motif
Package structure, naturally, changed too: org.netbeans.jemmy
package with subpackages: operators, drivers,
utils contains all new classes as well as
"compatibility" classes (although sources lay in
different source trees: common(new stuff) and
src(compatibility stuff)), new packages:
org.netbeans.jemmy.awt, org.netbeans.jemmy.swing,
org.netbeans.jemmy.native, ... contain the very same
subpackages set.
Old classes still exist, but only keep
functionality which is not present in new classes. Like, for example,
org.netbeans.jemmy.operators.JTreeOperator which code
stays in src source tree, extends
org.netbeans.jemmy.swing.operators.JTreeOperator which
source code is in swing source tree. Some methods could
be dropped in "real" version of operator:
JTreeOperator.findPath(String path, boolean ce, boolean ccs),
so, it presents only in "compatibility" version of class.
"Compatibility" version extends "real" version,
so all new functionality is available and support is done in one
place only.
c11y Some code might not be compiled with a
new version if and operator is casted like
this: (JTreeOperator)<a variable of ComponentOperator type>.
Basic concepts
There are two basic concepts for now: operator and driver,
these two still are basic concepts for next Jemmy design. However,
meaning of these ideas need to be expanded a little:
Driver:
A
class implementing the way things are done for some operator type in
some environment.
Operator:
A class providing
shortcuts to drivers functionality, which also is an environment
container.
Please, notice that I do not mention lookup
functionality in the definition of operator. That's because this kind
of functionality need to be moved into different place: drivers. See
"Lookup drivers".
Development stages
Finish and publish this document
in "plans" section. :)
Create common, awt, swing, native,
x, motif source trees (just directories, build scripts, etc).
Migrate all really common stuff to "common" tree, changing
old classes to extend new ones. During the migration, implement,
what applicable, from changes described below and deprecate, what
related, from the list below.
Basing on "common" tree,
create several simplest classes for motif
widgets. Create some documentation on how to create operators for a
new area.
After second stage is completed, all existing functionality
could started to be migrating to new class structure. Priority of
this task is very low - it could last as
long as necessary.
Changes
Besides creating of new source trees and several, most
important improvement: changes in operators
and drivers, there are some minor
changes.
Deprecation
Basically, all "compatibility"
classes may be deprecated, even though, most of them will stay for a
long time.
Appendixes
Environment/initializing
Currently, part of the environment is held by JemmyProperties
class: timeouts, output, dispatching model (which is now used only to
install driver set, and not used by itself), bundle manager,
etc.
Another part of environment is held by Operator:
StringComparator, PathParser,
ComponentVisualizer as it is really operator-only
environment.
Both environment storages have, basically,
four methods for each environment variable: two
dynamic methods to get/set a value assigned to the object and
two static methods to set/get default value.
By implementing
solution described below, we can get rid of unnecessary methods, and
two storages as well. While implementing this, we also can get rid of
ridiculous Timeout.initTimeout(String, long) method and
everything which is related to it.
So, first we need only one
storage. Since, operator is the storage of environment and we need it
anyway, lets call it EnvironmentOperator. It will hold
everything including the only property which is not used by operators
now: bundle manager.
Then, to get rid of two sets of methods
to define current and default values, let's have default properties
storage: an instance of EnvironmentOperator. This
instance needs to be stored in private static field of
EnvironmentOperator with a couple of methods accessing
the value:
static EnvironmentOperator EnvironmentOperator.getDefaults();
static EnvironmentOperator EnvironmentOperator.setDefaults(EnvironmentOperator);
Thus, we have all the default values in one place. It requires a bit
more code to access it:
EnvironmentOperator.getDefaults().getTimeouts() instead
of JemmyProperies.getCurrentTimeouts(), but API is much
clearer.
Changes in operators
Generalization of Operator
Till now operators were used only for subclasses of
java.awt.Component class, now we cannot talk about
"component" anymore, as, naturally, we cannot talk about
"widget" or "handler" or anything else particular
- we need to go on the higher abstraction level. The highest level in
Java is java.lang.Object.
ObjectOperator
by itself does not make any sense, since there is no functionality we
would like to work with in terms of GUI testing automation.
The
essential property of any object which can be covered by an operator
consist in a fact that this object could be used as a target for a
mouse click. Keyboard operations are important
too, but they are more like directed to
the screen (display) not to an object, so it's not that
essential.
The object which could be clicked by mouse is
rectangle. So, the highest level of abstraction is RectangleOperator.
So, we can write code like this: new RectangleOperator(0, 0,
100, 100).click(); Speaking strictly, RectangleOperator
is the topmost from non-abstract classes. The topmost ancestor of any
operator is, as was already told, EnvironmentOperator,
and there is one more abstract class between RectangleOperator
and EnvironmentOperator: AbstractRectangleOperator.
The necessity of the last one could be explained by the fact that
some operators know their location of screen, and the location is
changing. Besides that, AbstractRectangleOperator could
have several more methods:
boolean isVisible();
void prepareForInput();
Operator class needs to have this method: abstract Object
getSourceObject(); as now it is a container for any
object.
RectangleOperator as a non-abstract
class is useful too - imagine a test trying
to execute an application by mouse click on a desktop icon. The icon
location could be found by using of image searching functionality
similar to what we have in "image" jemmy subpackage. It,
BTW, gives us one more operator: ImageRectangleOperator
with constructor like ImageRectangleOperator(BufferedImage).
But,
surprisingly, such high-level abstraction operators make sense not
only for native GUI testing - they could be used for the very
traditional area of Jemmy using: java AWT and Swing components (see
the very next section).
Operators for compound types
There are several types of components which are used to display
multiple objects: list, table. tree. etc. An approach used in jemmy
for finding something inside this components currently consist in
having a bunch of "find" methods with all possible
combination of parameters.
As was already told, JTableOperator
has, for now, 38 different methods for finding cell, column or row.
This, obviously, shows that something designed wrong. ;)
Now,
let me remind about AbstractRectangleOperator operator.
Obviously, table cells could be represented by a subclass of
AbstractRectangleOperator. The TableCellOperator
will have next interface:
TableCellOperator((String[, StringComparator])|ObjectChooser|TableCellChooser[, int]);
public void click(*);
public void select();
public int getX();//overrides AbstractRectangleOperator.getX();
public int getY();//...
public int getWidth();//...
public int getHeight();//...
public int getColumnIndex();
public int getRowIndex();
Basically, code like this:
Point p = tableOperator.findCell(<criteria>);
tableOperator.clickOnCell(p.x, p.y);
could now be performed like this:
new JTableCellOperator(tableOperator, <criteria>).click();
which looks much more like OOP approach. ;)
Operators similar
to TableCellOperator could be created for all the compound component
types.
Mapping methods
Mapping methods are necessary - nobody objects. However there are
too many of them, sometimes. For example, JTableOperator contains 106
mapping methods.
There are two reasons why we need to do
something with it: API (javadoc) is too big, and code is too big. Try
to find something in JTableOperator's javadoc - it's not very easy.
As for code size, JTableOperator has 2094 lines, more then a third
part of which (746) is mapping methods.
There is a simple
solution allowing to make situation much better: we need to move
mapping methods into special intermediate classes: classes, ancestors
of operator classes but inheritor of classes extended by operators.
We can call them *Map: <inheritor component
name>Operator extends <inheritor component
name>Map, which extends <ancestor component
name>Operator.
Proposed solution does not look too
neat, however, it minimizes code, javadoc, and it is compatible. In
theory, *Map classes could even be generated on fly
during compilation. >8( )
Environment
As we already know, all the environment (except for driver
registration described in Registration procedures
section) is held by EnvironmentOperator. Rest of the schema stays
pretty much the same as described in
http://jemmy.netbeans.org/OperatorsEnvironment.html, except now,
every operator takes environment from another operator. The topmost
one is the operator described in "environment" section
above.
Changes in drivers
Component => object
Most of the driver types can be used not only for AWT and Swing
components, but also for any object having the same sense from GUI
point of view: ListDriver, for instance, could be used not only for
JList and List, but also any other list-like component.
Some
of the drivers can even be used for any object (ButtonDriver),
rectangle (MouseDriver), or, even, anything which can have a focus
(FocusDriver).
Other drivers like, TableDriver could be used
only for a component which represents a table data (i.e. has cells,
rows and columns).
This all leads to a conclusion that there
must be an interface specifying a type of a component(operator) which
could be processed by each driver type.
It could be
implemented as a driver subinterface: TableDriver.TableOperator. See
"Registration procedures" for more
info.
Lookup drivers
There is a need in changing of component lookup algorithms.
Currently a component lookup consist of two steps: looking for a
window and, then, looking for the component.
It's acceptable
for most component types, but for some of them it turns into very
complicated code. Imaging JPopupOperator looking for a popup
containing a menu item with a specified text. It turns to a search
like this: wait for a window which has a popup menu inside, which has
a menuitem inside, which has specified text. Changes proposed below
shows how to make life simple.
Currently,
operators serve two purposes - they provide shortcuts to
methods simulating user input (actual
implementation is inside drivers methods) and they contain
algorithms for component lookup.
Having action reproducing in
different classes (in drivers) allows to change (customize) test
behavior without actual changing of test code. But lookup
algorithms are the subject for customization too.
Which
gives an idea of lookup drivers: drivers containing algorithms for
component finding. The registration of these drivers is exactly the
same as for other drivers (see Registration
procedures for more info) - they are tied to operator
type.
Having that, we don't even need different methods to
wait and find components - it could be implemented by different
drivers. Default one should, naturally, do the waiting.
Since
we cannot talk about "component" anymore, top-level lookup
driver looks like this:
public interface LookupDriver {
public Object lookup(Object parent, ObjectChooser chooser);
}
This, of course, requires an ObjectChooser interface:
public interface ObjectChooser {
public boolean checkObject(Object obj);
public String getDescription();
}
Having a couple of auxiliary classes, we make lookup functionality
very flexible for AWT components:
public class ComponentObjectChooser implements ObjectChooser {
public ComponentObjectChooser(ComponentChooser subChooser);
public final boolean checkObject(Object obj);
public String getDescription();
}
public class CompoundChooser implements ObjectChooser {
public CompoundChooser(ObjectChooser chooser);
public CompoundChooser addChooser(ObjectChooser chooser); //returns a new chooser which combines criteria
public boolean checkObject(Object obj);
public String getDescription();
}
Obviously, it would be flexible for any other object types lookup,
like, for example, tree cell lookup will, naturally, be
implemented as driver.
Registration procedures
Driver registration algorithms are very ugly now - that's a fact.
:(
First, it is very bad to use "dynamic linking"
(i.e. registration driver type for operator class name). Second, it's
not really good to install all the driver set at startup,
although, such a possibility needs to be provided.
Current
driver registration design cases different problems, some of which
are filed as RFE already.
In order to get to correct
registration procedures, let me mention that MouseDriver
implementation is no different from any other class from
operator's environment. It's very much the
same important as anything else: StringComparator,
ComponentVisualizer or anything else.
Thus, it need to have
similar registration procedures: setMouseDriver(MouseDriver),
getMouseDriver().
This, naturally, means that
this procedures do not need to be in one class, but instead, each
operator type (or one of its ancestors) need to provide methods for
any the driver types it uses.
Which leads to an interface
(MouseDriver.MouseOperator) which would declare such
methods. Obviously, the implementation of these particular methods
(setMouseDriver(MouseDriver) and getMouseDriver())
will present in one class only: AbstractRectancleOperator
Back
to operator environment
That's fine, but there is one more
complication connected to passing operator environment between
operators. Let's suppose, one operator (JTableOoperator) specified a
non-default TableDriver. Then, the operator was passed as an env.
operator to an operator of a different type (ContainerOperator),
which, again, was used as an env. for another JTableOperator. We
expect the second table operator having the same driver as the fist,
don't we?
To do it, we only need to store all the
environment in a table belong to the topmost operator type.
Real driver accessing methods will then just store/ get the instance
from that table.
A list of minor changes
Here is a list of other minor changes need
to be done:
Move interfaces and subclasses from Operator. Classes
and interfaces like StringComparator, etc. They are used wider
anyway.
Move QueueTool.QueueAction from QueueTool and generalize
it. It should rather be default Action implementation supposed
to be used from different "unfriendly"
threads like EventQueue.
JavaVersion class Instead of checking of
System.getProperty("java.version") each time, it'd better
to have a class able to say what's supported and what isn't.
Improve TestOut See 24953 issue.
Timeout names. Names of timeouts used by each operator
types are hardcoded and there is no way to get the names from java
code - you have to hardcode them into test code too. That's simply
weird.
Instead of that, we need to have constants (public
static final String fields) like:
AbstractButtonOperator.PUSH_BUTTON_TIMEOUT_NAME.