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)
|