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.
@@ -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
+ )