pianola-0.1.0: Remotely controlling Java Swing applications

Safe HaskellNone

Pianola.Tutorial

Contents

Synopsis

Setting up the test application

This tutorial assumes that the Java Swing test application bundled with the package is up and running. To set up the application, unpack the package sources with

>>> cabal unpack pianola

Then go to the

>>> pianola/backends/java-swing

folder and follow the instructions in the README to compile and install the pianola agent jar into a local Maven repository. Finally, go to the

>>> pianola/backends/java-swing-testapp

folder and follow the instructions in the README to configure and launch the test application.

This tutorial will refer to the Java Swing application as the Application Under Test (AUT).

First steps

This minimal pianola script prints the title of the AUT's main window:

 import qualified Data.Text as T
 import Network 
 import Control.Monad
 import Control.Error
 
 import Pianola.Model.Swing.Driver
 
 extractTitle :: Pianola Protocol LogEntry (GUI Protocol) T.Text
 extractTitle = peek $ mainWindow >=> title  
 
 main:: IO ()
 main = do 
     let port =  PortNumber . fromIntegral $ 26060
         endpoint = Endpoint "127.0.0.1" port
         screenshots = screenshotStream "/tmp"   
     r <- runEitherT $ simpleSwingDriver endpoint extractTitle screenshots
     putStrLn $ case r of
        Left err -> do
           "ERROR: " ++ show err
        Right title -> "title: " ++ show title

Logging

We could also have logged the title inside the Pianola monad:

 extractTitle2 :: Pianola Protocol LogEntry (GUI Protocol) ()
 extractTitle2 = do 
     txt <- peek $ mainWindow >=> title  
     logmsg txt

Log messages emitted in the Pianola monad are printed as they are generated, unlike in a Writer monad. This is useful to check the progress of long-running scripts.

The expression to the right of peek has type Glance. Glance is just a type synonym for the Kleisli arrows of a particular monad. This monad allows some effects and disallows others. Turns out that logging is one of the allowed effects, so we can also emit messages inside a Glance:

 import Data.Functor.Identity

 extractTitle3 :: Pianola Protocol LogEntry (GUI Protocol) ()
 extractTitle3 = do 
     peek $ \gui -> do
         win <- mainWindow gui 
         logmsg $ runIdentity $ title win

Why bother at all with logging inside a Glance, instead of always doing it in the Pianola monad? As it happens, nondeterminism (returning several, or zero, results) is another of the allowed effects inside Glances. We can log about objects explored in search branches even if those branches eventually fail to produce any result. This can be useful for debugging.

Clicking buttons

Enough with just inspecting the GUI and logging the results! What if we actually want to effect some change, like clicking a button? For this, we can use the poke function.

 clicky :: Pianola Protocol LogEntry (GUI Protocol) ()
 clicky = poke $ mainWindow >=> contentPane >=> descendants >=> hasText (=="open dialog") >=> clickButton  

Like peek, poke takes a Glance as a parameter. Unlike peek, it only accepts Glances which return an action of type Sealed. Values of this type encapsulate actions to be performed on the GUI. Users never construct values of Sealed. Instead, they find them while exploring the data structure which represents the GUI.

In the example, the Glance supplied to poke is made up of smaller Glances combined with >=>. One which extracts the main window of the application from the GUI as a whole, one to access the content pane of the window, one for obtaining all the sub-components of the content pane, one that selects components according to their text content, and finally one which extracts the click action of the component if the component is a button.

Grouping actions

Suppose we want to click the clear button in the window before we click the open dialog button. We can do it like this:

 groupy :: Pianola Protocol LogEntry (GUI Protocol) ()
 groupy = do
    poke $ mainWindow >=> contentPane >=> descendants >=> hasText (=="clear") >=> clickButton  
    poke $ mainWindow >=> contentPane >=> descendants >=> hasText (=="open dialog") >=> clickButton  

