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,63 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+
5
+ from robot.api import logger
6
+ from robot.libraries.BuiltIn import BuiltIn
7
+ from robot.libraries.BuiltIn import RobotNotRunningError
8
+
9
+ from .keywordgroup import KeywordGroup
10
+
11
+
12
+ class _LoggingKeywords(KeywordGroup):
13
+ LOG_LEVEL_DEBUG = ['DEBUG']
14
+ LOG_LEVEL_INFO = ['DEBUG', 'INFO']
15
+ LOG_LEVEL_WARN = ['DEBUG', 'INFO', 'WARN']
16
+
17
+ @property
18
+ def _log_level(self):
19
+ try:
20
+ level = BuiltIn().get_variable_value("${APPIUM_LOG_LEVEL}", default='DEBUG')
21
+ except RobotNotRunningError:
22
+ level = 'DEBUG'
23
+ return level
24
+
25
+ def _debug(self, message):
26
+ if self._log_level in self.LOG_LEVEL_DEBUG:
27
+ logger.debug(message)
28
+
29
+ def _info(self, message):
30
+ if self._log_level in self.LOG_LEVEL_INFO:
31
+ logger.info(message)
32
+
33
+ def _warn(self, message):
34
+ if self._log_level in self.LOG_LEVEL_WARN:
35
+ logger.warn(message)
36
+
37
+ def _html(self, message):
38
+ logger.info(message, True, False)
39
+
40
+ def _get_log_dir(self):
41
+ variables = BuiltIn().get_variables()
42
+ logfile = variables['${LOG FILE}']
43
+ if logfile != 'NONE':
44
+ return os.path.dirname(logfile)
45
+ return variables['${OUTPUTDIR}']
46
+
47
+ def _log(self, message, level='INFO'):
48
+ level = level.upper()
49
+ if (level == 'INFO'):
50
+ self._info(message)
51
+ elif (level == 'DEBUG'):
52
+ self._debug(message)
53
+ elif (level == 'WARN'):
54
+ self._warn(message)
55
+ elif (level == 'HTML'):
56
+ self._html(message)
57
+
58
+ def _log_list(self, items, what='item'):
59
+ msg = ['Altogether %d %s%s.' % (len(items), what, ['s', ''][len(items) == 1])]
60
+ for index, item in enumerate(items):
61
+ msg.append('%d: %s' % (index + 1, item))
62
+ self._info('\n'.join(msg))
63
+ return items
@@ -0,0 +1,553 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import base64
4
+ import math
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from appium.webdriver.mobilecommand import MobileCommand as Command
9
+ from robot.libraries.BuiltIn import BuiltIn
10
+ from selenium.common import InvalidArgumentException
11
+
12
+ from AppiumLibrary import utils
13
+ from AppiumLibrary.locators import ElementFinder
14
+ from .keywordgroup import KeywordGroup
15
+
16
+ SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "scripts"))
17
+
18
+
19
+ class _PowershellKeywords(KeywordGroup):
20
+ def __init__(self):
21
+ self._element_finder = ElementFinder()
22
+ self._bi = BuiltIn()
23
+
24
+ # Public, element lookups
25
+ # Powershell command, need appium server allow shell, eg: appium --relaxed-security
26
+
27
+ def appium_ps_click(
28
+ self,
29
+ locator=None,
30
+ x=0,
31
+ y=0,
32
+ button='left',
33
+ **kwargs
34
+ ):
35
+ """
36
+ Click using PowerShell-level mouse simulation.
37
+
38
+ Supports:
39
+ - Absolute coordinates or element-based location via locator
40
+ - Optional offsets (absolute or 'center')
41
+ - Custom mouse button
42
+
43
+ kwargs:
44
+ - offset: sets both x/y offset globally (overrides individual)
45
+ - x_offset / y_offset
46
+
47
+ @param locator:
48
+ @param x:
49
+ @param y:
50
+ @param button:
51
+ 'left' - single left click
52
+ 'right' - single right click
53
+ 'middle' - single middle click
54
+ 'double' - double left click
55
+ 'triple' - triple left click
56
+ 'right-double' - double right click
57
+
58
+ """
59
+ self._info(f"Appium Ps Click")
60
+
61
+ if locator:
62
+ rect = self.get_element_rect(locator)
63
+ x, y = self._parse_location(rect, kwargs, 'x_offset', 'y_offset')
64
+ root = self._current_application().get_window_rect()
65
+ self._info(f"[DEBUG] App window rect: {root}")
66
+ x, y = x + root['x'], y + root['y']
67
+
68
+ self._info(f"[CLICK] {button.upper()} Button: ({x},{y}))")
69
+
70
+ ps_command = self._generate_click_command(x, y, button)
71
+ self._info(f"ps_command: \n{ps_command}")
72
+
73
+ self.appium_execute_powershell_command(ps_command)
74
+
75
+ def appium_ps_sendkeys(self, text: str):
76
+ """
77
+ SendKeys can found at: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys?view=windowsdesktop-10.0
78
+
79
+ To specify that any combination of SHIFT, CTRL, and ALT should be held down while several other keys are pressed,
80
+ enclose the code for those keys in parentheses. For example, to specify to hold down SHIFT while E and C are pressed,
81
+ use "+(EC)". To specify to hold down SHIFT while E is pressed, followed by C without SHIFT, use "+EC".
82
+
83
+ To specify repeating keys, use the form {key number}. You must put a space between key and number.
84
+ For example, {LEFT 42} means press the LEFT ARROW key 42 times; {h 10} means press H 10 times.
85
+
86
+ @param text: text to sendkeys
87
+ eg1: text = 123qwe{TAB}iop{ENTER}+a~ABC~
88
+ eg2: text = "{+}" ({%}) ({^})
89
+ eg3: text = This is test{LEFT}{BACKSPACE}x
90
+ eg4: text = 123qwe{BACKSPACE 3}{TAB}{ENTER}
91
+ @return:
92
+ """
93
+ self._info(f"Appium Ps Sendkeys: {text}")
94
+ text = text.replace('"', '""')
95
+
96
+ ps_command = (
97
+ f'Add-Type -AssemblyName System.Windows.Forms;'
98
+ f'[System.Windows.Forms.SendKeys]::SendWait("{text}")'
99
+ )
100
+ self._info(f'command: {ps_command}')
101
+
102
+ self.appium_execute_powershell_command(ps_command)
103
+
104
+ def appium_ps_drag_and_drop(
105
+ self,
106
+ start_locator=None,
107
+ end_locator=None,
108
+ x_start=0,
109
+ y_start=0,
110
+ x_end=0,
111
+ y_end=0,
112
+ button='left',
113
+ **kwargs
114
+ ):
115
+ """
116
+ Drag and drop using PowerShell-level mouse simulation.
117
+
118
+ Supports:
119
+ - Absolute coordinates or element-based location via start_locator/end_locator
120
+ - Optional offsets (absolute or 'center')
121
+ - Custom mouse button (left/right/mid)
122
+ - Drag duration (default 0.5 seconds)
123
+
124
+ kwargs:
125
+ - offset: sets both x/y offset globally (overrides individual)
126
+ - x_start_offset / y_start_offset
127
+ - x_end_offset / y_end_offset
128
+ - duration_sec: total drag time in seconds (default 0.5)
129
+ """
130
+ self._info(f"Appium Ps Drag And Drop")
131
+ # Get actual coordinates from locators (if any)
132
+ if start_locator:
133
+ start_rect = self.get_element_rect(start_locator)
134
+ x_start, y_start = self._parse_location(start_rect, kwargs, 'x_start_offset', 'y_start_offset')
135
+
136
+ if end_locator:
137
+ end_rect = self.get_element_rect(end_locator)
138
+ x_end, y_end = self._parse_location(end_rect, kwargs, 'x_end_offset', 'y_end_offset')
139
+
140
+ # Normalize against window position
141
+ root = self._current_application().get_window_rect()
142
+ self._info(f"[DEBUG] App window rect: {root}")
143
+
144
+ x_start, y_start = x_start + root['x'], y_start + root['y']
145
+ x_end, y_end = x_end + root['x'], y_end + root['y']
146
+
147
+ self._info(f"[DRAG] {button.upper()} Button: From ({x_start},{y_start}) → ({x_end},{y_end})")
148
+
149
+ # Get drag duration
150
+ duration_sec = float(kwargs.get('duration_sec', 0.5))
151
+ ps_command = self._generate_drag_command(x_start, y_start, x_end, y_end, button, duration_sec)
152
+ self._info(f"ps_command: \n{ps_command}")
153
+
154
+ # Execute PowerShell command
155
+ self.appium_execute_powershell_command(ps_command)
156
+
157
+ def appium_execute_powershell_command(self, command, handle_exception=False):
158
+ """
159
+ Executes a PowerShell command using Appium's execute_script method.
160
+
161
+ Note:
162
+ PowerShell command execution must be allowed on the Appium server.
163
+ For this, Appium must be started with the `--relaxed-security` flag:
164
+ appium --relaxed-security
165
+
166
+ Args:
167
+ command (str): The PowerShell command to be executed.
168
+ handle_exception (bool): If True, return the exception object on error. Otherwise, return None.
169
+
170
+ Returns:
171
+ str | dict | Exception: The result of the execution or the exception object.
172
+
173
+ Raises:
174
+ Exception: If handle_exception is False and an error occurs.
175
+ """
176
+ try:
177
+ driver = self._current_application()
178
+ result = driver.execute_script("powerShell", {"command": command})
179
+ return result
180
+ except Exception as exc:
181
+ if handle_exception:
182
+ return exc
183
+ raise
184
+
185
+ def appium_execute_powershell_script(self, ps_script=None, file_path=None, handle_exception=False):
186
+ """
187
+ Executes a PowerShell script using Appium's execute_script method.
188
+
189
+ Note:
190
+ PowerShell command execution must be allowed on the Appium server.
191
+ For this, Appium must be started with the `--relaxed-security` flag:
192
+ appium --relaxed-security
193
+
194
+ Args:
195
+ ps_script (str): The full PowerShell script to be executed.
196
+ file_path (str): The file ps1 to be executed.
197
+ handle_exception (bool): If True, return the exception object on failure. If False, return None on failure.
198
+
199
+ Returns:
200
+ str | dict | Exception: The result of the script execution or the exception object.
201
+
202
+ Raises:
203
+ Exception: If handle_exception is False and an error occurs.
204
+ """
205
+ try:
206
+ if file_path:
207
+ self._info(f'file_path: {file_path}')
208
+ ps_script = utils.read_file(file_path)
209
+ self._info(f"ps_script: \n{ps_script}")
210
+ driver = self._current_application()
211
+ result = driver.execute_script("powerShell", {"script": ps_script})
212
+ return result
213
+ except Exception as exc:
214
+ if handle_exception:
215
+ return exc
216
+ raise
217
+
218
+ def appium_pull_file(self, path: str, save_path: str = None) -> str:
219
+ """Retrieves the file at `path`.
220
+
221
+ Powershell command must be allowed. eg: appium --relaxed-security
222
+
223
+ Args:
224
+ path: the path to the file on the device, eg: c:/users/user1/desktop/screenshot_file.png
225
+ save_path: path to save, eg: /Users/user1/desktop/screenshot.png
226
+
227
+ Returns:
228
+ The file's contents encoded as Base64.
229
+ """
230
+
231
+ # base64data = self._current_application().pull_file(path)
232
+ base64data = self._current_application().execute(Command.PULL_FILE, {'path': path})['value']
233
+
234
+ if save_path:
235
+ with open(save_path, "wb") as file:
236
+ file.write(base64.b64decode(base64data))
237
+
238
+ return base64data
239
+
240
+ def appium_pull_folder(self, path: str, save_path_as_zip: str = '') -> str:
241
+ """Retrieves a folder at `path`.
242
+
243
+ Powershell command must be allowed. eg: appium --relaxed-security
244
+
245
+ Args:
246
+ path: the path to the folder on the device. eg: c:/users/user1/desktop/folder1
247
+ save_path_as_zip: zip file. eg: /Users/user1/desktop/file.zip
248
+
249
+ Returns:
250
+ The folder's contents zipped and encoded as Base64.
251
+ """
252
+ # base64data = self._current_application().pull_folder(path)
253
+ base64data = self._current_application().execute(Command.PULL_FOLDER, {'path': path})['value']
254
+
255
+ if save_path_as_zip:
256
+ with open(save_path_as_zip, "wb") as file:
257
+ file.write(base64.b64decode(base64data))
258
+
259
+ return base64data
260
+
261
+ def appium_push_file(self, destination_path: str, source_path: str = None, base64data: str = None):
262
+ """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`.
263
+
264
+ Specify either `base64data` or `source_path`, if both specified default to `source_path`
265
+
266
+ Powershell command must be allowed. eg: appium --relaxed-security
267
+
268
+ Args:
269
+ destination_path: the location on the device/simulator where the local file contents should be saved.
270
+ eg: c:/users/user1/desktop/screenshot_file.png
271
+ base64data: file contents, encoded as Base64, to be written
272
+ to the file on the device/simulator. Eg: iVBORw0KGgoAAAANSUh...
273
+ source_path: local file path for the file to be loaded on device. Eg: /Users/user1/desktop/source_file.png
274
+
275
+ Returns:
276
+ base64data
277
+ """
278
+ if source_path is None and base64data is None:
279
+ raise InvalidArgumentException('Must either pass base64 data or a local file path')
280
+
281
+ if source_path is not None:
282
+ try:
283
+ with open(source_path, 'rb') as f:
284
+ file_data = f.read()
285
+ except IOError as e:
286
+ message = f'source_path "{source_path}" could not be found. Are you sure the file exists?'
287
+ raise InvalidArgumentException(message) from e
288
+ base64data = base64.b64encode(file_data).decode('utf-8')
289
+
290
+ # result = self._current_application().push_file(destination_path, base64data, source_path)
291
+
292
+ self._current_application().execute(Command.PUSH_FILE, {'path': destination_path, 'data': base64data})
293
+
294
+ return base64data
295
+
296
+ def appium_transfer_file(self, file_path, remote_path):
297
+ """
298
+ Streams a binary file, base64-encodes it chunk by chunk,
299
+ and sends it directly to a remote machine via PowerShell commands.
300
+
301
+ Powershell command must be allowed. eg: appium --relaxed-security
302
+
303
+ file_path: source file path, eg: c:/users/user1/desktop/screenshot_file.png
304
+ remote_path: destination path, eg: c:/users/user1/download/screenshot_file.png
305
+ """
306
+ file_path = Path(file_path)
307
+ remote_path = str(remote_path)
308
+ remote_b64_path = remote_path + ".b64.tmp"
309
+ chunk_size = 6000 # chunk size in raw bytes (will expand when base64 encoded)
310
+
311
+ # 1. Ensure remote parent directory exists
312
+ remote_directory = str(Path(remote_path).parent)
313
+ mkdir_cmd = f'New-Item -Path "{remote_directory}" -ItemType Directory -Force'
314
+ self.execute_script("powerShell", command=mkdir_cmd)
315
+ self._info(f"Ensured remote directory: {remote_directory}")
316
+
317
+ # 2. Open and stream file, encoding and sending each chunk
318
+ with open(file_path, "rb") as f:
319
+ chunk_index = 0
320
+ while True:
321
+ chunk = f.read(chunk_size)
322
+ if not chunk:
323
+ break
324
+ chunk_b64 = base64.b64encode(chunk).decode('utf-8')
325
+ escaped_chunk = chunk_b64.replace("`", "``").replace('"', '`"')
326
+ ps = f'Add-Content -Path "{remote_b64_path}" -Value "{escaped_chunk}"'
327
+ self.execute_script("powerShell", command=ps)
328
+ chunk_index += 1
329
+ self._info(f"Sent chunk {chunk_index}")
330
+
331
+ # 3. Decode and write binary file on remote side
332
+ decode_script = (
333
+ f'[IO.File]::WriteAllBytes("{remote_path}";'
334
+ f'[Convert]::FromBase64String((Get-Content "{remote_b64_path}" -Raw)))'
335
+ )
336
+ self.execute_script("powerShell", command=decode_script)
337
+ self._info(f"File written to: {remote_path}")
338
+
339
+ # 4. Optional cleanup
340
+ cleanup_script = f'Remove-Item "{remote_b64_path}" -ErrorAction SilentlyContinue'
341
+ self.execute_script("powerShell", command=cleanup_script)
342
+ self._info("Cleaned up temporary base64 file.")
343
+
344
+ def appium_split_and_push_file(self, source_path: str, remote_path: str, chunk_size_mb: int = 20):
345
+ """
346
+ Splits a binary file into chunks, pushes each to a remote machine via Appium,
347
+ then executes a PowerShell script remotely to recombine and clean up chunk files.
348
+
349
+ Powershell command must be allowed. eg: appium --relaxed-security
350
+
351
+ Parameters:
352
+ source_path (str): Local path to the binary file.
353
+ remote_path (str): Full remote file path to create from recombination.
354
+ chunk_size_mb (int): Size of each chunk in MB (default: 20MB).
355
+ """
356
+ chunk_size = chunk_size_mb * 1024 * 1024
357
+ file_size = os.path.getsize(source_path)
358
+
359
+ file_name = os.path.basename(source_path)
360
+ remote_dir = os.path.dirname(remote_path)
361
+ total_chunks = math.ceil(file_size / chunk_size)
362
+ # chunk_index_digits = max(4, len(str(total_chunks - 1))) # at least 4 digits
363
+ chunk_index_digits = len(str(total_chunks - 1))
364
+
365
+ self._info(f"Splitting '{file_name}' ({file_size} bytes) into {total_chunks} chunks of {chunk_size_mb}MB each")
366
+
367
+ # Step 1: Split and push chunks
368
+ with open(source_path, "rb") as f:
369
+ for index in range(total_chunks):
370
+ chunk = f.read(chunk_size)
371
+ if not chunk:
372
+ break
373
+
374
+ b64_chunk = base64.b64encode(chunk).decode("utf-8")
375
+ chunk_suffix = f"{index:0{chunk_index_digits}d}"
376
+ remote_chunk_path = os.path.join(remote_dir, f"{file_name}.part{chunk_suffix}")
377
+
378
+ self.appium_push_file(destination_path=remote_chunk_path, base64data=b64_chunk)
379
+ self._info(f"Pushed chunk {chunk_suffix} to {remote_chunk_path}")
380
+
381
+ # Step 2: Build PowerShell recombine script
382
+ escaped_dir = remote_dir.replace("'", "''")
383
+ escaped_out = remote_path.replace("'", "''")
384
+ escaped_base = file_name.replace("'", "''")
385
+ pad_format = f"D{chunk_index_digits}"
386
+
387
+ ps_script_lines = [
388
+ f"$out = [System.IO.File]::OpenWrite('{escaped_out}')",
389
+ "$out.SetLength(0)",
390
+ f"for ($i = 0; $i -lt {total_chunks}; $i++) {{",
391
+ f" $chunkName = '{escaped_base}.part' + $i.ToString('{pad_format}')",
392
+ f" $chunkPath = Join-Path '{escaped_dir}' $chunkName",
393
+ ' if (-not (Test-Path $chunkPath)) { throw "Missing chunk: $chunkPath" }',
394
+ " $in = [System.IO.File]::OpenRead($chunkPath)",
395
+ " $buffer = New-Object byte[] (1MB)",
396
+ " while (($n = $in.Read($buffer, 0, $buffer.Length)) -gt 0) {",
397
+ " $out.Write($buffer, 0, $n)",
398
+ " }",
399
+ " $in.Close()",
400
+ "}",
401
+ "$out.Close()",
402
+ f"for ($i = 0; $i -lt {total_chunks}; $i++) {{",
403
+ f" $chunkName = '{escaped_base}.part' + $i.ToString('{pad_format}')",
404
+ f" $chunkPath = Join-Path '{escaped_dir}' $chunkName",
405
+ " Remove-Item -Path $chunkPath -Force -ErrorAction SilentlyContinue",
406
+ "}",
407
+ f"Write-Output 'Combine and cleanup complete: {escaped_out}'"
408
+ ]
409
+
410
+ ps_script = "\n".join(ps_script_lines)
411
+
412
+ # Step 3: Execute recombine script remotely
413
+ self._info("Combining chunks and cleaning up on remote machine...")
414
+ result = self.appium_execute_powershell_script(ps_script)
415
+ self._info(f"Remote PowerShell result: {result}")
416
+ return escaped_out
417
+
418
+ # Private
419
+
420
+ def _parse_location(self, rect, kwargs, x_offset_key, y_offset_key):
421
+ """Parse offset inside a rect."""
422
+ offset = kwargs.get('offset')
423
+ x_offset = offset if offset is not None else kwargs.get(x_offset_key, 'center')
424
+ y_offset = offset if offset is not None else kwargs.get(y_offset_key, 'center')
425
+
426
+ x = rect['x'] + (rect['width'] // 2 if x_offset == 'center' else int(x_offset))
427
+ y = rect['y'] + (rect['height'] // 2 if y_offset == 'center' else int(y_offset))
428
+ return x, y
429
+
430
+ def _generate_click_command(self, x, y, button='left'):
431
+ """
432
+ Generate a PowerShell command to click at the given coordinates.
433
+
434
+ https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event
435
+
436
+ @param x: X coordinate
437
+ @param y: Y coordinate
438
+ @param button:
439
+ 'left' - single left click
440
+ 'right' - single right click
441
+ 'middle' - single middle click
442
+ 'double' - double left click
443
+ 'triple' - triple left click
444
+ 'right-double' - double right click
445
+ @return: PowerShell one-liner as string
446
+ """
447
+ button = button.lower()
448
+
449
+ # Mouse event codes
450
+ left_code = 6 # MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP
451
+ right_code = 24 # MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP
452
+ middle_code = 40 # MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP
453
+
454
+ # Define all buttons
455
+ click_events = {
456
+ 'left': f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);',
457
+ 'right': f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);',
458
+ 'middle': f'[M]::mouse_event({middle_code},0,0,0,[UIntPtr]::Zero);',
459
+ 'double': (
460
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
461
+ f'Start-Sleep -m 100;'
462
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
463
+ ),
464
+ 'triple': (
465
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
466
+ f'Start-Sleep -m 100;'
467
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
468
+ f'Start-Sleep -m 100;'
469
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
470
+ ),
471
+ 'right-double': (
472
+ f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);'
473
+ f'Start-Sleep -m 100;'
474
+ f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);'
475
+ )
476
+ }
477
+
478
+ if button not in click_events:
479
+ valid = ', '.join(click_events.keys())
480
+ raise ValueError(f"button must be one of: {valid}")
481
+
482
+ event_code = click_events[button]
483
+
484
+ ps_cmd = (
485
+ 'if (-not ("M" -as [type])) {'
486
+ "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;"
487
+ 'public class M{'
488
+ '[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);'
489
+ '[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr i);}'
490
+ "'|Out-Null;};"
491
+ f'[M]::SetCursorPos({x},{y});Start-Sleep -m 300;'
492
+ f'{event_code}'
493
+ )
494
+
495
+ return ps_cmd
496
+
497
+ def _generate_drag_command(self, x_start, y_start, x_end, y_end, button='left', duration_sec=0.5):
498
+ """
499
+ https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event
500
+
501
+ Generate a PowerShell command to simulate mouse drag from (x_start, y_start) to (x_end, y_end)
502
+ over the given duration in seconds.
503
+
504
+ @param x_start: Start X coordinate
505
+ @param y_start: Start Y coordinate
506
+ @param x_end: End X coordinate
507
+ @param y_end: End Y coordinate
508
+ @param button: 'left' or 'right'
509
+ @param duration_sec: Total drag duration in seconds
510
+ @return: PowerShell command string
511
+ """
512
+ button = button.lower()
513
+ button_codes = {
514
+ 'left': (0x0002, 0x0004), # MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP
515
+ 'right': (0x0008, 0x0010), # MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP
516
+ 'middle': (0x0020, 0x0040), # MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP
517
+ }
518
+
519
+ if button not in button_codes:
520
+ raise ValueError("button must be 'left', 'right', or 'middle'")
521
+
522
+ down_code, up_code = button_codes[button]
523
+
524
+ delay_ms = 50
525
+ steps = round((float(duration_sec) * 1000) / delay_ms)
526
+ dx = x_end - x_start
527
+ dy = y_end - y_start
528
+
529
+ ps_cmd = (
530
+ 'if (-not ("M" -as [type])) {'
531
+ "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;"
532
+ 'public class M{'
533
+ '[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);'
534
+ '[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr i);}'
535
+ "'|Out-Null;};"
536
+ f'[M]::SetCursorPos({x_start},{y_start})|Out-Null; Start-Sleep -m 300;'
537
+ f'[M]::mouse_event({down_code},0,0,0,[UIntPtr]::Zero); Start-Sleep -m 300;'
538
+ f'for ($i=1; $i -le {steps}; $i++){{'
539
+ f'$x={x_start}+[math]::Round({dx}*$i/{steps});'
540
+ f'$y={y_start}+[math]::Round({dy}*$i/{steps});'
541
+ f'[M]::SetCursorPos($x,$y)|Out-Null; Start-Sleep -m {delay_ms};'
542
+ '};'
543
+ f'[M]::SetCursorPos({x_end},{y_end})|Out-Null; Start-Sleep -m 300;'
544
+ f'[M]::mouse_event({up_code},0,0,0,[UIntPtr]::Zero);'
545
+ )
546
+
547
+ return ps_cmd
548
+
549
+ def _generate_keyboard_command(self, sequences):
550
+ raise Exception('Not Implement yet')
551
+
552
+ def _script_path(self, name):
553
+ return os.path.join(SCRIPTS_DIR, name)
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from robot.libraries import BuiltIn
4
+
5
+ from .keywordgroup import KeywordGroup
6
+
7
+ BUILTIN = BuiltIn.BuiltIn()
8
+
9
+
10
+ class _RunOnFailureKeywords(KeywordGroup):
11
+
12
+ def __init__(self):
13
+ self._run_on_failure_keyword = None
14
+ self._running_on_failure_routine = False
15
+
16
+ # Public
17
+
18
+ def register_keyword_to_run_on_failure(self, keyword):
19
+ """Sets the keyword to execute when a AppiumLibrary keyword fails.
20
+
21
+ `keyword_name` is the name of a keyword (from any available
22
+ libraries) that will be executed if a AppiumLibrary keyword fails.
23
+ It is not possible to use a keyword that requires arguments.
24
+ Using the value "Nothing" will disable this feature altogether.
25
+
26
+ The initial keyword to use is set in `importing`, and the
27
+ keyword that is used by default is `Capture Page Screenshot`.
28
+ Taking a screenshot when something failed is a very useful
29
+ feature, but notice that it can slow down the execution.
30
+
31
+ This keyword returns the name of the previously registered
32
+ failure keyword. It can be used to restore the original
33
+ value later.
34
+
35
+ Example:
36
+ | Register Keyword To Run On Failure | Log Source | # Run `Log Source` on failure. |
37
+ | ${previous kw}= | Register Keyword To Run On Failure | Nothing | # Disables run-on-failure functionality and stores the previous kw name in a variable. |
38
+ | Register Keyword To Run On Failure | ${previous kw} | # Restore to the previous keyword. |
39
+
40
+ This run-on-failure functionality only works when running tests on Python/Jython 2.4
41
+ or newer and it does not work on IronPython at all.
42
+ """
43
+ old_keyword = self._run_on_failure_keyword
44
+ old_keyword_text = old_keyword if old_keyword is not None else "Nothing"
45
+
46
+ new_keyword = keyword if keyword.strip().lower() != "nothing" else None
47
+ new_keyword_text = new_keyword if new_keyword is not None else "Nothing"
48
+
49
+ self._run_on_failure_keyword = new_keyword
50
+ self._info('%s will be run on failure.' % new_keyword_text)
51
+
52
+ return old_keyword_text
53
+
54
+ # Private
55
+
56
+ def _run_on_failure(self):
57
+ if self._run_on_failure_keyword is None:
58
+ return
59
+ if self._running_on_failure_routine:
60
+ return
61
+ self._running_on_failure_routine = True
62
+ try:
63
+ BUILTIN.run_keyword(self._run_on_failure_keyword)
64
+ except Exception as err:
65
+ self._run_on_failure_error(err)
66
+ finally:
67
+ self._running_on_failure_routine = False
68
+
69
+ def _run_on_failure_error(self, err):
70
+ err = "Keyword '%s' could not be run on failure: %s" % (self._run_on_failure_keyword, err)
71
+ if hasattr(self, '_warn'):
72
+ self._warn(err)
73
+ return
74
+ raise Exception(err)