robotframework-appiumwindows 0.1.0__py3-none-any.whl → 0.1.3__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.
- AppiumLibrary/__init__.py +106 -106
- AppiumLibrary/appium_path.py +10 -9
- AppiumLibrary/keywords/__init__.py +21 -21
- AppiumLibrary/keywords/_applicationmanagement.py +595 -515
- AppiumLibrary/keywords/_element.py +1336 -1282
- AppiumLibrary/keywords/_logging.py +63 -63
- AppiumLibrary/keywords/_powershell.py +578 -553
- AppiumLibrary/keywords/_runonfailure.py +74 -74
- AppiumLibrary/keywords/_screenrecord.py +138 -138
- AppiumLibrary/keywords/_screenshot.py +105 -109
- AppiumLibrary/keywords/_waiting.py +163 -163
- AppiumLibrary/keywords/_windows.py +271 -215
- AppiumLibrary/keywords/keywordgroup.py +71 -70
- AppiumLibrary/locators/__init__.py +7 -7
- AppiumLibrary/locators/elementfinder.py +264 -264
- AppiumLibrary/utils/__init__.py +50 -50
- AppiumLibrary/utils/applicationcache.py +48 -48
- AppiumLibrary/version.py +2 -2
- robotframework_appiumwindows-0.1.3.dist-info/METADATA +216 -0
- robotframework_appiumwindows-0.1.3.dist-info/RECORD +23 -0
- {robotframework_appiumwindows-0.1.0.dist-info → robotframework_appiumwindows-0.1.3.dist-info}/WHEEL +1 -1
- {robotframework_appiumwindows-0.1.0.dist-info → robotframework_appiumwindows-0.1.3.dist-info}/licenses/LICENSE +20 -20
- robotframework_appiumwindows-0.1.0.dist-info/METADATA +0 -148
- robotframework_appiumwindows-0.1.0.dist-info/RECORD +0 -23
- {robotframework_appiumwindows-0.1.0.dist-info → robotframework_appiumwindows-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -1,215 +1,271 @@
|
|
|
1
|
-
import time
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.
|
|
20
|
-
|
|
21
|
-
def
|
|
22
|
-
self._info(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
self.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
self.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self._info(f"Appium Drag And Drop
|
|
40
|
-
self._appium_drag_and_drop_api(start_locator=
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
1
|
+
import time
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
import ntpath
|
|
6
|
+
import posixpath
|
|
7
|
+
|
|
8
|
+
from AppiumLibrary.keywords.keywordgroup import KeywordGroup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _WindowsKeywords(KeywordGroup):
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
# Public
|
|
17
|
+
def appium_hover(self, locator, start_locator=None, timeout=20, **kwargs):
|
|
18
|
+
self._info(f"Appium Hover '{locator}', timeout '{timeout}'")
|
|
19
|
+
self._appium_hover_api(start_locator=start_locator, end_locator=locator, timeout=timeout, **kwargs)
|
|
20
|
+
|
|
21
|
+
def appium_click_offset(self, locator, x_offset=0, y_offset=0, timeout=20, **kwargs):
|
|
22
|
+
self._info(
|
|
23
|
+
f"Appium Click Offset '{locator}', (x_offset,y_offset) '({x_offset},{y_offset})', timeout '{timeout}'")
|
|
24
|
+
self._appium_click_api(locator=locator, timeout=timeout, x_offset=x_offset, y_offset=y_offset, **kwargs)
|
|
25
|
+
|
|
26
|
+
def appium_right_click(self, locator, timeout=20, **kwargs):
|
|
27
|
+
self._info(f"Appium Right Click '{locator}', timeout '{timeout}'")
|
|
28
|
+
self._appium_click_api(locator=locator, timeout=timeout, button="right", **kwargs)
|
|
29
|
+
|
|
30
|
+
def appium_left_click(self, locator, timeout=20, **kwargs):
|
|
31
|
+
self._info(f"Appium Left Click '{locator}', timeout '{timeout}'")
|
|
32
|
+
self._appium_click_api(locator=locator, timeout=timeout, button="left", **kwargs)
|
|
33
|
+
|
|
34
|
+
def appium_double_click(self, locator, timeout=20, **kwargs):
|
|
35
|
+
self._info(f"Appium Double Click '{locator}', timeout '{timeout}'")
|
|
36
|
+
self._appium_click_api(locator=locator, timeout=timeout, times=2, **kwargs)
|
|
37
|
+
|
|
38
|
+
def appium_drag_and_drop(self, start_locator=None, end_locator=None, timeout=20, **kwargs):
|
|
39
|
+
self._info(f"Appium Drag And Drop '{start_locator} -> {end_locator}', timeout '{timeout}'")
|
|
40
|
+
self._appium_drag_and_drop_api(start_locator=start_locator, end_locator=end_locator, timeout=timeout, **kwargs)
|
|
41
|
+
|
|
42
|
+
def appium_drag_and_drop_by_offset(self, x_start, y_start, x_end, y_end):
|
|
43
|
+
x_start, y_start, x_end, y_end = (int(x) for x in [x_start, y_start, x_end, y_end])
|
|
44
|
+
self._info(f"Appium Drag And Drop By Offset ({x_start}, {y_start}) -> ({x_end}, {y_end})")
|
|
45
|
+
self._appium_drag_and_drop_api(start_locator=None, end_locator=None, timeout=1,
|
|
46
|
+
startX=x_start, startY=y_start,
|
|
47
|
+
endX=x_end, endY=y_end)
|
|
48
|
+
|
|
49
|
+
def appium_sendkeys(self, text=None, **kwargs):
|
|
50
|
+
self._info(f"Appium Sendkeys '{text}'")
|
|
51
|
+
self._appium_keys_api(text=text, **kwargs)
|
|
52
|
+
|
|
53
|
+
def appium_normalize_path(self, path, sep="\\", case_normalize=False, escape_backtick=True):
|
|
54
|
+
"""Normalizes the given path.
|
|
55
|
+
- Collapses redundant separators and up-level references.
|
|
56
|
+
- Set sep to ``/`` to Converts ``\\`` to ``/``
|
|
57
|
+
- Replaces initial ``~`` or ``~user`` by that user's home directory.
|
|
58
|
+
- If ``case_normalize`` is given a true value (see `Boolean arguments`)
|
|
59
|
+
on Windows, converts the path to all lowercase.
|
|
60
|
+
- Converts ``pathlib.Path`` instances to ``str``.
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
| ${path1} = | Appium Normalize Path | abc/ |
|
|
64
|
+
| ${path2} = | Appium Normalize Path | abc/../def |
|
|
65
|
+
| ${path3} = | Appium Normalize Path | abc/./def//ghi |
|
|
66
|
+
| ${path4} = | Appium Normalize Path | ~robot/stuff |
|
|
67
|
+
=>
|
|
68
|
+
- ${path1} = ``abc``
|
|
69
|
+
- ${path2} = ``def``
|
|
70
|
+
- ${path3} = ``abc\\def\\ghi``
|
|
71
|
+
- ${path4} = ``\\home\\robot\\stuff``
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
# Determine strict library to use based on target separator
|
|
75
|
+
if sep == "\\":
|
|
76
|
+
path_module = ntpath
|
|
77
|
+
else:
|
|
78
|
+
path_module = posixpath
|
|
79
|
+
|
|
80
|
+
if isinstance(path, pathlib.Path):
|
|
81
|
+
path = str(path)
|
|
82
|
+
|
|
83
|
+
path = path or "."
|
|
84
|
+
path = os.path.expanduser(path)
|
|
85
|
+
|
|
86
|
+
# If targeting Posix, ensure backslashes are converted to forward slashes
|
|
87
|
+
# BEFORE normalization, because posixpath treats backslash as a filename character.
|
|
88
|
+
if path_module is posixpath:
|
|
89
|
+
path = path.replace("\\", "/")
|
|
90
|
+
|
|
91
|
+
path = path_module.normpath(path)
|
|
92
|
+
|
|
93
|
+
if case_normalize:
|
|
94
|
+
path = path_module.normcase(path)
|
|
95
|
+
|
|
96
|
+
if escape_backtick:
|
|
97
|
+
path = re.sub(r"(?<!`)`(?!`)", "``", path)
|
|
98
|
+
|
|
99
|
+
# Force final separator just in case, though normpath usually handles it.
|
|
100
|
+
# ntpath produces '\', posixpath produces '/'
|
|
101
|
+
# The original code did a final cleaning, we can preserve rstrip.
|
|
102
|
+
return path.rstrip()
|
|
103
|
+
|
|
104
|
+
# Private
|
|
105
|
+
def _apply_modifier_keys(self, params: dict, modifier_keys):
|
|
106
|
+
"""Normalize modifier keys and update params in place."""
|
|
107
|
+
if modifier_keys:
|
|
108
|
+
if isinstance(modifier_keys, (list, tuple)):
|
|
109
|
+
params["modifierKeys"] = [str(k).lower() for k in modifier_keys]
|
|
110
|
+
else:
|
|
111
|
+
params["modifierKeys"] = str(modifier_keys).lower()
|
|
112
|
+
|
|
113
|
+
def _appium_click_api(self, locator, timeout, **kwargs):
|
|
114
|
+
"""
|
|
115
|
+
Perform a click action on a Windows element using Appium Windows Driver.
|
|
116
|
+
|
|
117
|
+
References:
|
|
118
|
+
https://github.com/appium/appium-windows-driver
|
|
119
|
+
https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-click
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
locator (str): Element locator.
|
|
123
|
+
timeout (int): Maximum time to retry locating and clicking the element (in seconds).
|
|
124
|
+
kwargs (dict): Additional click options.
|
|
125
|
+
|
|
126
|
+
Keyword Args:
|
|
127
|
+
button (str): Mouse button to click. One of:
|
|
128
|
+
- "left" (default)
|
|
129
|
+
- "middle"
|
|
130
|
+
- "right"
|
|
131
|
+
- "back"
|
|
132
|
+
- "forward"
|
|
133
|
+
modifierKeys (list|str): Keys to hold during the click. One or more of:
|
|
134
|
+
- "Shift"
|
|
135
|
+
- "Ctrl"
|
|
136
|
+
- "Alt"
|
|
137
|
+
- "Win"
|
|
138
|
+
modifier_keys (list|str): Same as `modifierKeys` (snake_case alias).
|
|
139
|
+
x_offset (int): X offset relative to the element's top-left corner. Default: 0.
|
|
140
|
+
y_offset (int): Y offset relative to the element's top-left corner. Default: 0.
|
|
141
|
+
is_center (bool): If True, click at the element's center. Default: False.
|
|
142
|
+
durationMs (int): Duration of the click in milliseconds. Default: 100.
|
|
143
|
+
times (int): Number of times to click. Default: 1.
|
|
144
|
+
interClickDelayMs (int): Delay between multiple clicks in milliseconds. Default: 100.
|
|
145
|
+
post_delay (float): Delay after click action (in seconds). Default: 0.5.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
Exception: If the element cannot be found or the click action fails within the timeout.
|
|
149
|
+
"""
|
|
150
|
+
x_offset = int(kwargs.get("x_offset", 0))
|
|
151
|
+
y_offset = int(kwargs.get("y_offset", 0))
|
|
152
|
+
is_center = bool(kwargs.get("is_center", False))
|
|
153
|
+
|
|
154
|
+
click_params = {
|
|
155
|
+
"button": str(kwargs.get("button", "left")),
|
|
156
|
+
"durationMs": int(kwargs.get("durationMs", 100)),
|
|
157
|
+
"times": int(kwargs.get("times", 1)),
|
|
158
|
+
"interClickDelayMs": int(kwargs.get("interClickDelayMs", 100)),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
self._apply_modifier_keys(click_params, kwargs.get("modifierKeys"))
|
|
162
|
+
|
|
163
|
+
def _action():
|
|
164
|
+
elements = self._element_find(locator, False, False)
|
|
165
|
+
if not elements:
|
|
166
|
+
raise Exception(f"Element not found: {locator}")
|
|
167
|
+
|
|
168
|
+
driver = self._current_application()
|
|
169
|
+
rect = driver.get_window_rect()
|
|
170
|
+
e_rect = elements[0].rect
|
|
171
|
+
|
|
172
|
+
x = rect['x'] + e_rect['x'] + x_offset
|
|
173
|
+
y = rect['y'] + e_rect['y'] + y_offset
|
|
174
|
+
if is_center:
|
|
175
|
+
x += e_rect['width'] // 2
|
|
176
|
+
y += e_rect['height'] // 2
|
|
177
|
+
|
|
178
|
+
click_params.update({"x": x, "y": y})
|
|
179
|
+
self._info(f"Click params {click_params}")
|
|
180
|
+
|
|
181
|
+
driver.execute_script("windows: click", click_params)
|
|
182
|
+
time.sleep(0.5)
|
|
183
|
+
|
|
184
|
+
self._retry(_action, timeout, f"Failed to perform click action on '{locator}'")
|
|
185
|
+
|
|
186
|
+
def _appium_hover_api(self, start_locator, end_locator, timeout, **kwargs):
|
|
187
|
+
"""
|
|
188
|
+
Perform a hover action using Platform-Specific Extensions.
|
|
189
|
+
"""
|
|
190
|
+
hover_params = {
|
|
191
|
+
"startX": int(kwargs.get("startX", 0)),
|
|
192
|
+
"startY": int(kwargs.get("startY", 0)),
|
|
193
|
+
"endX": int(kwargs.get("endX", 0)),
|
|
194
|
+
"endY": int(kwargs.get("endY", 0)),
|
|
195
|
+
"durationMs": int(kwargs.get("durationMs", 100)),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
self._apply_modifier_keys(hover_params, kwargs.get("modifierKeys"))
|
|
199
|
+
|
|
200
|
+
def _action():
|
|
201
|
+
if start_locator:
|
|
202
|
+
start_element = self._element_find(start_locator, True, False)
|
|
203
|
+
if start_element:
|
|
204
|
+
hover_params["startElementId"] = start_element.id
|
|
205
|
+
hover_params.pop("startX", None)
|
|
206
|
+
hover_params.pop("startY", None)
|
|
207
|
+
|
|
208
|
+
if end_locator:
|
|
209
|
+
end_element = self._element_find(end_locator, True, False)
|
|
210
|
+
if end_element:
|
|
211
|
+
hover_params["endElementId"] = end_element.id
|
|
212
|
+
hover_params.pop("endX", None)
|
|
213
|
+
hover_params.pop("endY", None)
|
|
214
|
+
|
|
215
|
+
self._current_application().execute_script("windows: hover", hover_params)
|
|
216
|
+
time.sleep(0.5)
|
|
217
|
+
|
|
218
|
+
self._retry(_action, timeout, "Failed to perform hover action")
|
|
219
|
+
|
|
220
|
+
def _appium_drag_and_drop_api(self, start_locator, end_locator, timeout, **kwargs):
|
|
221
|
+
"""
|
|
222
|
+
Perform a drag and drop action using Appium Windows Driver.
|
|
223
|
+
https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-clickanddrag
|
|
224
|
+
"""
|
|
225
|
+
drag_params = {
|
|
226
|
+
"startX": int(kwargs.get("startX", 0)),
|
|
227
|
+
"startY": int(kwargs.get("startY", 0)),
|
|
228
|
+
"endX": int(kwargs.get("endX", 0)),
|
|
229
|
+
"endY": int(kwargs.get("endY", 0)),
|
|
230
|
+
"durationMs": int(kwargs.get("durationMs", 5000)),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self._apply_modifier_keys(drag_params, kwargs.get("modifierKeys"))
|
|
234
|
+
|
|
235
|
+
def _action():
|
|
236
|
+
if start_locator:
|
|
237
|
+
start_element = self._element_find(start_locator, True, False)
|
|
238
|
+
if start_element:
|
|
239
|
+
drag_params["startElementId"] = start_element.id
|
|
240
|
+
drag_params.pop("startX", None)
|
|
241
|
+
drag_params.pop("startY", None)
|
|
242
|
+
|
|
243
|
+
if end_locator:
|
|
244
|
+
end_element = self._element_find(end_locator, True, False)
|
|
245
|
+
if end_element:
|
|
246
|
+
drag_params["endElementId"] = end_element.id
|
|
247
|
+
drag_params.pop("endX", None)
|
|
248
|
+
drag_params.pop("endY", None)
|
|
249
|
+
|
|
250
|
+
self._current_application().execute_script("windows: clickAndDrag", drag_params)
|
|
251
|
+
time.sleep(0.5)
|
|
252
|
+
|
|
253
|
+
self._retry(_action, timeout, "Failed to perform drag and drop action")
|
|
254
|
+
|
|
255
|
+
def _appium_keys_api(self, text, **kwargs):
|
|
256
|
+
"""
|
|
257
|
+
Perform a key input action using Appium Windows Driver.
|
|
258
|
+
https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-keys
|
|
259
|
+
|
|
260
|
+
@param text:
|
|
261
|
+
@param kwargs:
|
|
262
|
+
@return:
|
|
263
|
+
"""
|
|
264
|
+
actions = kwargs.get("actions", "")
|
|
265
|
+
# pause = int(kwargs.get("pause", 0))
|
|
266
|
+
# virtual_key_code = int(kwargs.get("virtualKeyCode", 0))
|
|
267
|
+
# down = bool(kwargs.get("down", False))
|
|
268
|
+
if not actions:
|
|
269
|
+
actions = [{"text": text}]
|
|
270
|
+
self._current_application().execute_script("windows: keys", {"actions": actions})
|
|
271
|
+
time.sleep(0.5)
|
|
@@ -1,70 +1,71 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
import inspect
|
|
3
|
-
import functools
|
|
4
|
-
|
|
5
|
-
# Internal/private marker attribute
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
raise
|
|
26
|
-
|
|
27
|
-
setattr(wrapper,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
and
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if method
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import inspect
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
# Internal/private marker attribute name
|
|
6
|
+
_RUN_ON_FAILURE_MARKER = "__rof_processed__"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _run_on_failure_decorator(method):
|
|
10
|
+
"""Decorator to wrap keyword methods with _run_on_failure support."""
|
|
11
|
+
if getattr(method, _RUN_ON_FAILURE_MARKER, False):
|
|
12
|
+
# Already decorated or ignored → skip re-wrapping
|
|
13
|
+
return method
|
|
14
|
+
|
|
15
|
+
@functools.wraps(method)
|
|
16
|
+
def wrapper(*args, **kwargs):
|
|
17
|
+
try:
|
|
18
|
+
return method(*args, **kwargs)
|
|
19
|
+
except Exception as err:
|
|
20
|
+
self = args[0] if args else None
|
|
21
|
+
if self and hasattr(self, "_run_on_failure"):
|
|
22
|
+
if not getattr(err, "_run_on_failure_executed", False):
|
|
23
|
+
self._run_on_failure()
|
|
24
|
+
err._run_on_failure_executed = True
|
|
25
|
+
raise
|
|
26
|
+
|
|
27
|
+
setattr(wrapper, _RUN_ON_FAILURE_MARKER, True) # mark as decorated
|
|
28
|
+
return wrapper
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ignore_on_fail(method):
|
|
32
|
+
"""Decorator to mark methods that should never be wrapped by run_on_failure."""
|
|
33
|
+
setattr(method, _RUN_ON_FAILURE_MARKER, True)
|
|
34
|
+
return method
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class KeywordGroupMetaClass(type):
|
|
38
|
+
def __new__(cls, clsname, bases, attrs):
|
|
39
|
+
for name, method in list(attrs.items()):
|
|
40
|
+
if (
|
|
41
|
+
not name.startswith('_')
|
|
42
|
+
and inspect.isfunction(method)
|
|
43
|
+
and not getattr(method, _RUN_ON_FAILURE_MARKER, False)
|
|
44
|
+
):
|
|
45
|
+
attrs[name] = _run_on_failure_decorator(method)
|
|
46
|
+
return super().__new__(cls, clsname, bases, attrs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class KeywordGroup(metaclass=KeywordGroupMetaClass):
|
|
50
|
+
def _invoke_original(self, method, *args, **kwargs):
|
|
51
|
+
"""
|
|
52
|
+
Call the original (undecorated) implementation of a method.
|
|
53
|
+
|
|
54
|
+
Accepts either:
|
|
55
|
+
- method name (str), e.g. self._invoke_original("click", el)
|
|
56
|
+
- bound method itself, e.g. self._invoke_original(self.click, el)
|
|
57
|
+
|
|
58
|
+
Falls back to the current method if undecorated.
|
|
59
|
+
Returns None if method not found at all.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(method, str):
|
|
62
|
+
method = getattr(self, method, None)
|
|
63
|
+
if method is None:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
if hasattr(method, "__wrapped__"):
|
|
67
|
+
# It's a decorated method (function), so we must pass self
|
|
68
|
+
return method.__wrapped__(self, *args, **kwargs)
|
|
69
|
+
|
|
70
|
+
# It's an undecorated bound method, so self is already bound
|
|
71
|
+
return method(*args, **kwargs)
|