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,138 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import robot
|
|
7
|
+
|
|
8
|
+
from .keywordgroup import KeywordGroup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ScreenrecordKeywords(KeywordGroup):
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._screenrecord_index = 0
|
|
15
|
+
self._recording = None
|
|
16
|
+
self._output_format = None
|
|
17
|
+
|
|
18
|
+
def start_screen_recording(self,
|
|
19
|
+
timeLimit='180s',
|
|
20
|
+
**options):
|
|
21
|
+
"""Starts an asynchronous Screen Recording for the current open application.
|
|
22
|
+
|
|
23
|
+
``timeLimit`` sets the actual time limit of the recorded video.
|
|
24
|
+
- The default value for both iOS and Android is 180 seconds (3 minutes).
|
|
25
|
+
- The maximum value for Android is 3 minutes.
|
|
26
|
+
- The maximum value for iOS is 10 minutes.
|
|
27
|
+
|
|
28
|
+
=== Optional Args ===
|
|
29
|
+
|
|
30
|
+
- ``bitRate`` (Android Only) The video bit rate for the video, in megabits per second.
|
|
31
|
+
4 Mbp/s(4000000) is by default for Android API level below 27. \
|
|
32
|
+
20 Mb/s(20000000) for API level 27 and above.
|
|
33
|
+
|
|
34
|
+
- ``videoSize`` (Android Only) The format is widthxheight. The default value is the \
|
|
35
|
+
device's native display resolution (if supported), 1280x720 if not. For best \
|
|
36
|
+
results, use a size supported by your device's Advanced Video Coding (AVC) \
|
|
37
|
+
encoder. For example, "1280x720"
|
|
38
|
+
|
|
39
|
+
- ``bugReport`` (Android Only) Set it to true in order to display additional \
|
|
40
|
+
information on the video overlay, such as a timestamp, that is helpful in \
|
|
41
|
+
videos captured to illustrate bugs. This option is only supported since \
|
|
42
|
+
API level 27 (Android O).
|
|
43
|
+
|
|
44
|
+
- ``videoQuality`` (iOS Only) The video encoding quality (low, medium, high, \
|
|
45
|
+
photo - defaults to medium).
|
|
46
|
+
|
|
47
|
+
- ``videoFps`` (iOS Only) The Frames Per Second rate of the recorded video. \
|
|
48
|
+
Change this value if the resulting video is too slow or too fast. Defaults to 10. \
|
|
49
|
+
This can decrease the resulting file size.
|
|
50
|
+
|
|
51
|
+
- ``videoScale`` (iOS Only) The scaling value to apply. Read \
|
|
52
|
+
https://trac.ffmpeg.org/wiki/Scaling for possible values. Example value of 720p \
|
|
53
|
+
scaling is '1280:720'. This can decrease/increase the resulting file size. \
|
|
54
|
+
No scale is applied by default.
|
|
55
|
+
|
|
56
|
+
`Start Screen Recording` is used hand in hand with `Stop Screen Recording`.
|
|
57
|
+
See `Stop Screen Recording` for more details.
|
|
58
|
+
Example:
|
|
59
|
+
| `Start Screen Recording` | | # starts a screen record session |
|
|
60
|
+
| .... keyword actions | | |
|
|
61
|
+
| `Stop Screen Recording` | filename=output | # saves the recorded session |
|
|
62
|
+
"""
|
|
63
|
+
timeLimit = robot.utils.timestr_to_secs(timeLimit)
|
|
64
|
+
options['timeLimit'] = timeLimit
|
|
65
|
+
self._output_format = self._set_output_format() \
|
|
66
|
+
if self._output_format is None else self._output_format
|
|
67
|
+
if self._recording is None:
|
|
68
|
+
self._recording = self._current_application().start_recording_screen(**options)
|
|
69
|
+
|
|
70
|
+
def stop_screen_recording(self, filename=None, **options):
|
|
71
|
+
"""Gathers the output from the previously started screen recording \
|
|
72
|
+
to a media file, then embeds it to the log.html(Android Only).
|
|
73
|
+
|
|
74
|
+
Requires an active or exhausted Screen Recording Session.
|
|
75
|
+
See `Start Screen Recording` for more details.
|
|
76
|
+
|
|
77
|
+
=== Optional Args ===
|
|
78
|
+
|
|
79
|
+
- ``remotePath`` The path to the remote location, where the resulting video should be \
|
|
80
|
+
uploaded. The following protocols are supported _http/https_, ftp. Null or empty \
|
|
81
|
+
string value (the default setting) means the content of resulting file should \
|
|
82
|
+
be encoded as Base64 and passed as the endpoint response value. An \
|
|
83
|
+
exception will be thrown if the generated media file is too big to fit \
|
|
84
|
+
into the available process memory.
|
|
85
|
+
|
|
86
|
+
- ``username`` The name of the user for the remote authentication.
|
|
87
|
+
|
|
88
|
+
- ``password`` The password for the remote authentication.
|
|
89
|
+
|
|
90
|
+
- ``method`` The http multipart upload method name. The _PUT_ one is used by default.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
| `Start Screen Recording` | | # starts a screen record session |
|
|
94
|
+
| .... keyword actions | | |
|
|
95
|
+
| `Stop Screen Recording` | filename=output | # saves the recorded session |
|
|
96
|
+
"""
|
|
97
|
+
if self._recording is not None:
|
|
98
|
+
self._recording = self._current_application().stop_recording_screen(**options)
|
|
99
|
+
return self._save_recording(filename, options)
|
|
100
|
+
else:
|
|
101
|
+
raise RuntimeError("There is no Active Screen Record Session.")
|
|
102
|
+
|
|
103
|
+
def _save_recording(self, filename, options):
|
|
104
|
+
path, link = self._get_screenrecord_paths(options, filename)
|
|
105
|
+
decoded = base64.b64decode(self._recording)
|
|
106
|
+
with open(path, 'wb') as screenrecording:
|
|
107
|
+
screenrecording.write(decoded)
|
|
108
|
+
# Embed the Screen Recording to the log file
|
|
109
|
+
# if the current platform is Android and no remotePath is set.
|
|
110
|
+
if self._is_android() and not self._is_remotepath_set(options):
|
|
111
|
+
self._html('</td></tr><tr><td colspan="3"><a href="{vid}">'
|
|
112
|
+
'<video width="800px" controls>'
|
|
113
|
+
'<source src="{vid}" type="video/mp4">'
|
|
114
|
+
'</video></a>'.format(vid=link)
|
|
115
|
+
)
|
|
116
|
+
# Empty Screen Record Variable
|
|
117
|
+
self._recording = None
|
|
118
|
+
return path
|
|
119
|
+
|
|
120
|
+
def _set_output_format(self):
|
|
121
|
+
return '.ffmpeg' if self._is_ios() else '.mp4'
|
|
122
|
+
|
|
123
|
+
def _get_screenrecord_paths(self, options, filename=None):
|
|
124
|
+
if filename is None:
|
|
125
|
+
self._screenrecord_index += 1
|
|
126
|
+
filename = 'appium-screenrecord-{index}{ext}'.format(index=self._screenrecord_index,
|
|
127
|
+
ext=self._output_format
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
filename = (filename.replace('/', os.sep)) + self._output_format
|
|
131
|
+
logdir = options['remotePath'] if self._is_remotepath_set(options) \
|
|
132
|
+
else self._get_log_dir()
|
|
133
|
+
path = os.path.join(logdir, filename)
|
|
134
|
+
link = robot.utils.get_link_path(path, logdir)
|
|
135
|
+
return path, link
|
|
136
|
+
|
|
137
|
+
def _is_remotepath_set(self, options):
|
|
138
|
+
return True if 'remotePath' in options else False
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from base64 import b64decode
|
|
5
|
+
|
|
6
|
+
import robot
|
|
7
|
+
|
|
8
|
+
from .keywordgroup import KeywordGroup, ignore_on_fail
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ScreenshotKeywords(KeywordGroup):
|
|
12
|
+
|
|
13
|
+
# Public
|
|
14
|
+
|
|
15
|
+
def appium_get_element_screenshot(self, locator, timeout=20, filename=None):
|
|
16
|
+
"""
|
|
17
|
+
Get a screenshot of element to base64
|
|
18
|
+
If provide filename, saves a screenshot to a PNG image file.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
-----------
|
|
22
|
+
locator: Element locator
|
|
23
|
+
timeout: timeout in second to find element
|
|
24
|
+
filename : str
|
|
25
|
+
- The full path you wish to save your screenshot to. This
|
|
26
|
+
- should end with a `.png` extension.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
--------
|
|
30
|
+
>>> driver.get_screenshot_as_file("/Screenshots/foo.png")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
element = self._invoke_original("appium_get_element", locator, timeout, False)
|
|
34
|
+
|
|
35
|
+
if not element:
|
|
36
|
+
self._info(f'Not found {locator}, return None')
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
base64data = element.screenshot_as_base64
|
|
40
|
+
|
|
41
|
+
if filename:
|
|
42
|
+
if not str(filename).lower().endswith(".png"):
|
|
43
|
+
self._info(
|
|
44
|
+
"name used for saved screenshot does not match file type. It should end with a `.png` extension")
|
|
45
|
+
|
|
46
|
+
png = b64decode(base64data.encode("ascii"))
|
|
47
|
+
try:
|
|
48
|
+
with open(filename, "wb") as f:
|
|
49
|
+
f.write(png)
|
|
50
|
+
except OSError:
|
|
51
|
+
self._info(f'Fail to write screenshot file {filename}, return False')
|
|
52
|
+
return False
|
|
53
|
+
finally:
|
|
54
|
+
del png
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return base64data
|
|
58
|
+
|
|
59
|
+
@ignore_on_fail
|
|
60
|
+
def appium_get_screenshot(self):
|
|
61
|
+
return self._invoke_original("appium_capture_page_screenshot", None, False)
|
|
62
|
+
|
|
63
|
+
def appium_capture_page_screenshot(self, filename=None, embed=True):
|
|
64
|
+
try:
|
|
65
|
+
return self._invoke_original("capture_page_screenshot", filename, embed)
|
|
66
|
+
except Exception as err:
|
|
67
|
+
self._info(err)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def capture_page_screenshot(self, filename=None, embed=True):
|
|
71
|
+
"""Takes a screenshot of the current page and embeds it into the log.
|
|
72
|
+
|
|
73
|
+
`filename` argument specifies the name of the file to write the
|
|
74
|
+
screenshot into. If no `filename` is given, the screenshot will be
|
|
75
|
+
embedded as Base64 image to the log.html. In this case no file is created in the filesystem.
|
|
76
|
+
|
|
77
|
+
`embed` is True: the screenshot will be embedded to the log.html
|
|
78
|
+
|
|
79
|
+
Warning: this behavior is new in 1.7. Previously if no filename was given
|
|
80
|
+
the screenshots where stored as separate files named `appium-screenshot-<counter>.png`
|
|
81
|
+
"""
|
|
82
|
+
if filename:
|
|
83
|
+
path, link = self._get_screenshot_paths(filename)
|
|
84
|
+
|
|
85
|
+
if hasattr(self._current_application(), 'get_screenshot_as_file'):
|
|
86
|
+
self._current_application().get_screenshot_as_file(path)
|
|
87
|
+
else:
|
|
88
|
+
self._current_application().save_screenshot(path)
|
|
89
|
+
|
|
90
|
+
# Image is shown on its own row and thus prev row is closed on purpose
|
|
91
|
+
if embed:
|
|
92
|
+
self._html('</td></tr><tr><td colspan="3"><a href="%s">'
|
|
93
|
+
'<img src="%s" width="800px"></a>' % (link, link))
|
|
94
|
+
return path
|
|
95
|
+
else:
|
|
96
|
+
base64_screenshot = self._current_application().get_screenshot_as_base64()
|
|
97
|
+
if embed:
|
|
98
|
+
self._html('</td></tr><tr><td colspan="3">'
|
|
99
|
+
'<img src="data:image/png;base64, %s" width="800px">' % base64_screenshot)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Private
|
|
103
|
+
|
|
104
|
+
def _get_screenshot_paths(self, filename):
|
|
105
|
+
filename = filename.replace('/', os.sep)
|
|
106
|
+
logdir = self._get_log_dir()
|
|
107
|
+
path = os.path.join(logdir, filename)
|
|
108
|
+
link = robot.utils.get_link_path(path, logdir)
|
|
109
|
+
return path, link
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import robot
|
|
4
|
+
|
|
5
|
+
from .keywordgroup import KeywordGroup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _WaitingKeywords(KeywordGroup):
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._sleep_between_wait = 0.2
|
|
12
|
+
|
|
13
|
+
def wait_until_element_is_visible(self, locator, timeout=None, error=None):
|
|
14
|
+
"""Waits until element specified with `locator` is visible.
|
|
15
|
+
|
|
16
|
+
Fails if `timeout` expires before the element is visible. See
|
|
17
|
+
`introduction` for more information about `timeout` and its
|
|
18
|
+
default value.
|
|
19
|
+
|
|
20
|
+
`error` can be used to override the default error message.
|
|
21
|
+
|
|
22
|
+
See also `Wait Until Page Contains`, `Wait Until Page Contains
|
|
23
|
+
Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
|
|
24
|
+
Succeeds`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def check_visibility():
|
|
28
|
+
visible = self._is_visible(locator)
|
|
29
|
+
if visible:
|
|
30
|
+
return
|
|
31
|
+
elif visible is None:
|
|
32
|
+
return error or "Element locator '%s' did not match any elements after %s" % (
|
|
33
|
+
locator, self._format_timeout(timeout))
|
|
34
|
+
else:
|
|
35
|
+
return error or "Element '%s' was not visible in %s" % (locator, self._format_timeout(timeout))
|
|
36
|
+
|
|
37
|
+
self._wait_until_no_error(timeout, check_visibility)
|
|
38
|
+
|
|
39
|
+
def wait_until_page_contains(self, text, timeout=None, error=None):
|
|
40
|
+
"""Waits until `text` appears on current page.
|
|
41
|
+
|
|
42
|
+
Fails if `timeout` expires before the text appears. See
|
|
43
|
+
`introduction` for more information about `timeout` and its
|
|
44
|
+
default value.
|
|
45
|
+
|
|
46
|
+
`error` can be used to override the default error message.
|
|
47
|
+
|
|
48
|
+
See also `Wait Until Page Does Not Contain`,
|
|
49
|
+
`Wait Until Page Contains Element`,
|
|
50
|
+
`Wait Until Page Does Not Contain Element` and
|
|
51
|
+
BuiltIn keyword `Wait Until Keyword Succeeds`.
|
|
52
|
+
"""
|
|
53
|
+
if not error:
|
|
54
|
+
error = "Text '%s' did not appear in <TIMEOUT>" % text
|
|
55
|
+
self._wait_until(timeout, error, self._is_text_present, text)
|
|
56
|
+
|
|
57
|
+
def wait_until_page_does_not_contain(self, text, timeout=None, error=None):
|
|
58
|
+
"""Waits until `text` disappears from current page.
|
|
59
|
+
|
|
60
|
+
Fails if `timeout` expires before the `text` disappears. See
|
|
61
|
+
`introduction` for more information about `timeout` and its
|
|
62
|
+
default value.
|
|
63
|
+
|
|
64
|
+
`error` can be used to override the default error message.
|
|
65
|
+
|
|
66
|
+
See also `Wait Until Page Contains`,
|
|
67
|
+
`Wait Until Page Contains Element`,
|
|
68
|
+
`Wait Until Page Does Not Contain Element` and
|
|
69
|
+
BuiltIn keyword `Wait Until Keyword Succeeds`.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def check_present():
|
|
73
|
+
present = self._is_text_present(text)
|
|
74
|
+
if not present:
|
|
75
|
+
return
|
|
76
|
+
else:
|
|
77
|
+
return error or "Text '%s' did not disappear in %s" % (text, self._format_timeout(timeout))
|
|
78
|
+
|
|
79
|
+
self._wait_until_no_error(timeout, check_present)
|
|
80
|
+
|
|
81
|
+
def wait_until_page_contains_element(self, locator, timeout=None, error=None):
|
|
82
|
+
"""Waits until element specified with `locator` appears on current page.
|
|
83
|
+
|
|
84
|
+
Fails if `timeout` expires before the element appears. See
|
|
85
|
+
`introduction` for more information about `timeout` and its
|
|
86
|
+
default value.
|
|
87
|
+
|
|
88
|
+
`error` can be used to override the default error message.
|
|
89
|
+
|
|
90
|
+
See also `Wait Until Page Contains`,
|
|
91
|
+
`Wait Until Page Does Not Contain`
|
|
92
|
+
`Wait Until Page Does Not Contain Element`
|
|
93
|
+
and BuiltIn keyword `Wait Until Keyword Succeeds`.
|
|
94
|
+
"""
|
|
95
|
+
if not error:
|
|
96
|
+
error = "Element '%s' did not appear in <TIMEOUT>" % locator
|
|
97
|
+
self._wait_until(timeout, error, self._is_element_present, locator)
|
|
98
|
+
|
|
99
|
+
def wait_until_page_does_not_contain_element(self, locator, timeout=None, error=None):
|
|
100
|
+
"""Waits until element specified with `locator` disappears from current page.
|
|
101
|
+
|
|
102
|
+
Fails if `timeout` expires before the element disappears. See
|
|
103
|
+
`introduction` for more information about `timeout` and its
|
|
104
|
+
default value.
|
|
105
|
+
|
|
106
|
+
`error` can be used to override the default error message.
|
|
107
|
+
|
|
108
|
+
See also `Wait Until Page Contains`,
|
|
109
|
+
`Wait Until Page Does Not Contain`,
|
|
110
|
+
`Wait Until Page Contains Element` and
|
|
111
|
+
BuiltIn keyword `Wait Until Keyword Succeeds`.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def check_present():
|
|
115
|
+
present = self._is_element_present(locator)
|
|
116
|
+
if not present:
|
|
117
|
+
return
|
|
118
|
+
else:
|
|
119
|
+
return error or "Element '%s' did not disappear in %s" % (locator, self._format_timeout(timeout))
|
|
120
|
+
|
|
121
|
+
self._wait_until_no_error(timeout, check_present)
|
|
122
|
+
|
|
123
|
+
def set_sleep_between_wait_loop(self, seconds=0.2):
|
|
124
|
+
"""Sets the sleep in seconds used by wait until loop.
|
|
125
|
+
|
|
126
|
+
If you use the remote appium server, the default value is not recommended because
|
|
127
|
+
it is another 200ms overhead to the network latency and will slow down your test
|
|
128
|
+
execution.
|
|
129
|
+
"""
|
|
130
|
+
old_sleep = self._sleep_between_wait
|
|
131
|
+
self._sleep_between_wait = robot.utils.timestr_to_secs(seconds)
|
|
132
|
+
return old_sleep
|
|
133
|
+
|
|
134
|
+
def get_sleep_between_wait_loop(self):
|
|
135
|
+
"""Gets the sleep between wait loop in seconds that is used by wait until keywords.
|
|
136
|
+
"""
|
|
137
|
+
return robot.utils.secs_to_timestr(self._sleep_between_wait)
|
|
138
|
+
|
|
139
|
+
# Private
|
|
140
|
+
|
|
141
|
+
def _wait_until(self, timeout, error, function, *args):
|
|
142
|
+
error = error.replace('<TIMEOUT>', self._format_timeout(timeout))
|
|
143
|
+
|
|
144
|
+
def wait_func():
|
|
145
|
+
return None if function(*args) else error
|
|
146
|
+
|
|
147
|
+
self._wait_until_no_error(timeout, wait_func)
|
|
148
|
+
|
|
149
|
+
def _wait_until_no_error(self, timeout, wait_func, *args):
|
|
150
|
+
timeout = robot.utils.timestr_to_secs(timeout) if timeout is not None else self._timeout_in_secs
|
|
151
|
+
maxtime = time.time() + timeout
|
|
152
|
+
while True:
|
|
153
|
+
timeout_error = wait_func(*args)
|
|
154
|
+
if not timeout_error:
|
|
155
|
+
return
|
|
156
|
+
if time.time() > maxtime:
|
|
157
|
+
self._invoke_original("log_source")
|
|
158
|
+
raise AssertionError(timeout_error)
|
|
159
|
+
time.sleep(self._sleep_between_wait)
|
|
160
|
+
|
|
161
|
+
def _format_timeout(self, timeout):
|
|
162
|
+
timeout = robot.utils.timestr_to_secs(timeout) if timeout is not None else self._timeout_in_secs
|
|
163
|
+
return robot.utils.secs_to_timestr(timeout)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from AppiumLibrary.keywords.keywordgroup import KeywordGroup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _WindowsKeywords(KeywordGroup):
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__()
|
|
10
|
+
|
|
11
|
+
# Public
|
|
12
|
+
def appium_hover(self, locator, start_locator=None, timeout=20, **kwargs):
|
|
13
|
+
self._info(f"Appium Hover '{locator}', timeout '{timeout}'")
|
|
14
|
+
self._appium_hover_api(start_locator=start_locator, end_locator=locator, timeout=timeout, **kwargs)
|
|
15
|
+
|
|
16
|
+
def appium_click_offset(self, locator, x_offset=0, y_offset=0, timeout=20, **kwargs):
|
|
17
|
+
self._info(
|
|
18
|
+
f"Appium Click Offset '{locator}', (x_offset,y_offset) '({x_offset},{y_offset})', timeout '{timeout}'")
|
|
19
|
+
self._appium_click_api(locator=locator, timeout=timeout, x_offset=x_offset, y_offset=y_offset, **kwargs)
|
|
20
|
+
|
|
21
|
+
def appium_right_click(self, locator, timeout=20, **kwargs):
|
|
22
|
+
self._info(f"Appium Right Click '{locator}', timeout '{timeout}'")
|
|
23
|
+
self._appium_click_api(locator=locator, timeout=timeout, button="right", **kwargs)
|
|
24
|
+
|
|
25
|
+
def appium_left_click(self, locator, timeout=20, **kwargs):
|
|
26
|
+
self._info(f"Appium Left Click '{locator}', timeout '{timeout}'")
|
|
27
|
+
self._appium_click_api(locator=locator, timeout=timeout, button="left", **kwargs)
|
|
28
|
+
|
|
29
|
+
def appium_double_click(self, locator, timeout=20, **kwargs):
|
|
30
|
+
self._info(f"Appium Double Click '{locator}', timeout '{timeout}'")
|
|
31
|
+
self._appium_click_api(locator=locator, timeout=timeout, times=2, **kwargs)
|
|
32
|
+
|
|
33
|
+
def appium_drag_and_drop(self, start_locator=None, end_locator=None, timeout=20, **kwargs):
|
|
34
|
+
self._info(f"Appium Drag And Drop '{start_locator} -> {end_locator}', timeout '{timeout}'")
|
|
35
|
+
self._appium_drag_and_drop_api(start_locator=start_locator, end_locator=end_locator, timeout=timeout, **kwargs)
|
|
36
|
+
|
|
37
|
+
def appium_drag_and_drop_by_offset(self, x_start, y_start, x_end, y_end):
|
|
38
|
+
x_start, y_start, x_end, y_end = (int(x) for x in [x_start, y_start, x_end, y_end])
|
|
39
|
+
self._info(f"Appium Drag And Drop By Offset ({x_start}, {y_start}) -> ({x_end}, {y_end})")
|
|
40
|
+
self._appium_drag_and_drop_api(start_locator=None, end_locator=None, timeout=1,
|
|
41
|
+
startX=x_start, startY=y_start,
|
|
42
|
+
endX=x_end, endY=y_end)
|
|
43
|
+
|
|
44
|
+
def appium_sendkeys(self, text=None, **kwargs):
|
|
45
|
+
self._info(f"Appium Sendkeys '{text}'")
|
|
46
|
+
self._appium_keys_api(text=text, **kwargs)
|
|
47
|
+
|
|
48
|
+
# Private
|
|
49
|
+
def _apply_modifier_keys(self, params: dict, modifier_keys):
|
|
50
|
+
"""Normalize modifier keys and update params in place."""
|
|
51
|
+
if modifier_keys:
|
|
52
|
+
if isinstance(modifier_keys, (list, tuple)):
|
|
53
|
+
params["modifierKeys"] = [str(k).lower() for k in modifier_keys]
|
|
54
|
+
else:
|
|
55
|
+
params["modifierKeys"] = str(modifier_keys).lower()
|
|
56
|
+
|
|
57
|
+
def _appium_click_api(self, locator, timeout, **kwargs):
|
|
58
|
+
"""
|
|
59
|
+
Perform a click action on a Windows element using Appium Windows Driver.
|
|
60
|
+
|
|
61
|
+
References:
|
|
62
|
+
https://github.com/appium/appium-windows-driver
|
|
63
|
+
https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-click
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
locator (str): Element locator.
|
|
67
|
+
timeout (int): Maximum time to retry locating and clicking the element (in seconds).
|
|
68
|
+
kwargs (dict): Additional click options.
|
|
69
|
+
|
|
70
|
+
Keyword Args:
|
|
71
|
+
button (str): Mouse button to click. One of:
|
|
72
|
+
- "left" (default)
|
|
73
|
+
- "middle"
|
|
74
|
+
- "right"
|
|
75
|
+
- "back"
|
|
76
|
+
- "forward"
|
|
77
|
+
modifierKeys (list|str): Keys to hold during the click. One or more of:
|
|
78
|
+
- "Shift"
|
|
79
|
+
- "Ctrl"
|
|
80
|
+
- "Alt"
|
|
81
|
+
- "Win"
|
|
82
|
+
modifier_keys (list|str): Same as `modifierKeys` (snake_case alias).
|
|
83
|
+
x_offset (int): X offset relative to the element's top-left corner. Default: 0.
|
|
84
|
+
y_offset (int): Y offset relative to the element's top-left corner. Default: 0.
|
|
85
|
+
is_center (bool): If True, click at the element's center. Default: False.
|
|
86
|
+
durationMs (int): Duration of the click in milliseconds. Default: 100.
|
|
87
|
+
times (int): Number of times to click. Default: 1.
|
|
88
|
+
interClickDelayMs (int): Delay between multiple clicks in milliseconds. Default: 100.
|
|
89
|
+
post_delay (float): Delay after click action (in seconds). Default: 0.5.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
Exception: If the element cannot be found or the click action fails within the timeout.
|
|
93
|
+
"""
|
|
94
|
+
x_offset = int(kwargs.get("x_offset", 0))
|
|
95
|
+
y_offset = int(kwargs.get("y_offset", 0))
|
|
96
|
+
is_center = bool(kwargs.get("is_center", False))
|
|
97
|
+
|
|
98
|
+
click_params = {
|
|
99
|
+
"button": str(kwargs.get("button", "left")),
|
|
100
|
+
"durationMs": int(kwargs.get("durationMs", 100)),
|
|
101
|
+
"times": int(kwargs.get("times", 1)),
|
|
102
|
+
"interClickDelayMs": int(kwargs.get("interClickDelayMs", 100)),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
self._apply_modifier_keys(click_params, kwargs.get("modifierKeys"))
|
|
106
|
+
|
|
107
|
+
def _action():
|
|
108
|
+
elements = self._element_find(locator, False, False)
|
|
109
|
+
if not elements:
|
|
110
|
+
raise Exception(f"Element not found: {locator}")
|
|
111
|
+
|
|
112
|
+
driver = self._current_application()
|
|
113
|
+
rect = driver.get_window_rect()
|
|
114
|
+
e_rect = elements[0].rect
|
|
115
|
+
|
|
116
|
+
x = rect['x'] + e_rect['x'] + x_offset
|
|
117
|
+
y = rect['y'] + e_rect['y'] + y_offset
|
|
118
|
+
if is_center:
|
|
119
|
+
x += e_rect['width'] // 2
|
|
120
|
+
y += e_rect['height'] // 2
|
|
121
|
+
|
|
122
|
+
click_params.update({"x": x, "y": y})
|
|
123
|
+
self._info(f"Click params {click_params}")
|
|
124
|
+
|
|
125
|
+
driver.execute_script("windows: click", click_params)
|
|
126
|
+
time.sleep(0.5)
|
|
127
|
+
|
|
128
|
+
self._retry(_action, timeout, f"Failed to perform click action on '{locator}'")
|
|
129
|
+
|
|
130
|
+
def _appium_hover_api(self, start_locator, end_locator, timeout, **kwargs):
|
|
131
|
+
"""
|
|
132
|
+
Perform a hover action using Platform-Specific Extensions.
|
|
133
|
+
"""
|
|
134
|
+
hover_params = {
|
|
135
|
+
"startX": int(kwargs.get("startX", 0)),
|
|
136
|
+
"startY": int(kwargs.get("startY", 0)),
|
|
137
|
+
"endX": int(kwargs.get("endX", 0)),
|
|
138
|
+
"endY": int(kwargs.get("endY", 0)),
|
|
139
|
+
"durationMs": int(kwargs.get("durationMs", 100)),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
self._apply_modifier_keys(hover_params, kwargs.get("modifierKeys"))
|
|
143
|
+
|
|
144
|
+
def _action():
|
|
145
|
+
if start_locator:
|
|
146
|
+
start_element = self._element_find(start_locator, True, False)
|
|
147
|
+
if start_element:
|
|
148
|
+
hover_params["startElementId"] = start_element.id
|
|
149
|
+
hover_params.pop("startX", None)
|
|
150
|
+
hover_params.pop("startY", None)
|
|
151
|
+
|
|
152
|
+
if end_locator:
|
|
153
|
+
end_element = self._element_find(end_locator, True, False)
|
|
154
|
+
if end_element:
|
|
155
|
+
hover_params["endElementId"] = end_element.id
|
|
156
|
+
hover_params.pop("endX", None)
|
|
157
|
+
hover_params.pop("endY", None)
|
|
158
|
+
|
|
159
|
+
self._current_application().execute_script("windows: hover", hover_params)
|
|
160
|
+
time.sleep(0.5)
|
|
161
|
+
|
|
162
|
+
self._retry(_action, timeout, "Failed to perform hover action")
|
|
163
|
+
|
|
164
|
+
def _appium_drag_and_drop_api(self, start_locator, end_locator, timeout, **kwargs):
|
|
165
|
+
"""
|
|
166
|
+
Perform a drag and drop action using Appium Windows Driver.
|
|
167
|
+
https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-clickanddrag
|
|
168
|
+
"""
|
|
169
|
+
drag_params = {
|
|
170
|
+
"startX": int(kwargs.get("startX", 0)),
|
|
171
|
+
"startY": int(kwargs.get("startY", 0)),
|
|
172
|
+
"endX": int(kwargs.get("endX", 0)),
|
|
173
|
+
"endY": int(kwargs.get("endY", 0)),
|
|
174
|
+
"durationMs": int(kwargs.get("durationMs", 5000)),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
self._apply_modifier_keys(drag_params, kwargs.get("modifierKeys"))
|
|
178
|
+
|
|
179
|
+
def _action():
|
|
180
|
+
if start_locator:
|
|
181
|
+
start_element = self._element_find(start_locator, True, False)
|
|
182
|
+
if start_element:
|
|
183
|
+
drag_params["startElementId"] = start_element.id
|
|
184
|
+
drag_params.pop("startX", None)
|
|
185
|
+
drag_params.pop("startY", None)
|
|
186
|
+
|
|
187
|
+
if end_locator:
|
|
188
|
+
end_element = self._element_find(end_locator, True, False)
|
|
189
|
+
if end_element:
|
|
190
|
+
drag_params["endElementId"] = end_element.id
|
|
191
|
+
drag_params.pop("endX", None)
|
|
192
|
+
drag_params.pop("endY", None)
|
|
193
|
+
|
|
194
|
+
self._current_application().execute_script("windows: clickAndDrag", drag_params)
|
|
195
|
+
time.sleep(0.5)
|
|
196
|
+
|
|
197
|
+
self._retry(_action, timeout, "Failed to perform drag and drop action")
|
|
198
|
+
|
|
199
|
+
def _appium_keys_api(self, text, **kwargs):
|
|
200
|
+
"""
|
|
201
|
+
Perform a key input action using Appium Windows Driver.
|
|
202
|
+
https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-keys
|
|
203
|
+
|
|
204
|
+
@param text:
|
|
205
|
+
@param kwargs:
|
|
206
|
+
@return:
|
|
207
|
+
"""
|
|
208
|
+
actions = kwargs.get("actions", "")
|
|
209
|
+
# pause = int(kwargs.get("pause", 0))
|
|
210
|
+
# virtual_key_code = int(kwargs.get("virtualKeyCode", 0))
|
|
211
|
+
# down = bool(kwargs.get("down", False))
|
|
212
|
+
if not actions:
|
|
213
|
+
actions = [{"text": text}]
|
|
214
|
+
self._current_application().execute_script("windows: keys", {"actions": actions})
|
|
215
|
+
time.sleep(0.5)
|