robotframework-appiumwindows 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.
- AppiumLibrary/__init__.py +106 -0
- AppiumLibrary/appium_path.py +10 -0
- AppiumLibrary/keywords/__init__.py +21 -0
- AppiumLibrary/keywords/_applicationmanagement.py +515 -0
- AppiumLibrary/keywords/_element.py +1282 -0
- AppiumLibrary/keywords/_logging.py +63 -0
- AppiumLibrary/keywords/_powershell.py +553 -0
- AppiumLibrary/keywords/_runonfailure.py +74 -0
- AppiumLibrary/keywords/_screenrecord.py +138 -0
- AppiumLibrary/keywords/_screenshot.py +109 -0
- AppiumLibrary/keywords/_waiting.py +163 -0
- AppiumLibrary/keywords/_windows.py +215 -0
- AppiumLibrary/keywords/keywordgroup.py +70 -0
- AppiumLibrary/locators/__init__.py +7 -0
- AppiumLibrary/locators/elementfinder.py +264 -0
- AppiumLibrary/utils/__init__.py +50 -0
- AppiumLibrary/utils/applicationcache.py +48 -0
- AppiumLibrary/version.py +2 -0
- robotframework_appiumwindows-0.1.0.dist-info/METADATA +148 -0
- robotframework_appiumwindows-0.1.0.dist-info/RECORD +23 -0
- robotframework_appiumwindows-0.1.0.dist-info/WHEEL +5 -0
- robotframework_appiumwindows-0.1.0.dist-info/licenses/LICENSE +21 -0
- robotframework_appiumwindows-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import inspect
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
# Internal/private marker attribute names
|
|
6
|
+
_RUN_ON_FAILURE_ATTR = "__rof_wrapped__"
|
|
7
|
+
_IGNORE_RUN_FAILURE_ATTR = "__rof_ignore__"
|
|
8
|
+
_ORIGINAL_METHOD_ATTR = "__original__"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _run_on_failure_decorator(method):
|
|
12
|
+
"""Decorator to wrap keyword methods with _run_on_failure support."""
|
|
13
|
+
if getattr(method, _RUN_ON_FAILURE_ATTR, False):
|
|
14
|
+
# Already decorated → skip re-wrapping
|
|
15
|
+
return method
|
|
16
|
+
|
|
17
|
+
@functools.wraps(method)
|
|
18
|
+
def wrapper(*args, **kwargs):
|
|
19
|
+
try:
|
|
20
|
+
return method(*args, **kwargs)
|
|
21
|
+
except Exception:
|
|
22
|
+
self = args[0] if args else None
|
|
23
|
+
if self and hasattr(self, "_run_on_failure"):
|
|
24
|
+
self._run_on_failure()
|
|
25
|
+
raise
|
|
26
|
+
|
|
27
|
+
setattr(wrapper, _RUN_ON_FAILURE_ATTR, True) # mark as decorated
|
|
28
|
+
setattr(wrapper, _ORIGINAL_METHOD_ATTR, method) # keep reference to original
|
|
29
|
+
return wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ignore_on_fail(method):
|
|
33
|
+
"""Decorator to mark methods that should never be wrapped by run_on_failure."""
|
|
34
|
+
setattr(method, _IGNORE_RUN_FAILURE_ATTR, True)
|
|
35
|
+
return method
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class KeywordGroupMetaClass(type):
|
|
39
|
+
def __new__(cls, clsname, bases, attrs):
|
|
40
|
+
for name, method in list(attrs.items()):
|
|
41
|
+
if (
|
|
42
|
+
not name.startswith('_')
|
|
43
|
+
and inspect.isfunction(method)
|
|
44
|
+
and not getattr(method, _IGNORE_RUN_FAILURE_ATTR, False)
|
|
45
|
+
and not getattr(method, _RUN_ON_FAILURE_ATTR, False)
|
|
46
|
+
):
|
|
47
|
+
attrs[name] = _run_on_failure_decorator(method)
|
|
48
|
+
return super().__new__(cls, clsname, bases, attrs)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class KeywordGroup(metaclass=KeywordGroupMetaClass):
|
|
52
|
+
|
|
53
|
+
def _invoke_original(self, method, *args, **kwargs):
|
|
54
|
+
"""
|
|
55
|
+
Call the original (undecorated) implementation of a method.
|
|
56
|
+
|
|
57
|
+
Accepts either:
|
|
58
|
+
- method name (str), e.g. self._invoke_original("click", el)
|
|
59
|
+
- bound method itself, e.g. self._invoke_original(self.click, el)
|
|
60
|
+
|
|
61
|
+
Falls back to the current method if undecorated.
|
|
62
|
+
Returns None if method not found at all.
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(method, str):
|
|
65
|
+
method = getattr(self, method, None)
|
|
66
|
+
if method is None:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
original = getattr(method, _ORIGINAL_METHOD_ATTR, method)
|
|
70
|
+
return original(self, *args, **kwargs)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from AppiumLibrary import utils
|
|
4
|
+
from appium.webdriver.common.appiumby import AppiumBy
|
|
5
|
+
from robot.api import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ElementFinder(object):
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._strategies = {
|
|
12
|
+
'identifier': self._find_by_identifier,
|
|
13
|
+
'id': self._find_by_id,
|
|
14
|
+
'name': self._find_by_name,
|
|
15
|
+
'xpath': self._find_by_xpath,
|
|
16
|
+
'class': self._find_by_class_name,
|
|
17
|
+
'accessibility_id': self._find_element_by_accessibility_id,
|
|
18
|
+
'android': self._find_by_android,
|
|
19
|
+
'viewtag': self._find_by_android_viewtag,
|
|
20
|
+
'data_matcher': self._find_by_android_data_matcher,
|
|
21
|
+
'view_matcher': self._find_by_android_view_matcher,
|
|
22
|
+
'ios': self._find_by_ios,
|
|
23
|
+
'css': self._find_by_css_selector,
|
|
24
|
+
'jquery': self._find_by_sizzle_selector,
|
|
25
|
+
'predicate': self._find_by_ios_predicate,
|
|
26
|
+
'chain': self._find_by_chain,
|
|
27
|
+
'default': self._find_by_default
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def find(self, application, locator, tag=None):
|
|
31
|
+
assert application is not None
|
|
32
|
+
assert locator is not None and len(locator) > 0
|
|
33
|
+
|
|
34
|
+
(prefix, criteria) = self._parse_locator(locator)
|
|
35
|
+
prefix = 'default' if prefix is None else prefix
|
|
36
|
+
strategy = self._strategies.get(prefix)
|
|
37
|
+
if strategy is None:
|
|
38
|
+
raise ValueError("Element locator with prefix '" + prefix + "' is not supported")
|
|
39
|
+
(tag, constraints) = self._get_tag_and_constraints(tag)
|
|
40
|
+
return strategy(application, criteria, tag, constraints)
|
|
41
|
+
|
|
42
|
+
# Strategy routines, private
|
|
43
|
+
|
|
44
|
+
def _find_by_identifier(self, application, criteria, tag, constraints):
|
|
45
|
+
elements = self._normalize_result(application.find_elements(by=AppiumBy.ID, value=criteria))
|
|
46
|
+
elements.extend(self._normalize_result(application.find_elements(by=AppiumBy.NAME, value=criteria)))
|
|
47
|
+
return self._filter_elements(elements, tag, constraints)
|
|
48
|
+
|
|
49
|
+
def _find_by_id(self, application, criteria, tag, constraints):
|
|
50
|
+
# print(f"criteria is {criteria}")
|
|
51
|
+
return self._filter_elements(
|
|
52
|
+
application.find_elements(by=AppiumBy.ID, value=criteria),
|
|
53
|
+
tag, constraints)
|
|
54
|
+
|
|
55
|
+
def _find_by_name(self, application, criteria, tag, constraints):
|
|
56
|
+
return self._filter_elements(
|
|
57
|
+
application.find_elements(by=AppiumBy.NAME, value=criteria),
|
|
58
|
+
tag, constraints)
|
|
59
|
+
|
|
60
|
+
def _find_by_xpath(self, application, criteria, tag, constraints):
|
|
61
|
+
return self._filter_elements(
|
|
62
|
+
application.find_elements(by=AppiumBy.XPATH, value=criteria),
|
|
63
|
+
tag, constraints)
|
|
64
|
+
|
|
65
|
+
def _find_by_dom(self, application, criteria, tag, constraints):
|
|
66
|
+
result = application.execute_script("return %s;" % criteria)
|
|
67
|
+
if result is None:
|
|
68
|
+
return []
|
|
69
|
+
if not isinstance(result, list):
|
|
70
|
+
result = [result]
|
|
71
|
+
return self._filter_elements(result, tag, constraints)
|
|
72
|
+
|
|
73
|
+
def _find_by_sizzle_selector(self, application, criteria, tag, constraints):
|
|
74
|
+
js = "return jQuery('%s').get();" % criteria.replace("'", "\\'")
|
|
75
|
+
return self._filter_elements(
|
|
76
|
+
application.execute_script(js),
|
|
77
|
+
tag, constraints)
|
|
78
|
+
|
|
79
|
+
def _find_by_link_text(self, application, criteria, tag, constraints):
|
|
80
|
+
return self._filter_elements(
|
|
81
|
+
application.find_elements(by=AppiumBy.LINK_TEXT, value=criteria),
|
|
82
|
+
tag, constraints)
|
|
83
|
+
|
|
84
|
+
def _find_by_css_selector(self, application, criteria, tag, constraints):
|
|
85
|
+
return self._filter_elements(
|
|
86
|
+
application.find_elements(by=AppiumBy.CSS_SELECTOR, value=criteria),
|
|
87
|
+
tag, constraints)
|
|
88
|
+
|
|
89
|
+
def _find_by_tag_name(self, application, criteria, tag, constraints):
|
|
90
|
+
return self._filter_elements(
|
|
91
|
+
application.find_elements(by=AppiumBy.TAG_NAME, value=criteria),
|
|
92
|
+
tag, constraints)
|
|
93
|
+
|
|
94
|
+
def _find_by_class_name(self, application, criteria, tag, constraints):
|
|
95
|
+
return self._filter_elements(
|
|
96
|
+
application.find_elements(by=AppiumBy.CLASS_NAME, value=criteria),
|
|
97
|
+
tag, constraints)
|
|
98
|
+
|
|
99
|
+
def _find_element_by_accessibility_id(self, application, criteria, tag, constraints):
|
|
100
|
+
return self._filter_elements(
|
|
101
|
+
application.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value=criteria),
|
|
102
|
+
tag, constraints)
|
|
103
|
+
|
|
104
|
+
def _find_by_android(self, application, criteria, tag, constraints):
|
|
105
|
+
"""Find element matches by UI Automator."""
|
|
106
|
+
return self._filter_elements(
|
|
107
|
+
application.find_elements(by=AppiumBy.ANDROID_UIAUTOMATOR, value=criteria),
|
|
108
|
+
tag, constraints)
|
|
109
|
+
|
|
110
|
+
def _find_by_android_viewtag(self, application, criteria, tag, constraints):
|
|
111
|
+
"""Find element matches by its view tag
|
|
112
|
+
Espresso only
|
|
113
|
+
"""
|
|
114
|
+
return self._filter_elements(
|
|
115
|
+
application.find_elements(by=AppiumBy.ANDROID_VIEWTAG, value=criteria),
|
|
116
|
+
tag, constraints)
|
|
117
|
+
|
|
118
|
+
def _find_by_android_data_matcher(self, application, criteria, tag, constraints):
|
|
119
|
+
"""Find element matches by Android Data Matcher
|
|
120
|
+
Espresso only
|
|
121
|
+
"""
|
|
122
|
+
return self._filter_elements(
|
|
123
|
+
application.find_elements(by=AppiumBy.ANDROID_DATA_MATCHER, value=criteria),
|
|
124
|
+
tag, constraints)
|
|
125
|
+
|
|
126
|
+
def _find_by_android_view_matcher(self, application, criteria, tag, constraints):
|
|
127
|
+
"""Find element matches by Android View Matcher
|
|
128
|
+
Espresso only
|
|
129
|
+
"""
|
|
130
|
+
return self._filter_elements(
|
|
131
|
+
application.find_elements(by=AppiumBy.ANDROID_VIEW_MATCHER, value=criteria),
|
|
132
|
+
tag, constraints)
|
|
133
|
+
|
|
134
|
+
def _find_by_ios(self, application, criteria, tag, constraints):
|
|
135
|
+
"""Find element matches by UI Automation."""
|
|
136
|
+
return self._filter_elements(
|
|
137
|
+
application.find_elements(by=AppiumBy.IOS_UIAUTOMATION, value=criteria),
|
|
138
|
+
tag, constraints)
|
|
139
|
+
|
|
140
|
+
def _find_by_ios_predicate(self, application, criteria, tag, constraints):
|
|
141
|
+
"""Find element matches by iOSNsPredicateString."""
|
|
142
|
+
return self._filter_elements(
|
|
143
|
+
application.find_elements(by=AppiumBy.IOS_PREDICATE, value=criteria),
|
|
144
|
+
tag, constraints)
|
|
145
|
+
|
|
146
|
+
def _find_by_chain(self, application, criteria, tag, constraints):
|
|
147
|
+
"""Find element matches by iOSChainString."""
|
|
148
|
+
return self._filter_elements(
|
|
149
|
+
application.find_elements(by=AppiumBy.IOS_CLASS_CHAIN, value=criteria),
|
|
150
|
+
tag, constraints)
|
|
151
|
+
|
|
152
|
+
def _find_by_default(self, application, criteria, tag, constraints):
|
|
153
|
+
if self._is_xpath(criteria):
|
|
154
|
+
return self._find_by_xpath(application, criteria, tag, constraints)
|
|
155
|
+
# Used `id` instead of _find_by_key_attrs since iOS and Android internal `id` alternatives are
|
|
156
|
+
# different and inside appium python client. Need to expose these and improve in order to make
|
|
157
|
+
# _find_by_key_attrs useful.
|
|
158
|
+
return self._find_by_id(application, criteria, tag, constraints)
|
|
159
|
+
|
|
160
|
+
# TODO: Not in use after conversion from Selenium2Library need to make more use of multiple auto selector strategy
|
|
161
|
+
def _find_by_key_attrs(self, application, criteria, tag, constraints):
|
|
162
|
+
key_attrs = self._key_attrs.get(None)
|
|
163
|
+
if tag is not None:
|
|
164
|
+
key_attrs = self._key_attrs.get(tag, key_attrs)
|
|
165
|
+
|
|
166
|
+
xpath_criteria = utils.escape_xpath_value(criteria)
|
|
167
|
+
xpath_tag = tag if tag is not None else '*'
|
|
168
|
+
xpath_constraints = ["@%s='%s'" % (name, constraints[name]) for name in constraints]
|
|
169
|
+
xpath_searchers = ["%s=%s" % (attr, xpath_criteria) for attr in key_attrs]
|
|
170
|
+
xpath_searchers.extend(
|
|
171
|
+
self._get_attrs_with_url(key_attrs, criteria, application))
|
|
172
|
+
xpath = "//%s[%s(%s)]" % (
|
|
173
|
+
xpath_tag,
|
|
174
|
+
' and '.join(xpath_constraints) + ' and ' if len(xpath_constraints) > 0 else '',
|
|
175
|
+
' or '.join(xpath_searchers))
|
|
176
|
+
return self._normalize_result(application.find_elements(by=AppiumBy.XPATH, value=xpath))
|
|
177
|
+
|
|
178
|
+
# Private
|
|
179
|
+
_key_attrs = {
|
|
180
|
+
None: ['@id', '@name'],
|
|
181
|
+
'a': ['@id', '@name', '@href', 'normalize-space(descendant-or-self::text())'],
|
|
182
|
+
'img': ['@id', '@name', '@src', '@alt'],
|
|
183
|
+
'input': ['@id', '@name', '@value', '@src'],
|
|
184
|
+
'button': ['@id', '@name', '@value', 'normalize-space(descendant-or-self::text())']
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def _get_tag_and_constraints(self, tag):
|
|
188
|
+
if tag is None:
|
|
189
|
+
return None, {}
|
|
190
|
+
|
|
191
|
+
tag = tag.lower()
|
|
192
|
+
constraints = {}
|
|
193
|
+
if tag == 'link':
|
|
194
|
+
tag = 'a'
|
|
195
|
+
elif tag == 'image':
|
|
196
|
+
tag = 'img'
|
|
197
|
+
elif tag == 'list':
|
|
198
|
+
tag = 'select'
|
|
199
|
+
elif tag == 'radio button':
|
|
200
|
+
tag = 'input'
|
|
201
|
+
constraints['type'] = 'radio'
|
|
202
|
+
elif tag == 'checkbox':
|
|
203
|
+
tag = 'input'
|
|
204
|
+
constraints['type'] = 'checkbox'
|
|
205
|
+
elif tag == 'text field':
|
|
206
|
+
tag = 'input'
|
|
207
|
+
constraints['type'] = 'text'
|
|
208
|
+
elif tag == 'file upload':
|
|
209
|
+
tag = 'input'
|
|
210
|
+
constraints['type'] = 'file'
|
|
211
|
+
return tag, constraints
|
|
212
|
+
|
|
213
|
+
def _element_matches(self, element, tag, constraints):
|
|
214
|
+
if not element.tag_name.lower() == tag:
|
|
215
|
+
return False
|
|
216
|
+
for name in constraints:
|
|
217
|
+
if not element.get_attribute(name) == constraints[name]:
|
|
218
|
+
return False
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
def _filter_elements(self, elements, tag, constraints):
|
|
222
|
+
elements = self._normalize_result(elements)
|
|
223
|
+
if tag is None:
|
|
224
|
+
return elements
|
|
225
|
+
return filter(
|
|
226
|
+
lambda element: self._element_matches(element, tag, constraints),
|
|
227
|
+
elements)
|
|
228
|
+
|
|
229
|
+
def _get_attrs_with_url(self, key_attrs, criteria, browser):
|
|
230
|
+
attrs = []
|
|
231
|
+
url = None
|
|
232
|
+
xpath_url = None
|
|
233
|
+
for attr in ['@src', '@href']:
|
|
234
|
+
if attr in key_attrs:
|
|
235
|
+
if url is None or xpath_url is None:
|
|
236
|
+
url = self._get_base_url(browser) + "/" + criteria
|
|
237
|
+
xpath_url = utils.escape_xpath_value(url)
|
|
238
|
+
attrs.append("%s=%s" % (attr, xpath_url))
|
|
239
|
+
return attrs
|
|
240
|
+
|
|
241
|
+
def _get_base_url(self, browser):
|
|
242
|
+
url = browser.get_current_url()
|
|
243
|
+
if '/' in url:
|
|
244
|
+
url = '/'.join(url.split('/')[:-1])
|
|
245
|
+
return url
|
|
246
|
+
|
|
247
|
+
def _parse_locator(self, locator):
|
|
248
|
+
prefix = None
|
|
249
|
+
criteria = locator
|
|
250
|
+
if not self._is_xpath(locator):
|
|
251
|
+
locator_parts = locator.partition('=')
|
|
252
|
+
if len(locator_parts[1]) > 0:
|
|
253
|
+
prefix = locator_parts[0].strip().lower()
|
|
254
|
+
criteria = locator_parts[2].strip()
|
|
255
|
+
return (prefix, criteria)
|
|
256
|
+
|
|
257
|
+
def _is_xpath(self, locator):
|
|
258
|
+
return locator and locator.startswith('/')
|
|
259
|
+
|
|
260
|
+
def _normalize_result(self, elements):
|
|
261
|
+
if not isinstance(elements, list):
|
|
262
|
+
logger.debug("WebDriver find returned %s" % elements)
|
|
263
|
+
return []
|
|
264
|
+
return elements
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from robot.utils import abspath
|
|
5
|
+
|
|
6
|
+
from .applicationcache import ApplicationCache
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def escape_xpath_value(value):
|
|
10
|
+
value = str(value)
|
|
11
|
+
if '"' in value and '\'' in value:
|
|
12
|
+
parts_wo_apos = value.split('\'')
|
|
13
|
+
return "concat('%s')" % "', \"'\", '".join(parts_wo_apos)
|
|
14
|
+
if '\'' in value:
|
|
15
|
+
return "\"%s\"" % value
|
|
16
|
+
return "'%s'" % value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_file(file_path):
|
|
20
|
+
with open(_absnorm(file_path), encoding='UTF-8', errors='strict', newline="") as f:
|
|
21
|
+
file_content = f.read().replace("\r\n", "\n")
|
|
22
|
+
return file_content
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _absnorm(path):
|
|
26
|
+
return abspath(_normalize_path(path))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _normalize_path(path, case_normalize=False):
|
|
30
|
+
"""Normalizes the given path.
|
|
31
|
+
|
|
32
|
+
- Collapses redundant separators and up-level references.
|
|
33
|
+
- Converts ``/`` to ``\\`` on Windows.
|
|
34
|
+
- Replaces initial ``~`` or ``~user`` by that user's home directory.
|
|
35
|
+
- Converts ``pathlib.Path`` instances to ``str``.
|
|
36
|
+
On Windows result would use ``\\`` instead of ``/`` and home directory
|
|
37
|
+
would be different.
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(path, Path):
|
|
40
|
+
path = str(path)
|
|
41
|
+
else:
|
|
42
|
+
path = path.replace("/", os.sep)
|
|
43
|
+
path = os.path.normpath(os.path.expanduser(path))
|
|
44
|
+
# os.path.normcase doesn't normalize on OSX which also, by default,
|
|
45
|
+
# has case-insensitive file system. Our robot.utils.normpath would
|
|
46
|
+
# do that, but it's not certain would that, or other things that the
|
|
47
|
+
# utility do, desirable.
|
|
48
|
+
if case_normalize:
|
|
49
|
+
path = os.path.normcase(path)
|
|
50
|
+
return path or "."
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from robot.utils import ConnectionCache
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApplicationCache(ConnectionCache):
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
ConnectionCache.__init__(self, no_current_msg='No current application')
|
|
8
|
+
self._closed = set()
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def applications(self):
|
|
12
|
+
return self._connections
|
|
13
|
+
|
|
14
|
+
def get_open_browsers(self):
|
|
15
|
+
open_applications = []
|
|
16
|
+
for application in self._connections:
|
|
17
|
+
if application not in self._closed:
|
|
18
|
+
open_applications.append(application)
|
|
19
|
+
return open_applications
|
|
20
|
+
|
|
21
|
+
def close(self, ignore_fail=False, quit_app=True):
|
|
22
|
+
if self.current:
|
|
23
|
+
application = self.current
|
|
24
|
+
try:
|
|
25
|
+
if quit_app:
|
|
26
|
+
application.quit()
|
|
27
|
+
else:
|
|
28
|
+
application.close()
|
|
29
|
+
except Exception as err:
|
|
30
|
+
if not ignore_fail:
|
|
31
|
+
raise err
|
|
32
|
+
self.current = self._no_current
|
|
33
|
+
self.current_index = None
|
|
34
|
+
self._closed.add(application)
|
|
35
|
+
|
|
36
|
+
def close_all(self, ignore_fail=True, quit_app=True):
|
|
37
|
+
for application in self._connections:
|
|
38
|
+
if application not in self._closed:
|
|
39
|
+
try:
|
|
40
|
+
if quit_app:
|
|
41
|
+
application.quit()
|
|
42
|
+
else:
|
|
43
|
+
application.close()
|
|
44
|
+
except Exception as err:
|
|
45
|
+
if not ignore_fail:
|
|
46
|
+
raise err
|
|
47
|
+
self.empty_cache()
|
|
48
|
+
return self.current
|
AppiumLibrary/version.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-appiumwindows
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Robot Framework AppiumLibrary extension for Windows desktop automation using NovaWindows Driver instead of WinAppDriver.
|
|
5
|
+
Author-email: Huy Nguyen <nguyenvanhuy0612@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nguyenvanhuy0612/robotframework-appiumwindows
|
|
8
|
+
Project-URL: Repository, https://github.com/nguyenvanhuy0612/robotframework-appiumwindows
|
|
9
|
+
Project-URL: Issues, https://github.com/nguyenvanhuy0612/robotframework-appiumwindows/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Robot Framework
|
|
12
|
+
Classifier: Framework :: Robot Framework :: Library
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: robotframework>=7.3.2
|
|
27
|
+
Requires-Dist: Appium-Python-Client>=5.1.1
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Robot Framework AppiumLibrary Compatible with NovaWindows Driver
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
This library extends [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary) to provide compatibility with the **NovaWindows Driver** for Appium 2.x.
|
|
36
|
+
|
|
37
|
+
It allows you to automate Windows desktop applications using **Robot Framework** with minimal setup.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
> **Note**
|
|
42
|
+
>
|
|
43
|
+
> - NovaWindows Driver currently uses a **PowerShell session** as its back-end.
|
|
44
|
+
> - No Developer Mode required
|
|
45
|
+
> - No extra dependencies required
|
|
46
|
+
> - A future update is planned to move to a **.NET-based backend** for:
|
|
47
|
+
> - Improved reliability
|
|
48
|
+
> - Better error handling
|
|
49
|
+
> - More feature support beyond PowerShell limitations
|
|
50
|
+
>
|
|
51
|
+
> Reference: [AutomateThePlanet/appium-novawindows-driver](https://github.com/AutomateThePlanet/appium-novawindows-driver)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
### 1. On the **Test Runner (local machine)**
|
|
58
|
+
|
|
59
|
+
Install the Robot Framework library:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install robotframework-appiumwindows
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> This is the only requirement on the machine where you run Robot Framework tests.
|
|
66
|
+
> No need to install Node.js or Appium here.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 2. On the **Target Machine (remote machine under test)**
|
|
71
|
+
|
|
72
|
+
This is where the Appium server and NovaWindows driver must be installed.
|
|
73
|
+
|
|
74
|
+
1. Install **Node.js**
|
|
75
|
+
[Download Node.js](https://nodejs.org/en/download)
|
|
76
|
+
|
|
77
|
+
2. Install **Appium** globally:
|
|
78
|
+
```bash
|
|
79
|
+
npm install -g appium
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. Install **NovaWindows Driver**:
|
|
83
|
+
```bash
|
|
84
|
+
appium driver install --source=npm appium-novawindows-driver
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
4. Start the Appium server:
|
|
88
|
+
```bash
|
|
89
|
+
appium --relaxed-security
|
|
90
|
+
```
|
|
91
|
+
(use `--relaxed-security` if you plan to execute PowerShell commands)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Example Test
|
|
96
|
+
|
|
97
|
+
```robot
|
|
98
|
+
*** Settings ***
|
|
99
|
+
Library AppiumLibrary
|
|
100
|
+
|
|
101
|
+
Test Setup Open Root Session
|
|
102
|
+
Test Teardown Appium Close All Applications
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
*** Test Cases ***
|
|
106
|
+
Type To Notepad
|
|
107
|
+
[Documentation] Launch Notepad, type text, and close without saving
|
|
108
|
+
Appium Execute Powershell Command Start-Process "notepad"
|
|
109
|
+
Appium Input class=Notepad This is example{enter 3}Close without save
|
|
110
|
+
Appium Click //Window[@ClassName='Notepad']//Button[@Name='Close']
|
|
111
|
+
Appium Click name=Don't Save
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
*** Keywords ***
|
|
115
|
+
Open Root Session
|
|
116
|
+
${parameters}= Create Dictionary
|
|
117
|
+
... remote_url=http://<TARGET_MACHINE_IP>:4723
|
|
118
|
+
... platformName=Windows
|
|
119
|
+
... appium:app=Root
|
|
120
|
+
... appium:automationName=NovaWindows
|
|
121
|
+
... appium:newCommandTimeout=30
|
|
122
|
+
Open Application &{parameters}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
+--------------------------+ +----------------------------+
|
|
131
|
+
| Test Runner (Local PC) | | Target Machine (Windows) |
|
|
132
|
+
|--------------------------| |----------------------------|
|
|
133
|
+
| - Robot Framework | | - Node.js |
|
|
134
|
+
| - robotframework- | ---> | - Appium 2.x |
|
|
135
|
+
| appium-windows (pip) | | - NovaWindows Driver |
|
|
136
|
+
+--------------------------+ +----------------------------+
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## References
|
|
142
|
+
|
|
143
|
+
- [Appium](https://appium.io/)
|
|
144
|
+
- [Robot Framework](https://robotframework.org/)
|
|
145
|
+
- [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary)
|
|
146
|
+
- [NovaWindows Driver](https://github.com/AutomateThePlanet/appium-novawindows-driver)
|
|
147
|
+
|
|
148
|
+
---
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
AppiumLibrary/__init__.py,sha256=_8Mcsywy1P1ebzUl0Cn49RCLSHzG4zRrFQTNAL1r_2o,5901
|
|
2
|
+
AppiumLibrary/appium_path.py,sha256=0LZFLn07dsxOY0FIJ4REEfyRbNBxtgNzX-rEfovEh_4,260
|
|
3
|
+
AppiumLibrary/version.py,sha256=hrqXUu4slpU-usm7Rv3LJhMR6paooe0eSmHZXr8iYHY,47
|
|
4
|
+
AppiumLibrary/keywords/__init__.py,sha256=vRZLNwHfUSaBPBY3dmDUejCihFL0N2ODYkrubhBwcjA,767
|
|
5
|
+
AppiumLibrary/keywords/_applicationmanagement.py,sha256=qJYKpNjHEcNvXlWVfKIh-xCUkWJGyDTWigl2-nAJSMk,19786
|
|
6
|
+
AppiumLibrary/keywords/_element.py,sha256=MsY69h0ZiRc8Tl6K4KU56k8fgWHv4WeHwIGm_h68TTE,53655
|
|
7
|
+
AppiumLibrary/keywords/_logging.py,sha256=r9P56w85BFQIZw3pMQtFliu7uD4sqoCITDXDhB_LURM,1927
|
|
8
|
+
AppiumLibrary/keywords/_powershell.py,sha256=j_-LrLIOkx6sdKj-P-bUerfcdpHIC_fIG4VZT4G2Ovo,23611
|
|
9
|
+
AppiumLibrary/keywords/_runonfailure.py,sha256=t6kJrVJdCo9cHbuUUs1Qoy63Z7AKTrDBepOIxvsSNDQ,2921
|
|
10
|
+
AppiumLibrary/keywords/_screenrecord.py,sha256=e8EXHTwiujeQVMrDhmYq6QIQk3MwSuuFpu8SWHZ0r7Q,6927
|
|
11
|
+
AppiumLibrary/keywords/_screenshot.py,sha256=L_o1ncYsJ_ghJEVrNVlilXCxvkR9KyR7D-twZIAmEAQ,3998
|
|
12
|
+
AppiumLibrary/keywords/_waiting.py,sha256=vDp7duuVuRsmtpf4DYgyvP363KpG9Ldffr2TD3GqTcI,6446
|
|
13
|
+
AppiumLibrary/keywords/_windows.py,sha256=W7M5ba1IZ7YdAn3CPP58fq-CrBC_Ck-GaKXVh7dJjFE,9666
|
|
14
|
+
AppiumLibrary/keywords/keywordgroup.py,sha256=SvgJNazRMZoyH4pBmGY_05l1FjVDaoVfwX6I3YN_fU0,2449
|
|
15
|
+
AppiumLibrary/locators/__init__.py,sha256=-t6gBYeVN8LS1q_agwCNikDaH54v_JXPfRtE5slY9dg,109
|
|
16
|
+
AppiumLibrary/locators/elementfinder.py,sha256=EdKVUJIHZCxrQkAu-0xp3yhCwA5qd5y55SnP58-dBP0,11241
|
|
17
|
+
AppiumLibrary/utils/__init__.py,sha256=FlB2DCknRHFvtj4ZZbq3B0sSmZ9RTnvqucHNC9pReN4,1622
|
|
18
|
+
AppiumLibrary/utils/applicationcache.py,sha256=AeeZIzRLxqcOLw9lIAir2_CypGAEwglCzzvMep4IQrg,1571
|
|
19
|
+
robotframework_appiumwindows-0.1.0.dist-info/licenses/LICENSE,sha256=yVLQqHcOliLXZJC6M0K2u0WJL9yIW1u7d5bsM_qW1Es,1090
|
|
20
|
+
robotframework_appiumwindows-0.1.0.dist-info/METADATA,sha256=HXX0laXT4t3XFysxHQL8Pr9G_bD6cs930X2-5Aux_iE,4902
|
|
21
|
+
robotframework_appiumwindows-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
robotframework_appiumwindows-0.1.0.dist-info/top_level.txt,sha256=2o2iQDagXnzsewODgcttx95vTzvaYaZB7q6KJB8sf-I,14
|
|
23
|
+
robotframework_appiumwindows-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nguyen Van Huy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AppiumLibrary
|