Jemmy 3 user tutorial

Note This tutorial is complementary to javadoc - it only describes principles of Jemmy API usage - not the API itself.

Jemmy 3 have several fundamental principles which everybody should get himself familiar with before starting test implementation.

Relations to Jemmy2. If you are familiar with the Jemmy 2 API (TBD link), you would find this totally different. Everything is implemented in a different way and there is a good reason for that (below). During the tutorial, I will sometimes compare Jemmy 3 & 2 design principles. If you are not familiar with Jemmy2, just ignore those paragraphs.

Jemmy3, firstly, consist of Jemmy Core module which provide functionality not dependent on any particular UI library weather AWT or Swing or anything else. The Jemmy Core uses a couple of classes from AWT, such as java.awt.Robot to perform operations with components and org.jemmy.Point, java.awt.Rectangle to mention screen coordinates. Even Robot, however, in the future could be abstracted from the core so that mobile testing is possible.

The three main aspects of UI testing are:

Lookup

Jemmy2 So, in Jemmy2, the lookup functionality was implemented through "operator"'s constructor and also not-so-convenient static methods. This (the constructors) is acceptable and proven to be good approach for every particular UI component library. It is possible to implement the lookup functionality in a similar way with Jemmy 3. The "generic" Jemmy 3 approach is quite different, however.

So, how do you go about finding a control to deal with. First you have to know some Parent<SomeControlType> which would be some class which is aware of a set of controls of SomeControlType type (for example, javax.swing.JButton in Swing or javafx.scene.control.Button in JavaFX). The set could either be hierarchical, which is typically the case - take AWT or Scenegraph or just about anything, or a plain structure - it does not matter from the usage point of view, because Parent instance knows what is it dealing with.

Such Parents could be (TBD throw in some examples)

Having the Parent, you could proceed to getting Lookup<SomeControlType> instance by calling lookup(LookupCriteria<SomeControlType>) method.
        someParent.lookup(new SomeLookupCriteria<SomeControlType>())// this gives an instance of Lookup<SomeControlType>
        
Jemmy2 LookupCriteria used to be called ComponentChooser.

So, out of those components which someParent is aware about, you, so far picked those which pass the SomeLookupCriteria. Similarly, you could narrow the search with a LookupCriteria and the control type:
        someParent.lookup(Class<SomeControlSubType>new SomeLookupCriteria<SomeControlSubType>())// this gives an instance of Lookup<SomeControlSubType>
        
where SomeControlSubType extends SomeControlType

Lookup<SomeControlType> represents a set of controls, so you could proceed directly to examining one of those by index. You could use get(int) method to get one of this. It is important to notice that the get(int) method returns an instance of SomeControlType, so you do not need to cast it.

You could also check how many are there (size()) or ask the lookup to wait for at least a certain number of them (wait(int)).

You could also get the control wrapped for you (wrap(int)) so that you could use some test logic of dealing with the control. The wrapping will be described in details later in the tutorial.

Perhaps, the most important feature of Lookup is that it itself is a Parent<SomeControlType> (or Parent<SomeControlSubType>). Thus, you could proceed with narrowing down the search further with lookup(LookupCriteria) method (or lookup(Class, LookupCriteria)).

So, finally, looking up for a control gets to ...
        someParent.lookup([Class, ]LookupCriteria)[.lookup([Class, ]LookupCriteria) ...].(get(int)|wrap()|size()|wait(int))
        
It is also worthy to note that, if the default abstract Jemmy Core's implementation of Lookup is used, the actual component hierarchy is not examined up to the moment it has to be. That is, lookup(...) methods itself do not go through the list of controls trying to apply the criteria, when they called. It is only when one of the get(int)|wrap()|size()|wait(int) methods called, the hierarchy is explored. Hence, there is neither memory spend for creating intermediate lists nor performance is wasted. For more info, check the Jemmy developers tutorial.

Wrapping

Now, finding out the control is a necessary step to do. However, in order to deal with the control - to simulate user input, such as text typing, mouse pressing whatnot some code must be used, which is implemented in some other Java class. The most natural way, perhaps, is to "wrap" the actual control which some class which knows how to perform operations on the control type.

Jemmy2 Such classes were the "operators". Wrapping happened naturally as the lookup was implemented through the operator constructors.

In Jemmy 3, the wrapping happens when you call the wrap(int) methods of a lookup. Naturally, the wrappers are UI library specific, so, Jemmy Core could not possible be aware about them. The library specific Jemmy Core extension is taking care about supplying them. For more info, check the Jemmy developers tutorial (TBD).

All of the wrapper classes extend Wrap<CONTROL> class, which, itself, provides core functionality to perform mouse and keyboard operations on the control.

Interfaces

Mouse and keyboard operations are only the basis for user interaction simulation. It is also the only functionality which is applicable to every control, independently of its type. There is much more to user input than this. Good example is text typing. The test developer would expect something in a way of type(String) method for a text field of any kind. Naturally, Wrap class itself does not have anything like this.

That's where the ControlInterface concept comes to the game. Having had a wrapper, gotten by wrap(int) method, you could proceed to treating is as MyInterface (which should be a class/interface implementing/extending ControlInterface - just to have some control over it):
        myWrappedControl.as(MyInterface.class) /// this would be an instance of MyInterface
        
Please consult Jemmy developers tutorial (TBD) on the ways to implement as method.

You could verify in the test code if the wrapper actually could be used as MyInterface by calling myWrappedControl.is(MyInterface.class), however generally it is up to Jemmy Core extension developer which interfaces the wrapper implements.

Getting back to our example of the text typing, the test developer would be able to use code like this:
        myWrappedControl.as(Text.class).type("some new text");
        
One side effect of the interfaces is that the implementations of wrappers could be private or package private, as the wrapper classes are not accessed directly from the test.

Parameterised interfaces

Some interfaces could itself be parameterised by a type. One example of such is Parent<CONTROL> which is itself an interface.

To decrease amount of necessary casting you could use is(Class interfaceClass, Class parameterClass) and as(Class interfaceClass, Class parameterClass) methods. It could look like this:
        someParent.lookup(new SomeCriteria<SomeControlType>()).wrap(0).as(Parent.class, SomeOtherControlType.class).lookup(new SomeOtherCriteria<SomeOtherControlType>).get(0) // this would give an instance of SomeOtherControlType which leaves within some instance of SomeControlType.
        

Waiting and timing

It is important to understand that just about any call in Jemmy involves some time checking. When you call wait(int) on a lookup, it waits for a given number of controls to exist which pass the criteria, when you call get(int) or wrap(int), it first calls wait(int) method to ensure that big enough number of controls are there.

In addition, when you call just about any operation on a control, it has its own time limit to be completed within.

Should you need to implement your own waiting or time check of any kind, it is a good idea to use Waiter supplied by Jemmy.

If the time expires, TimeoutExpiredException is thrown. That allows not to worry about time checking within the test itself. If something goes wrong, test will simply fail with the exception. It also allows to check the negative scenarios by catching the exception.

All the timeouts are specified through Environment instance.

Environment

Any Parent or Lookup always have an instance of Environment associated with it. Whenever Wrap is created, Environment is cloned (i.e. child Environment is created) and assigned to the Wrap.

Environment hierarchy is build the way that you could always change an environment instance not worrying about other parts of the test behaving differently. All the child environments for the one you are changing will behave differently, which gives you the power to customise behaviour of single Wrap (by changing its environment) or the while hierarchy (by changing Environment associated with a Parent).