.. _automate-tutorial:

Getting Started with :mod:`dip.automate`
========================================

In this section we work through a number of examples of automating user
interfaces using the :mod:`dip.automate` module.

dip also includes the ``dip-automate`` tool which runs a PyQt application (it
does not need to be a dip application) under the control of an automation
script written using the :mod:`dip.automate` module.  The only requirement
imposed on the PyQt application being automated is that individual widgets that
are to be automated have their ``objectName`` property set as this is how they
are refered to by the automation script.

Automation is typically used for the following reasons:

- during debugging to get the application into a particular state that would be
  time consuming to do manually

- to unit test user interfaces, usually by verifying that the effects on the
  model that the user interface is bound to is as expected

- to produce canned demonstrations of an applications functionality.

Of course in a Test Driven Development environment the first stage would be to
create an automation script that demonstrates a bug.  Once the bug is fixed
then the script is added to the test suite so that any future regressions are
quickly identified.


How Automation Works
--------------------

When using the default PyQt4 toolkit, the :mod:`dip.automate` module is
implemented using the :mod:`~PyQt4.QtTest` module.  The :mod:`~PyQt4.QtTest`
module simulates keyboard and mouse events and applies them to specific
widgets.  This very low level of functionality has some problems:

- you need a reference to the widget

- you need to know the type of the widget so that you know which events to
  simulate to achieve the desired behaviour.

The second problem is particularly significant.  It means that whenever you
change the GUI's implementation (i.e. the widgets it uses) then you have to
change the automation.  Also, in a dip application, the automation would be
specific to a particular.  What you really want is to only have to change the
automation when you change what the GUI *does* (i.e. what changes it makes to
the model it is bound to).

The :mod:`dip.automate` module solves the first problem by requiring that
widgets to be automated have their ``objectName`` property set.  Means are
provided to easily find widgets with a particular name, and to distinguish
between multiple widgets with the same name.  When widgets are created from
declarative definitions by the :mod:`dip.ui` module they are automatically
given predictable names.  They can also be given names explicitly by setting
their :attr:`~dip.ui.IObject.id` attribute.

The second problem is solved by the provision of the
:class:`~dip.automate.IAutomated` interface and its sub-classes.  These define
the simulated operations that different sorts of widgets support.  A toolkit
will provide :term:`adapters<adapter>` that implement those operations on the
particular widgets that the toolkit creates.

The most common simulated operation is ``set`` which is supported by any widget
that can be adapted to the :class:`~dip.automate.IAutomatedEditor` interface.
For example, the default PyQt4 toolkit provides adapters for, amongst others,
:class:`~PyQt4.QtGui.QSpinBox` and :class:`~PyQt4.QtGui.QComboBox`.  Therefore
if you change your GUI to use a spin box rather than a combo box then you do
not need to change any automation as dip will automatically pick the correct
adapter that will simulate the appropriate keyboard and mouse events.


A Simple Example
----------------

The following is an automated version of our very first example.  It can be
dowloaded from :download:`here</examples/automate/simple.py>`.

.. literalinclude:: /examples/automate/simple.py

There are only a couple of changes.  The first one, below, imports from the
:mod:`dip.automate` module::

    from dip.automate import Robot

The second change, below, replaces the call to the
:meth:`~PyQt4.QCoreApplication.exec_()` method that enters the event loop::

    Robot.simulate('name', 'set', "Bill", delay=200)

The first argument to :meth:`~dip.automate.Robot.simulate` is the
``objectName`` of the widget to be automated.  When the view is created the
``objectName`` property of all :term:`editors<editor>` are automatically set to
the :attr:`~dip.ui.IObject.id` of the corresponding editor factory.  The
default :attr:`~dip.ui.IObject.id` is the name of the model attribute that the
editor is bound to.  So, in this case, ``'name'`` identifies the widget that is
bound to the ``'name'`` attribute of the model.

The second argument, ``'set'`` in this case, is the high-level command to be 
simulated.  There is no definitive list of these commands.  Instead the adapter
that is created under the covers that implements the
:class:`~dip.automate.IAutomated` interface is introspected for methods whose
name is of the form ``simulate_xyz()``.  ``xyz`` can then be used as a
high-level command to :meth:`~dip.automate.Robot.simulate`.

The remaining non-keyword arguments are passed as arguments to the high-level
command.  In this case ``'set'`` takes a single argument, ``"Bill"`` that is
the value to set.

Any other arguments are keyword arguments and are optional.  In our example we
specify a ``delay`` of 200 milliseconds.  This is the delay between simulated
events.  If we didn't specify a delay then, at least with an asynchronous
system such as X11, we wouldn't actually see anything displayed even though
the automation is happening.  For the purposes of this example the delay means
that you can see exactly what is happening.


More on Identifying Widgets
---------------------------

When dip searches a GUI for the widget to be automated it stops when it finds
a visible widget with the required value of the ``objectName`` property and
that can be adapted to implement the :class:`~dip.automate.IAutomated`
interface or one of its sub-classes.  Most of the time this is sufficient, but
there may be cases where there is more than one widget that meets these
criteria.  This is particularly true if your application may have more than
one top-level widget (including dialogs) being displayed at the same time.  It
shouldn't be necessary for the developer of an automation script for one part
of an application to be aware of how the GUI's of other parts of the
application that might be displayed at the same time are implemented
internally.

The string passed as the first argument to :meth:`~dip.automate.Robot.record`
and :meth:`~dip.automate.Robot.simulate`, as well as being a simple object
name, can also be a sequence of object names each separated by a colon
character.  Each object name is searched for in turn starting at the widget
found by the previous search (or all top-level widgets if this is the first
one).