There is a lot of repetition in the Glances. We can factor it out with the with function:

 groupy2 :: Pianola Protocol LogEntry (GUI Protocol) ()
 groupy2 = with (mainWindow >=> contentPane >=> descendants) $ do
    poke $ hasText (=="clear") >=> clickButton  
    poke $ hasText (=="open dialog") >=> clickButton  

This is equivalent to the previous code:

 groupy3 :: Pianola Protocol LogEntry (GUI Protocol) ()
 groupy3 = with mainWindow $ with contentPane $ with descendants $ do
    poke $ hasText (=="clear") >=> clickButton  
    poke $ hasText (=="open dialog") >=> clickButton  

In this case, we could even go a little further:

 groupy4 :: Pianola Protocol LogEntry (GUI Protocol) ()
 groupy4 = with mainWindow $ with contentPane $ with descendants $ do
       forM_ ["clear","open dialog] $ \txt ->
          poke $ hasText (==txt) >=> clickButton 

Logging, again

Besides purely textual messages, we can also log window image captures. The easiest way is by using the logcapture function.

 captureExample :: Pianola Protocol LogEntry (GUI Protocol) () 
 captureExample = with mainWindow $ logcapture

Remember that in the first example of the tutorial, one of the parameters passed to simpleSwingDriver was an infinite stream of screenshot filenames, created using screenshotStream. Each time a screenshot is taken, the current head of the filename stream is used to store the screenshot.

A certain degree of automatic logging is also supported. The autolog function trasforms a Pianola by automatically inserting log messages after each executed action. The messages are not very detailed, but they distinguish between types of actions.

 autologged = autolog groupy3

Sleeping

If we need to introduce a delay between two steps of a pianola computation, we can use the sleep function, supplying it with the delay in seconds.

 delayed1 :: Pianola Protocol LogEntry (GUI Protocol) ()
 delayed1 = with mainWindow $ with contentPane $ with descendants $ do
    poke $ hasText (=="clear") >=> clickButton  
    sleep 3
    poke $ hasText (=="open dialog") >=> clickButton  

Going slow

Sometimes it can be useful to play a computation in slow motion. Instead of inserting sleep commands manually, we can use the ralentize function which automatically inserts a delay after each action performed on the GUI.

 ralentized = ralentize 4 groupy3 

Failing

We can abort a pianola computation with pfail. It is recommended that the user emits an informative log message before calling this function.

Another way for a pianola computation to fail is when a glance to returns no results (unless we are using peekMaybe or similar function, see the next section.)

Optional components

Sometimes we want to target a component which may be present in the interface, but without failing outright if the component can't be found. In those cases we can use peekMaybe or pokeMaybe, which return Nothing when the Glance fails to find any result.

 pokeOptional = with (mainWindow >=> contentPane >=> descendants) $ do
    pokeMaybe $ hasText (=="no component which this text") >=> clickButton  

Retries

For components which only appear after a certain delay, a few retries may be needed until the component is found. To target these kinds of components, we can use the retryPeek1s, retryPoke1s and withRetry1s functions.

 retryExample = with mainWindow $ with contentPane $ do
     poke $ clickButtonByText (=="open slow dialog")
     with window $ do
         pmaybe pfail $ withRetry1s 14 childWindow $ do       
             with contentPane $ poke $ clickButtonByText (=="close dialog")

These functions can be combined with pmaybe and pfail to abort the pianola computation if the component is not found after the specified retries.

But what if instead of waiting for a component to appear, we want to wait for a component to dissapear? We can do this with the help of the missing function:

 poke $ clickButtonByText (=="open slow dialog")
 with window $ do
     pmaybe pfail $ withRetry1s 14 childWindow $ do       
         with contentPane $ poke $ clickButtonByText (=="close dialog")
     logmsg "clicked delayed close button"
     pmaybe pfail $ retryPeek1s 14 $ missing childWindow 

Complex components

Complex Swing components like lists, tables and trees are represented Haskell-side as composed of Cell values. See the documentation for Cell and ComponentInfo for more details.

Cells do not count as regular children of a component and they do not appear among the results of a children or descendants call. Instead, see the family of functions listCellByText, tableCellByText and treeCellByText.