FeaturesPluginsDocs & SupportCommunityPartners

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.


Companion
Projects:
MySQL Database Server   Open JDK: an Open SourceJDK   GlassFish Community: an Open Source Application Server    Mobile & Embedded Community    Open Solaris   java.net - The Source for Java Technology Collaboration   Virtual Box - full virtualizer  Open ESB - The Open Enterprise Service Bus Powered by