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