1
#  Copyright (c) 2005-2020, Enthought, Inc.
2
#  All rights reserved.
3
#
4
#  This software is provided without warranty under the terms of the BSD
5
#  license included in LICENSE.txt and may be redistributed only
6
#  under the conditions described in the aforementioned license.  The license
7
#  is also available online at http://www.enthought.com/licenses/BSD.txt
8
#
9
#  Thanks for using Enthought open source!
10
#
11 10
import contextlib
12

13 10
from traitsui.testing._gui import process_cascade_events
14 10
from traitsui.testing._exception_handling import reraise_exceptions
15 10
from traitsui.testing.tester._ui_tester_registry.default_registry import (
16
    get_default_registry
17
)
18 10
from traitsui.testing.tester.ui_wrapper import UIWrapper
19

20

21 10
class UITester:
22
    """ UITester assists testing of GUI applications developed using TraitsUI.
23

24
    The following actions typically found in tests are supported by the tester:
25

26
    - (1) Create a GUI that will be cleaned up when the test exits.
27
    - (2) Locate the GUI element to be tested.
28
    - (3) Perform the user interaction for side effects (e.g. mouse clicking)
29
    - (4) Inspect GUI element as a user would (e.g. checking the displayed text
30
          on a widget)
31

32
    Creating a GUI
33
    --------------
34
    Given a ``HasTraits`` object, a GUI can be created using the
35
    ``UITester.create_ui`` method::
36

37
        class App(HasTraits):
38
            text = Str()
39

40
        obj = App()
41
        tester = UITester()
42
        with tester.create_ui(obj) as ui:
43
            pass
44

45
    ``create_ui`` is a context manager such that it ensures the GUI is always
46
    disposed of at the end of a test.
47

48
    The returned value is an instance of ``traitsui.ui.UI``. This is the entry
49
    point for locating GUI elements for further testing.
50

51
    Locating GUI elements
52
    ---------------------
53
    After creating an ``UI`` object, ``UITester.find_by_name`` can be used
54
    to locate a specific UI target::
55

56
        obj = App()
57
        tester = UITester()
58
        with tester.create_ui(obj) as ui:
59
            wrapper = tester.find_by_name(ui, "text")
60

61
    The returned value is an instance of ``UIWrapper``. It wraps the
62
    UI target instance found and allows further test actions to be performed on
63
    the target.
64

65
    Performing an interaction (commands)
66
    ------------------------------------
67
    After locating the GUI element, we typically want to perform some user
68
    actions on it for testing. Examples of user interactions that produce side
69
    effects are clicking or double clicking a mouse button, typing some keys
70
    on the keyboard, etc.
71

72
    To perform an interaction for side effects, call the ``UIWrapper.perform``
73
    method with the interaction required. A set of predefined command types can
74
    be found in the ``traitsui.testing.tester.command``.
75

76
    Example::
77

78
        with tester.create_ui(app, dict(view=view)) as ui:
79
            tester.find_by_name(ui, "button").perform(command.MouseClick())
80
            assert app.clicked
81

82
    Inspecting GUI element (queries)
83
    --------------------------------
84
    Sometimes, the test may want to inspect visual elements on the GUI, e.g.
85
    checking if a textbox has displayed the expected value.
86

87
    To perform an interaction for queries, call the ``UIWrapper.inspect``
88
    method with the query required. A set of predefined query types can be
89
    found in the ``traitsui.testing.tester.query``.
90

91
    Example::
92

93
        with tester.create_ui(app, dict(view=view)) as ui:
94
            text = (
95
                tester.find_by_name(ui, "text").inspect(query.DisplayedText())
96
            )
97
            assert text == "Hello"
98

99
    Extending the API
100
    -----------------
101
    The API can be extended by defining a registry for mapping target and
102
    interaction types to a specific implementation that handles the
103
    interaction.
104

105
    For example, suppose there is a custom UI target ``MyEditor``, to implement
106
    a custom interaction type ``ManyMouseClick`` for this target::
107

108
        custom_registry = TargetRegistry()
109
        custom_registry.register_handler(
110
            target_class=MyEditor,
111
            interaction_class=ManyMouseClick,
112
            handler=lambda wrapper, interaction: wrapper._target.do_something()
113
        )
114

115
    Then the registry can be used in a UITester::
116

117
        tester = UITester(registries=[custom_registry])
118

119
    This is how TraitsUI supplies testing support for specific editors; the
120
    default setup of ``UITester`` comes with a registry for testing TraitsUI
121
    editors.
122

123
    Similar the location resolution logic can be extended.
124

125
    See documentation on ``TargetRegistry`` for details.
126

127
    Parameters
128
    ----------
129
    registries : list of TargetRegistry, optional
130
        Registries of interaction for different targets, in the order
131
        of decreasing priority. If provided, a shallow copy will be made.
132
        Default registries are always appended to the list to provide
133
        builtin support for TraitsUI UI and editors.
134
    delay : int, optional
135
        Time delay (in ms) in which actions by the tester are performed. Note
136
        it is propagated through to created child wrappers. The delay allows
137
        visual confirmation test code is working as desired. Defaults to 0.
138

139
    Attributes
140
    ----------
141
    delay : int
142
        Time delay (in ms) in which actions by the tester are performed. Note
143
        it is propagated through to created child wrappers. The delay allows
144
        visual confirmation test code is working as desired.
145
    """
