robotframework-desktop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DesktopLibrary/__init__.py +278 -0
- DesktopLibrary/context.py +272 -0
- DesktopLibrary/cursor.py +243 -0
- DesktopLibrary/engines/__init__.py +4 -0
- DesktopLibrary/engines/image.py +174 -0
- DesktopLibrary/engines/ui.py +192 -0
- DesktopLibrary/geometry.py +187 -0
- DesktopLibrary/keywords/__init__.py +23 -0
- DesktopLibrary/keywords/application.py +149 -0
- DesktopLibrary/keywords/base.py +90 -0
- DesktopLibrary/keywords/config.py +159 -0
- DesktopLibrary/keywords/cursor.py +201 -0
- DesktopLibrary/keywords/element.py +260 -0
- DesktopLibrary/keywords/find.py +136 -0
- DesktopLibrary/keywords/keyboard.py +171 -0
- DesktopLibrary/keywords/mouse.py +357 -0
- DesktopLibrary/keywords/ocr.py +166 -0
- DesktopLibrary/keywords/region.py +288 -0
- DesktopLibrary/keywords/wait.py +178 -0
- DesktopLibrary/locator.py +223 -0
- DesktopLibrary/overlay.py +180 -0
- robotframework_desktop-0.1.0.dist-info/METADATA +215 -0
- robotframework_desktop-0.1.0.dist-info/RECORD +25 -0
- robotframework_desktop-0.1.0.dist-info/WHEEL +5 -0
- robotframework_desktop-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from robot.api.deco import keyword
|
|
2
|
+
|
|
3
|
+
from ..cursor import normalize_cursor_types
|
|
4
|
+
from .base import KeywordGroup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CursorKeywords(KeywordGroup):
|
|
8
|
+
@keyword
|
|
9
|
+
def get_current_cursor_type(self) -> str:
|
|
10
|
+
"""Returns the current mouse cursor type.
|
|
11
|
+
|
|
12
|
+
Possible values: ``arrow``, ``ibeam``, ``wait``, ``cross``, ``uparrow``,
|
|
13
|
+
``sizenwse``, ``sizenesw``, ``sizewe``, ``sizens``, ``sizeall``, ``no``,
|
|
14
|
+
``hand``, ``appstarting``, ``help``, ``custom`` (application specific
|
|
15
|
+
cursor) or ``none`` (hidden cursor).
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
| ${cursor}= | Get Current Cursor Type |
|
|
19
|
+
| Should Be Equal | ${cursor} | arrow |
|
|
20
|
+
"""
|
|
21
|
+
return self.ctx.cursor.current()
|
|
22
|
+
|
|
23
|
+
@keyword
|
|
24
|
+
def cursor_should_be(self, expected):
|
|
25
|
+
"""Fails unless the current cursor is one of ``expected``.
|
|
26
|
+
|
|
27
|
+
``expected`` can be a name, numeric id (e.g. ``32514``), a comma
|
|
28
|
+
separated string or a list.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
| Cursor Should Be | arrow |
|
|
32
|
+
| Cursor Should Be | arrow,ibeam |
|
|
33
|
+
| Cursor Should Be | 32512 |
|
|
34
|
+
"""
|
|
35
|
+
types = normalize_cursor_types(expected)
|
|
36
|
+
current = self.ctx.cursor.current()
|
|
37
|
+
if current not in types:
|
|
38
|
+
raise AssertionError(
|
|
39
|
+
f"Cursor is '{current}', expected one of: {', '.join(types)}."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@keyword
|
|
43
|
+
def cursor_should_not_be(self, unexpected):
|
|
44
|
+
"""Fails if the current cursor is one of ``unexpected``.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
| Cursor Should Not Be | wait |
|
|
48
|
+
| Cursor Should Not Be | wait,appstarting |
|
|
49
|
+
"""
|
|
50
|
+
types = normalize_cursor_types(unexpected)
|
|
51
|
+
current = self.ctx.cursor.current()
|
|
52
|
+
if current in types:
|
|
53
|
+
raise AssertionError(f"Cursor should not be '{current}'.")
|
|
54
|
+
|
|
55
|
+
@keyword
|
|
56
|
+
def wait_for_cursor_type(self, expected, timeout='30s') -> str:
|
|
57
|
+
"""Waits until the cursor becomes one of ``expected`` and returns it.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
| Wait For Cursor Type | hand |
|
|
61
|
+
| Wait For Cursor Type | 32649 |
|
|
62
|
+
| Wait For Cursor Type | hand,arrow | 10s |
|
|
63
|
+
| ${cursor}= | Wait For Cursor Type | ${cursor_list} | timeout=1 min |
|
|
64
|
+
"""
|
|
65
|
+
types = normalize_cursor_types(expected)
|
|
66
|
+
secs = self.ctx.to_seconds(timeout)
|
|
67
|
+
ok, cursor, _ = self.ctx.cursor.wait_until(
|
|
68
|
+
lambda c: c in types, secs, 0, 'type'
|
|
69
|
+
)
|
|
70
|
+
if not ok:
|
|
71
|
+
raise AssertionError(
|
|
72
|
+
f"Cursor did not become {', '.join(types)} in {secs:g} seconds "
|
|
73
|
+
f"(current: '{cursor}')."
|
|
74
|
+
)
|
|
75
|
+
return cursor
|
|
76
|
+
|
|
77
|
+
@keyword
|
|
78
|
+
def wait_until_cursor_is_not_busy(
|
|
79
|
+
self, timeout='', stable_duration='', fail_on_timeout=None
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Waits until the cursor is not busy for ``stable_duration`` and
|
|
82
|
+
returns the final cursor type.
|
|
83
|
+
|
|
84
|
+
Busy types are defined by `Set Busy Cursor Types` (default ``wait`` and
|
|
85
|
+
``appstarting``). Empty arguments use the values configured on import
|
|
86
|
+
or with `Set Busy Cursor Wait`. Unlike the automatic wait, this keyword
|
|
87
|
+
always waits the stability window, even when the cursor is not busy at start.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
| Click | load_button.png |
|
|
91
|
+
| Wait Until Cursor Is Not Busy |
|
|
92
|
+
| Wait Until Cursor Is Not Busy | 2 min | 1s |
|
|
93
|
+
| Wait Until Cursor Is Not Busy | 10s | fail_on_timeout=False |
|
|
94
|
+
"""
|
|
95
|
+
mon = self.ctx.cursor
|
|
96
|
+
secs = self.ctx.to_seconds(timeout, mon.timeout)
|
|
97
|
+
stable = self.ctx.to_seconds(stable_duration, mon.stable_duration)
|
|
98
|
+
fail = (
|
|
99
|
+
mon.fail_on_timeout
|
|
100
|
+
if fail_on_timeout in (None, '')
|
|
101
|
+
else _bool(fail_on_timeout)
|
|
102
|
+
)
|
|
103
|
+
return mon.wait_not_busy(secs, stable, fail)
|
|
104
|
+
|
|
105
|
+
@keyword
|
|
106
|
+
def wait_for_normalized_cursor(
|
|
107
|
+
self, stable_duration='2s', timeout='30s', cursor=None
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Waits until the cursor stops changing for ``stable_duration``.
|
|
110
|
+
|
|
111
|
+
If ``cursor`` is given, the stable cursor must be of that type.
|
|
112
|
+
Useful when an action makes the cursor oscillate between types.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
| Wait For Normalized Cursor |
|
|
116
|
+
| Wait For Normalized Cursor | stable_duration=1.5 |
|
|
117
|
+
| Wait For Normalized Cursor | cursor=arrow | timeout=60 |
|
|
118
|
+
| ${final}= | Wait For Normalized Cursor | 1s |
|
|
119
|
+
"""
|
|
120
|
+
secs = self.ctx.to_seconds(timeout)
|
|
121
|
+
stable = self.ctx.to_seconds(stable_duration)
|
|
122
|
+
types = (
|
|
123
|
+
normalize_cursor_types(cursor)
|
|
124
|
+
if cursor not in (None, '')
|
|
125
|
+
else None
|
|
126
|
+
)
|
|
127
|
+
ok, final, changes = self.ctx.cursor.wait_until(
|
|
128
|
+
lambda c: types is None or c in types, secs, stable, 'normalized'
|
|
129
|
+
)
|
|
130
|
+
if not ok:
|
|
131
|
+
wanted = f" as {', '.join(types)}" if types else ''
|
|
132
|
+
raise AssertionError(
|
|
133
|
+
f"Cursor did not stabilize{wanted} in {secs:g} seconds (current: '{final}', "
|
|
134
|
+
f'changes: {len(changes)}).'
|
|
135
|
+
)
|
|
136
|
+
return final
|
|
137
|
+
|
|
138
|
+
@keyword
|
|
139
|
+
def set_busy_cursor_wait(
|
|
140
|
+
self,
|
|
141
|
+
enabled=True,
|
|
142
|
+
timeout='',
|
|
143
|
+
stable_duration='',
|
|
144
|
+
fail_on_timeout=None,
|
|
145
|
+
after_delay='',
|
|
146
|
+
) -> dict:
|
|
147
|
+
"""Configures the automatic busy cursor wait and returns the previous
|
|
148
|
+
configuration as a dictionary.
|
|
149
|
+
|
|
150
|
+
When enabled, keywords that act on the application wait while the
|
|
151
|
+
cursor is busy before searching elements, before mouse/keyboard
|
|
152
|
+
actions and after the action (watching ``after_delay`` seconds for the
|
|
153
|
+
cursor to become busy). When the cursor stays busy longer than
|
|
154
|
+
``timeout`` the keyword fails (``fail_on_timeout=True``) or logs a
|
|
155
|
+
warning and continues.
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
| Set Busy Cursor Wait | False |
|
|
159
|
+
| Set Busy Cursor Wait | True | timeout=2 min | stable_duration=1s |
|
|
160
|
+
| Set Busy Cursor Wait | True | fail_on_timeout=False |
|
|
161
|
+
| Set Busy Cursor Wait | True | after_delay=0.5s |
|
|
162
|
+
| ${old}= | Set Busy Cursor Wait | False |
|
|
163
|
+
| Set Busy Cursor Wait | &{old} |
|
|
164
|
+
"""
|
|
165
|
+
mon = self.ctx.cursor
|
|
166
|
+
previous = {
|
|
167
|
+
'enabled': mon.enabled,
|
|
168
|
+
'timeout': mon.timeout,
|
|
169
|
+
'stable_duration': mon.stable_duration,
|
|
170
|
+
'fail_on_timeout': mon.fail_on_timeout,
|
|
171
|
+
'after_delay': mon.after_delay,
|
|
172
|
+
}
|
|
173
|
+
mon.enabled = _bool(enabled)
|
|
174
|
+
if str(after_delay).strip():
|
|
175
|
+
mon.after_delay = self.ctx.to_seconds(after_delay)
|
|
176
|
+
if str(timeout).strip():
|
|
177
|
+
mon.timeout = self.ctx.to_seconds(timeout)
|
|
178
|
+
if str(stable_duration).strip():
|
|
179
|
+
mon.stable_duration = self.ctx.to_seconds(stable_duration)
|
|
180
|
+
if fail_on_timeout not in (None, ''):
|
|
181
|
+
mon.fail_on_timeout = _bool(fail_on_timeout)
|
|
182
|
+
return previous
|
|
183
|
+
|
|
184
|
+
@keyword
|
|
185
|
+
def set_busy_cursor_types(self, types) -> list:
|
|
186
|
+
"""Sets which cursor types are considered busy and returns the
|
|
187
|
+
previous list. Default: ``wait,appstarting``.
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
| Set Busy Cursor Types | wait,appstarting,custom |
|
|
191
|
+
| Set Busy Cursor Types | wait |
|
|
192
|
+
"""
|
|
193
|
+
previous = list(self.ctx.cursor.busy_types)
|
|
194
|
+
self.ctx.cursor.busy_types = normalize_cursor_types(types)
|
|
195
|
+
return previous
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _bool(value) -> bool:
|
|
199
|
+
if isinstance(value, bool):
|
|
200
|
+
return value
|
|
201
|
+
return str(value).strip().lower() in ('true', '1', 'yes', 'on')
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from robot.api.deco import keyword
|
|
2
|
+
|
|
3
|
+
from ..geometry import Match
|
|
4
|
+
from ..locator import parse_locator
|
|
5
|
+
from .base import KeywordGroup, busy_action
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ElementKeywords(KeywordGroup):
|
|
9
|
+
"""Keywords that need a real UI Automation element (UI locators only)."""
|
|
10
|
+
|
|
11
|
+
def _element(self, element):
|
|
12
|
+
if isinstance(element, Match) and element.element is not None:
|
|
13
|
+
self.ctx.cursor.auto_wait()
|
|
14
|
+
self.ctx.last_match = element
|
|
15
|
+
return element.element
|
|
16
|
+
locator = parse_locator(element)
|
|
17
|
+
if locator.kind != 'ui':
|
|
18
|
+
raise ValueError(
|
|
19
|
+
f"'{locator.describe()}' is not a UI locator. Use e.g. 'automation_id:txtName'."
|
|
20
|
+
)
|
|
21
|
+
match = self.ctx.find(locator)
|
|
22
|
+
return match.element
|
|
23
|
+
|
|
24
|
+
@keyword
|
|
25
|
+
def get_element_text(self, element) -> str:
|
|
26
|
+
"""Returns the text/value of a UI element.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
| ${text}= | Get Element Text | automation_id:lblStatus |
|
|
30
|
+
| Should Be Equal | ${text} | Saved |
|
|
31
|
+
"""
|
|
32
|
+
el = self._element(element)
|
|
33
|
+
for getter in ('get_value', 'window_text'):
|
|
34
|
+
try:
|
|
35
|
+
value = getattr(el, getter)()
|
|
36
|
+
if value not in (None, ''):
|
|
37
|
+
return str(value)
|
|
38
|
+
except Exception:
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
return '\n'.join(t for t in el.texts() if t)
|
|
42
|
+
except Exception:
|
|
43
|
+
return ''
|
|
44
|
+
|
|
45
|
+
@keyword
|
|
46
|
+
@busy_action
|
|
47
|
+
def set_element_value(self, element, value: str):
|
|
48
|
+
"""Sets the value of an edit control directly (no typing simulation).
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
| Set Element Value | automation_id:txtName | John |
|
|
52
|
+
"""
|
|
53
|
+
el = self._element(element)
|
|
54
|
+
if hasattr(el, 'set_edit_text'):
|
|
55
|
+
el.set_edit_text(str(value))
|
|
56
|
+
elif hasattr(el, 'set_value'):
|
|
57
|
+
el.set_value(str(value))
|
|
58
|
+
else:
|
|
59
|
+
el.set_focus()
|
|
60
|
+
el.type_keys(str(value), with_spaces=True, set_foreground=True)
|
|
61
|
+
|
|
62
|
+
@keyword
|
|
63
|
+
def get_element_property(self, element, name: str):
|
|
64
|
+
"""Returns an element property. Supported names include ``name``,
|
|
65
|
+
``automation_id``, ``class_name``, ``control_type``, ``enabled``,
|
|
66
|
+
``visible``, ``rectangle``, ``handle``, ``process_id`` and any
|
|
67
|
+
pywinauto ``element_info`` attribute.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
| ${enabled}= | Get Element Property | automation_id:btnSave | enabled |
|
|
71
|
+
| ${type}= | Get Element Property | name:OK | control_type |
|
|
72
|
+
"""
|
|
73
|
+
el = self._element(element)
|
|
74
|
+
info = el.element_info
|
|
75
|
+
key = str(name).lower()
|
|
76
|
+
special = {
|
|
77
|
+
'name': lambda: info.name,
|
|
78
|
+
'automation_id': lambda: info.automation_id,
|
|
79
|
+
'class_name': lambda: info.class_name,
|
|
80
|
+
'control_type': lambda: info.control_type,
|
|
81
|
+
'enabled': el.is_enabled,
|
|
82
|
+
'visible': el.is_visible,
|
|
83
|
+
'rectangle': lambda: self.ctx.ui.region_of(el),
|
|
84
|
+
'handle': lambda: info.handle,
|
|
85
|
+
'process_id': lambda: info.process_id,
|
|
86
|
+
'text': el.window_text,
|
|
87
|
+
}
|
|
88
|
+
if key in special:
|
|
89
|
+
return special[key]()
|
|
90
|
+
if hasattr(info, name):
|
|
91
|
+
return getattr(info, name)
|
|
92
|
+
if hasattr(el, name):
|
|
93
|
+
attr = getattr(el, name)
|
|
94
|
+
return attr() if callable(attr) else attr
|
|
95
|
+
raise ValueError(f"Unknown property '{name}'.")
|
|
96
|
+
|
|
97
|
+
@keyword
|
|
98
|
+
@busy_action
|
|
99
|
+
def select_combobox_item(self, element, item: str):
|
|
100
|
+
"""Selects ``item`` (text or 0 based index) in a combo box or list.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
| Select Combobox Item | automation_id:cmbColor | Blue |
|
|
104
|
+
| Select Combobox Item | automation_id:cmbColor | 2 |
|
|
105
|
+
"""
|
|
106
|
+
el = self._element(element)
|
|
107
|
+
target = int(item) if str(item).isdigit() else str(item)
|
|
108
|
+
try:
|
|
109
|
+
el.select(target)
|
|
110
|
+
return
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
# Fallback: open the drop down with the mouse and click the item.
|
|
114
|
+
el.click_input()
|
|
115
|
+
criteria = (
|
|
116
|
+
{'control_type': 'ListItem'} if el.backend.name == 'uia' else {}
|
|
117
|
+
)
|
|
118
|
+
if isinstance(target, str):
|
|
119
|
+
criteria['title'] = target
|
|
120
|
+
else:
|
|
121
|
+
criteria['found_index'] = target
|
|
122
|
+
items = self.ctx._poll(
|
|
123
|
+
lambda: self.ctx.ui.find_elements(
|
|
124
|
+
criteria, el.backend.name, parent=el
|
|
125
|
+
)
|
|
126
|
+
or self.ctx.ui.find_elements(
|
|
127
|
+
criteria, el.backend.name, use_window=False
|
|
128
|
+
),
|
|
129
|
+
self.ctx.timeout,
|
|
130
|
+
)
|
|
131
|
+
if not items:
|
|
132
|
+
raise AssertionError(f"Item '{item}' not found.")
|
|
133
|
+
items[0].click_input()
|
|
134
|
+
|
|
135
|
+
@keyword
|
|
136
|
+
@busy_action
|
|
137
|
+
def set_checkbox_state(self, element, checked: bool = True):
|
|
138
|
+
"""Checks or unchecks a checkbox.
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
| Set Checkbox State | name:Remember me | ${TRUE} |
|
|
142
|
+
| Set Checkbox State | automation_id:chkAgree | ${FALSE} |
|
|
143
|
+
"""
|
|
144
|
+
el = self._element(element)
|
|
145
|
+
wanted = str(checked).lower() in ('true', '1', 'yes', 'on')
|
|
146
|
+
state = (
|
|
147
|
+
el.get_toggle_state()
|
|
148
|
+
if hasattr(el, 'get_toggle_state')
|
|
149
|
+
else el.get_check_state()
|
|
150
|
+
)
|
|
151
|
+
if bool(state) != wanted:
|
|
152
|
+
if hasattr(el, 'toggle'):
|
|
153
|
+
el.toggle()
|
|
154
|
+
else:
|
|
155
|
+
el.click()
|
|
156
|
+
|
|
157
|
+
@keyword
|
|
158
|
+
@busy_action
|
|
159
|
+
def expand_tree_item(self, element, path: str = ''):
|
|
160
|
+
"""Expands a tree item. If ``path`` (e.g. ``Root\\Child``) is given,
|
|
161
|
+
``element`` is the tree and the item at ``path`` is expanded.
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
| Expand Tree Item | name:Documents |
|
|
165
|
+
| Expand Tree Item | automation_id:treeFolders | Root\\\\Child |
|
|
166
|
+
"""
|
|
167
|
+
el = self._element(element)
|
|
168
|
+
if path:
|
|
169
|
+
el = el.get_item(path)
|
|
170
|
+
el.expand()
|
|
171
|
+
|
|
172
|
+
@keyword
|
|
173
|
+
@busy_action
|
|
174
|
+
def invoke_element(self, element):
|
|
175
|
+
"""Invokes (activates) an element through UI Automation without
|
|
176
|
+
moving the mouse.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
| Invoke Element | automation_id:btnSave |
|
|
180
|
+
"""
|
|
181
|
+
el = self._element(element)
|
|
182
|
+
if hasattr(el, 'invoke'):
|
|
183
|
+
el.invoke()
|
|
184
|
+
else:
|
|
185
|
+
el.click()
|
|
186
|
+
|
|
187
|
+
@keyword
|
|
188
|
+
def element_should_be_enabled(self, element):
|
|
189
|
+
"""Fails if the UI element is disabled.
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
| Element Should Be Enabled | automation_id:btnSave |
|
|
193
|
+
"""
|
|
194
|
+
if not self._element(element).is_enabled():
|
|
195
|
+
raise AssertionError(
|
|
196
|
+
f"Element '{parse_locator(element).describe()}' is disabled."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@keyword
|
|
200
|
+
def element_should_be_disabled(self, element):
|
|
201
|
+
"""Fails if the UI element is enabled.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
| Element Should Be Disabled | automation_id:btnSave |
|
|
205
|
+
"""
|
|
206
|
+
if self._element(element).is_enabled():
|
|
207
|
+
raise AssertionError(
|
|
208
|
+
f"Element '{parse_locator(element).describe()}' is enabled."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@keyword
|
|
212
|
+
def element_should_be_visible(self, element):
|
|
213
|
+
"""Fails if the UI element is not visible.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
| Element Should Be Visible | name:Status |
|
|
217
|
+
"""
|
|
218
|
+
if not self._element(element).is_visible():
|
|
219
|
+
raise AssertionError(
|
|
220
|
+
f"Element '{parse_locator(element).describe()}' is not visible."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@keyword
|
|
224
|
+
def get_window_tree(self, window='', depth: int = 4) -> str:
|
|
225
|
+
"""Returns (and logs) the control tree of ``window`` or the attached
|
|
226
|
+
window. Helpful to discover locators.
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
| Attach To Window | name:Sample App |
|
|
230
|
+
| ${tree}= | Get Window Tree |
|
|
231
|
+
| ${tree}= | Get Window Tree | name:Untitled - Notepad | 6 |
|
|
232
|
+
"""
|
|
233
|
+
from robot.api import logger
|
|
234
|
+
|
|
235
|
+
el = (
|
|
236
|
+
self._element(window)
|
|
237
|
+
if window not in (None, '')
|
|
238
|
+
else self.ctx.ui.window
|
|
239
|
+
)
|
|
240
|
+
if el is None:
|
|
241
|
+
raise AssertionError(
|
|
242
|
+
"No window given or attached. Use 'Attach To Window'."
|
|
243
|
+
)
|
|
244
|
+
lines: list[str] = []
|
|
245
|
+
|
|
246
|
+
def walk(node, level):
|
|
247
|
+
info = node.element_info
|
|
248
|
+
lines.append(
|
|
249
|
+
f"{' ' * level}{info.control_type or info.class_name} "
|
|
250
|
+
f"name='{info.name}' automation_id='{getattr(info, 'automation_id', '')}' "
|
|
251
|
+
f"class_name='{info.class_name}' rect={self.ctx.ui.region_of(node).to_list()}"
|
|
252
|
+
)
|
|
253
|
+
if level < int(depth):
|
|
254
|
+
for child in node.children():
|
|
255
|
+
walk(child, level + 1)
|
|
256
|
+
|
|
257
|
+
walk(el, 0)
|
|
258
|
+
text = '\n'.join(lines)
|
|
259
|
+
logger.info(text)
|
|
260
|
+
return text
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from robot.api import logger
|
|
2
|
+
from robot.api.deco import keyword
|
|
3
|
+
|
|
4
|
+
from ..geometry import Match, sort_matches
|
|
5
|
+
from ..locator import parse_locator
|
|
6
|
+
from .base import KeywordGroup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FindKeywords(KeywordGroup):
|
|
10
|
+
"""Keywords returning elements so they can be iterated in loops."""
|
|
11
|
+
|
|
12
|
+
@keyword
|
|
13
|
+
def find_element(self, element, timeout='') -> Match:
|
|
14
|
+
"""Returns the best match of ``element``, waiting up to ``timeout``
|
|
15
|
+
(default: library timeout). Fails if not found.
|
|
16
|
+
|
|
17
|
+
The returned match can be passed to any keyword accepting an element
|
|
18
|
+
(`Click`, `Input Text`, `Get Element Text`, `Highlight`...). For UI
|
|
19
|
+
locators it keeps the UI Automation element, so element keywords do
|
|
20
|
+
not search again. Attributes: ``x``, ``y``, ``width``, ``height``,
|
|
21
|
+
``center``, ``target``, ``score``, ``source``.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
| ${save}= | Find Element | automation_id:btnSave |
|
|
25
|
+
| Click | ${save} |
|
|
26
|
+
| ${logo}= | Find Element | logo.png | 10s |
|
|
27
|
+
| Log | ${logo.x} ${logo.y} ${logo.score} |
|
|
28
|
+
"""
|
|
29
|
+
return self.ctx.find(element, timeout)
|
|
30
|
+
|
|
31
|
+
@keyword
|
|
32
|
+
def find_elements(
|
|
33
|
+
self, element, timeout=0, sort_by: str = 'row', min_count: int = 0
|
|
34
|
+
) -> list:
|
|
35
|
+
"""Returns all matches of ``element`` as a list, or an empty list
|
|
36
|
+
when nothing is found.
|
|
37
|
+
|
|
38
|
+
- ``timeout``: wait up to this time for at least one match (default ``0``: no wait).
|
|
39
|
+
- ``sort_by``: ``row`` (top to bottom, then left to right, default),
|
|
40
|
+
``column`` (left to right, then top to bottom), ``score`` (best first) or ``none``.
|
|
41
|
+
- ``min_count``: fail if fewer matches are found.
|
|
42
|
+
|
|
43
|
+
Each item is a match usable with any element keyword. Image matches
|
|
44
|
+
are screen positions: if the screen scrolls or changes while looping,
|
|
45
|
+
their coordinates become stale. UI matches keep the UI Automation element.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
| @{items}= | Find Elements | control_type:ListItem |
|
|
49
|
+
| FOR | ${item} | IN | @{items} |
|
|
50
|
+
| | ${text}= | Get Element Text | ${item} |
|
|
51
|
+
| | IF | $text == "Item 3" | Click | ${item} |
|
|
52
|
+
| END |
|
|
53
|
+
| @{boxes}= | Find Elements | checkbox.png | sort_by=column | min_count=2 |
|
|
54
|
+
| @{none}= | Find Elements | name:Not There |
|
|
55
|
+
| Should Be Empty | ${none} |
|
|
56
|
+
"""
|
|
57
|
+
secs = self.ctx.to_seconds(timeout, 0)
|
|
58
|
+
matches = sort_matches(
|
|
59
|
+
self.ctx.find_all(element, secs, wait_cursor=True), sort_by
|
|
60
|
+
)
|
|
61
|
+
_check_min(matches, min_count, element)
|
|
62
|
+
logger.info(
|
|
63
|
+
f"Found {len(matches)} element(s) for '{parse_locator(element).describe()}'."
|
|
64
|
+
)
|
|
65
|
+
return matches
|
|
66
|
+
|
|
67
|
+
@keyword
|
|
68
|
+
def get_element_count(self, element) -> int:
|
|
69
|
+
"""Returns how many times ``element`` is currently found (image or UI).
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
| ${count}= | Get Element Count | control_type:Button |
|
|
73
|
+
| ${icons}= | Get Element Count | icon.png |
|
|
74
|
+
"""
|
|
75
|
+
return len(self.ctx.find_all(element))
|
|
76
|
+
|
|
77
|
+
@keyword
|
|
78
|
+
def find_child_elements(
|
|
79
|
+
self,
|
|
80
|
+
element,
|
|
81
|
+
child='',
|
|
82
|
+
depth=None,
|
|
83
|
+
sort_by: str = 'none',
|
|
84
|
+
min_count: int = 0,
|
|
85
|
+
) -> list:
|
|
86
|
+
"""Returns the descendants of UI ``element`` matching the UI locator
|
|
87
|
+
``child`` (all descendants when empty), or an empty list.
|
|
88
|
+
|
|
89
|
+
``depth=1`` returns direct children only; empty means any depth.
|
|
90
|
+
``element`` can be a UI locator or a match returned by `Find Element`.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
| @{items}= | Find Child Elements | automation_id:lstItems | control_type:ListItem |
|
|
94
|
+
| @{direct}= | Find Child Elements | ${WINDOW} | depth=1 |
|
|
95
|
+
| @{buttons}= | Find Child Elements | ${WINDOW} | control_type:Button | sort_by=column |
|
|
96
|
+
| FOR | ${b} | IN | @{buttons} |
|
|
97
|
+
| | Log | ${b.source} |
|
|
98
|
+
| END |
|
|
99
|
+
"""
|
|
100
|
+
parent = self._ui_element(element)
|
|
101
|
+
criteria = {}
|
|
102
|
+
backend = None
|
|
103
|
+
if child not in (None, ''):
|
|
104
|
+
loc = parse_locator(child)
|
|
105
|
+
if loc.kind != 'ui':
|
|
106
|
+
raise ValueError(f"'{child}' is not a UI locator.")
|
|
107
|
+
criteria, backend = loc.criteria, loc.backend
|
|
108
|
+
depth = None if depth in (None, '') else int(depth)
|
|
109
|
+
wrappers = self.ctx.ui.find_children(parent, criteria, depth, backend)
|
|
110
|
+
source = (
|
|
111
|
+
parse_locator(child).describe()
|
|
112
|
+
if child not in (None, '')
|
|
113
|
+
else 'child'
|
|
114
|
+
)
|
|
115
|
+
matches = sort_matches(
|
|
116
|
+
self.ctx.ui.to_matches(wrappers, source), sort_by
|
|
117
|
+
)
|
|
118
|
+
_check_min(matches, min_count, child or element)
|
|
119
|
+
return matches
|
|
120
|
+
|
|
121
|
+
def _ui_element(self, element):
|
|
122
|
+
if isinstance(element, Match) and element.element is not None:
|
|
123
|
+
return element.element
|
|
124
|
+
locator = parse_locator(element)
|
|
125
|
+
if locator.kind != 'ui':
|
|
126
|
+
raise ValueError(f"'{locator.describe()}' is not a UI locator.")
|
|
127
|
+
return self.ctx.find(locator).element
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _check_min(matches, min_count, element):
|
|
131
|
+
min_count = int(min_count or 0)
|
|
132
|
+
if len(matches) < min_count:
|
|
133
|
+
raise AssertionError(
|
|
134
|
+
f'Expected at least {min_count} element(s) for '
|
|
135
|
+
f"'{parse_locator(element).describe()}', found {len(matches)}."
|
|
136
|
+
)
|