.. _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 PyQt5 toolkit, the :mod:`dip.automate` module is implemented using the :mod:`~PyQt5.QtTest` module. The :mod:`~PyQt5.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` 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 PyQt5 toolkit provides adapters for, amongst others, :class:`~PyQt5.QtWidgets.QSpinBox` and :class:`~PyQt5.QtWidgets.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`. .. 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:`~dip.ui.Application.execute` 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` 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`, 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`. .. 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`. .. 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