146

147 10
    def __init__(self, registries=None, delay=0):
148 10
        if registries is None:
149 8
            self._registries = []
150
        else:
151 8
            self._registries = registries.copy()
152

153
        # The find_by_name method in this class depends on this registry
154 8
        self._registries.append(get_default_registry())
155 8
        self.delay = delay
156

157 10
    @contextlib.contextmanager
158 10
    def create_ui(self, object, ui_kwargs=None):
159
        """ Context manager to create a UI and dispose it upon exit.
160

161
        Parameters
162
        ----------
163
        object : HasTraits
164
            An instance of HasTraits for which a GUI will be created.
165
        ui_kwargs : dict or None, optional
166
            Keyword arguments to be provided to ``HasTraits.edit_traits``.
167
            Default is to call ``edit_traits`` with no additional keyword
168
            arguments.
169

170
        Yields
171
        ------
172
        ui : traitsui.ui.UI
173
        """
174
        # Nothing here uses UITester, but it is an instance method to preserve
175
        # options to extend using instance states.
176

177 8
        ui_kwargs = {} if ui_kwargs is None else ui_kwargs
178 8
        ui = object.edit_traits(**ui_kwargs)
179 8
        try:
180 8
            yield ui
181
        finally:
182 8
            with reraise_exceptions():
183
                # At the end of a test, there may be events to be processed.
184
                # If dispose happens first, those events will be processed
185
                # after various editor states are removed, causing errors.
186 8
                process_cascade_events()
187 8
                try:
188 8
                    ui.dispose()
189
                finally:
190
                    # dispose is not atomic and may push more events to the
191
                    # event queue. Flush those too.
192 8
                    process_cascade_events()
193

194 10
    def find_by_name(self, ui, name):
195
        """ Find the TraitsUI editor with the given name and return a new
196
        ``UIWrapper`` object for further interactions with the editor.
197

198
        Parameters
199
        ----------
200
        ui : traitsui.ui.UI
201
            The UI created, e.g. by ``create_ui``.
202
        name : str
203
            A single name for retrieving a target on a UI.
204

205
        Returns
206
        -------
207
        wrapper : UIWrapper
208
        """
209 8
        return UIWrapper(
210
            target=ui,
211
            registries=self._registries,
212
            delay=self.delay,
213
        ).find_by_name(name=name)
214

215 10
    def find_by_id(self, ui, id):
216
        """ Find the TraitsUI editor with the given identifier and return a new
217
        ``UIWrapper`` object for further interactions with the editor.
218

219
        Parameters
220
        ----------
221
        ui : traitsui.ui.UI
222
            The UI created, e.g. by ``create_ui``.
223
        id : str
224
            Id for finding an item in the UI.
225

226
        Returns
227
        -------
228
        wrapper : UIWrapper
229
        """
230 8
        return UIWrapper(
231
            target=ui,
232
            registries=self._registries,
233
            delay=self.delay,
234
        ).find_by_id(id=id)

Read our documentation on viewing source code .

Loading