Note that you don't need to provide a complete widget path, just sufficient
information to resolve any potential ambiguities.  It is recommended that you
adopt a naming convention for any temporary top-level widgets, i.e. dialogs and
wizards, and use this name (separated by a colon character) with the name of
the particular widget.


Building Up Automation Sequences
--------------------------------

In the example above we use the :meth:`~dip.automate.Robot.simulate` static
method to immediately simulate a single high-level command.  We can also use
the :class:`~dip.automate.Robot` class to record a sequence of commands which
we can then play.

When testing a modal user interface such as a typical dialog then it is
actually necessary to record the simulated commands in this way so that they
can be played once the event loop has started.

In the following more complicated example, which can be downloaded from
:download:`here</examples/automate/dialog.py>`, we show how to do this.  We
also show the use of scoped object names to identify widgets, as described in
the previous section.

.. literalinclude:: /examples/automate/dialog.py

The following line declaratively defines the view as a dialog.  Note that it
explicitly sets an :attr:`~dip.ui.WidgetFactory.id` which will be used as the
value of the ``objectName`` property of the dialog widget that will be created.

.. literalinclude:: /examples/automate/dialog.py
    :start-after: Define the view
    :end-before: Create an instance

The following line creates the :class:`~dip.automate.Robot` instance.  By
default the robot will delay for 200ms between simulating events.

.. literalinclude:: /examples/automate/dialog.py
    :start-after: Create a robot
    :end-before: Enter data

The following lines record the ``set`` high-level commands that will simulate
the events for the widgets that are bound to the ``name`` and ``age``
attributes of the model.

.. literalinclude:: /examples/automate/dialog.py
    :start-after: Enter data
    :end-before: Click the Ok

The widgets are identified using the :attr:`~dip.ui.WidgetFactory.id` of the
:class:`~dip.ui.Dialog` as a scope so that they don't get confused with any
other widget with the same ``name`` or ``age`` object names that the
application may be displaying at the same time.

Note that the :meth:`~dip.automate.Robot.record` method takes the same
arguments as the :meth:`~dip.automate.Robot.simulate` static method.

The following line records the ``'click'`` high-level command that will
simulate a click on the ``Ok`` button.

.. literalinclude:: /examples/automate/dialog.py
    :start-after: Click the Ok
    :end-before: Play the commands

The following line plays the sequence of commands recorded so far.  By default
commands are played immediately.  In our example we must delay this until the
event loop starts, so we use the ``after`` keyword argument to specify that
play should start 0ms after the start of the event loop.

.. literalinclude:: /examples/automate/dialog.py
    :start-after: Play the commands
    :end-before: Enter the dialog

Note that the sequences of commands can be played repeatedly if so desired.


Creating Automation Scripts
---------------------------

So far we have automated our examples by adding the automation calls to the
example code itself.  This is fine when using automation to produce unit tests
as it makes sense to keep the automation and the code together in one place.
However when automating an existing application (either to get the application
to a state that a bug is apparent, or to produce a canned demonstration) you
don't want to have to change the application code itself.  dip provides the
``dip-automate`` tool to allow a PyQt application to be run under the control
of an automation script.

As an example we will use a pure PyQt equivalent of the dialog example above.
This is shown below and can be dowloaded from
:download:`here</examples/automate/pyqt_dialog.py>`.

.. literalinclude:: /examples/automate/pyqt_dialog.py

We won't go through this code as it should be self explanatory.  However it is
interesting to note how many more lines of code are needed than the dip
version.

``dip-automate`` is a command line tool that takes the name of the application
as its argument.  Any preceeding command line flags are handled by
``dip-automate`` itself.  Any following command line flags and arguments are
passed to the application being run. By default ``dip-automate`` will run the
application as normal, without any automation.  To automate our example we have
to write an automation script which we pass to ``dip-automate`` using the
``--commands`` command line flag.

The automation script itself is shown below and can be dowloaded from
:download:`here</examples/automate/automate_pyqt_dialog.py>`.

.. literalinclude:: /examples/automate/automate_pyqt_dialog.py

An automation script is an ordinary Python script that is run by
``dip-automate`` before it runs the application.  After it is run,
``dip-automate`` inspects the script's module dictionary for an object called
``automation_commands`` which should be a sequence of
:class:`~dip.automate.AutomationCommands` instances.  After the application's
event loop is entered these commands will be executed in the order in which
they appear in the sequence.

We will now go through the script looking at the important sections.

The following line causes the automation adapters for the default toolkit to be
imported.  We have to do this only because the application being automated is
not a dip application.  A dip application would implicitly or explicitly
reference a toolkit.

.. literalinclude:: /examples/automate/automate_pyqt_dialog.py
    :start-after: # the toolkit.
    :end-before: class AutomateName

The following lines define a sub-class of
:class:`~dip.automate.AutomationCommands` that records the individual
high-level commands (only the single ``'set'`` in this case) that implement the
command to set the dialog's ``'name'`` widget.

.. literalinclude:: /examples/automate/automate_pyqt_dialog.py
    :start-after: import dip
    :end-before: class AutomateAge

The other class definitions are very similar and define the commands to set the
dialog's ``'age'`` widget and to click the ``Ok`` button.

Note that in a complex environment these classes would probably be defined in
separate modules and imported by each automation script that uses them.

Finally, the following lines show the definition of the sequence of
automation commands that will be executed.

.. literalinclude:: /examples/automate/automate_pyqt_dialog.py
    :start-after: Create the command
    :end-before: EOF

All that needs to be done now is to run ``dip-automate`` as follows::

    dip-automate --commands=automate_pyqt_dialog.py pyqt_dialog.py
