selmate 1.0.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.
- selmate/__init__.py +3 -0
- selmate/composites.py +662 -0
- selmate/constants.py +329 -0
- selmate/humanity/constants.py +6 -0
- selmate/humanity/latency.py +41 -0
- selmate/js_primitives.py +170 -0
- selmate/safe_exceptions.py +34 -0
- selmate/selenium_primitives.py +70 -0
- selmate/utils.py +165 -0
- selmate-1.0.0.dist-info/LICENSE +21 -0
- selmate-1.0.0.dist-info/METADATA +133 -0
- selmate-1.0.0.dist-info/RECORD +13 -0
- selmate-1.0.0.dist-info/WHEEL +4 -0
selmate/__init__.py
ADDED
selmate/composites.py
ADDED
@@ -0,0 +1,662 @@
|
|
1
|
+
import logging
|
2
|
+
import random
|
3
|
+
import re
|
4
|
+
from itertools import pairwise
|
5
|
+
from operator import itemgetter
|
6
|
+
from typing import List, Any, Generator
|
7
|
+
from urllib.parse import urlparse
|
8
|
+
|
9
|
+
from bs4 import BeautifulSoup
|
10
|
+
from rapidfuzz import fuzz
|
11
|
+
from selenium.webdriver import ActionChains
|
12
|
+
|
13
|
+
from selenium.webdriver.common.by import By
|
14
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
15
|
+
from selenium.webdriver.remote.webelement import WebElement
|
16
|
+
from selenium.webdriver.support import expected_conditions
|
17
|
+
from selenium.webdriver.support.wait import WebDriverWait
|
18
|
+
|
19
|
+
from selmate.constants import WINDOW_LOCATION_HREF_JS_PATTERN
|
20
|
+
from selmate.humanity.constants import HUMAN_SCROLL_LATENCY
|
21
|
+
from selmate.humanity.latency import human_click_latency, human_mouse_move_latency, human_focus_element_latency, \
|
22
|
+
human_observe_view_latency, human_scroll_latency
|
23
|
+
from selmate.js_primitives import js_smooth_scroll, js_remove_element, js_need_scroll_to_element, js_scroll_to_element, \
|
24
|
+
js_click, js_choose_elements_above_z_index
|
25
|
+
from selmate.safe_exceptions import safe_timeout, safe_stale, safe_out_of_bound, safe_not_found, safe_not_interactable
|
26
|
+
from selmate.selenium_primitives import find_element_safely, selenium_element_center, selenium_scroll_to_element, \
|
27
|
+
selenium_click
|
28
|
+
from selmate.utils import is_confirmation_text, norm_string, latency_time, normalize_url, \
|
29
|
+
is_webpage, generate_curved_path
|
30
|
+
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
|
34
|
+
def wandering_between_elements(elements, driver, max_moves=0):
|
35
|
+
"""Simulates mouse wandering between pairs of elements.
|
36
|
+
:param elements: List of elements to wander between.
|
37
|
+
:param driver: The Selenium WebDriver instance.
|
38
|
+
:param max_moves: Maximum number of moves to perform.
|
39
|
+
:return: Tuple of the last element and number of moves made.
|
40
|
+
"""
|
41
|
+
move_cnt = 0
|
42
|
+
el2 = None
|
43
|
+
for el1, el2 in pairwise(elements):
|
44
|
+
if el1 is None or not element_displayed_enabled(el1):
|
45
|
+
continue
|
46
|
+
|
47
|
+
if el2 is None or not element_displayed_enabled(el2):
|
48
|
+
continue
|
49
|
+
|
50
|
+
wander_between_2_elements(el1, el2, driver, 0.01)
|
51
|
+
human_observe_view_latency()
|
52
|
+
|
53
|
+
move_cnt += 1
|
54
|
+
if max_moves and move_cnt >= max_moves:
|
55
|
+
break
|
56
|
+
|
57
|
+
return el2, move_cnt
|
58
|
+
|
59
|
+
|
60
|
+
def get_current_base_url(driver):
|
61
|
+
"""Retrieves the base URL from the page or driver.
|
62
|
+
:param driver: The Selenium WebDriver instance.
|
63
|
+
:return: The base URL of the current page.
|
64
|
+
"""
|
65
|
+
base_tag = find_element_safely(By.TAG_NAME, 'base', driver)
|
66
|
+
if base_tag:
|
67
|
+
base_current_url = base_tag.get_attribute("href")
|
68
|
+
else:
|
69
|
+
base_current_url = driver.current_url
|
70
|
+
|
71
|
+
parsed_url = urlparse(base_current_url)
|
72
|
+
return f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path if parsed_url.path else ''}"
|
73
|
+
|
74
|
+
|
75
|
+
def find_transition_buttons(base_url, driver):
|
76
|
+
"""Finds navigation elements like links or buttons.
|
77
|
+
:param base_url: The base URL for normalizing links.
|
78
|
+
:param driver: The Selenium WebDriver instance.
|
79
|
+
:return: List of navigation web elements.
|
80
|
+
"""
|
81
|
+
xpath_selector = (
|
82
|
+
"//a[@href] | "
|
83
|
+
"//button[@onclick or @data-href or contains(@class, 'nav')] | "
|
84
|
+
"//*[contains(@class, 'link') or contains(@class, 'menu-item')]"
|
85
|
+
)
|
86
|
+
elements = driver.find_elements(By.XPATH, xpath_selector)
|
87
|
+
|
88
|
+
navigation_elements = []
|
89
|
+
for element in elements:
|
90
|
+
if not element_displayed_enabled(element):
|
91
|
+
continue
|
92
|
+
|
93
|
+
url = get_element_url(element)
|
94
|
+
if not url:
|
95
|
+
navigation_elements.append(element)
|
96
|
+
continue
|
97
|
+
|
98
|
+
norm_url = normalize_url(url, base_url, True, True, True)
|
99
|
+
if not norm_url:
|
100
|
+
navigation_elements.append(element)
|
101
|
+
continue
|
102
|
+
|
103
|
+
if is_webpage(norm_url):
|
104
|
+
navigation_elements.append(element)
|
105
|
+
continue
|
106
|
+
|
107
|
+
return navigation_elements
|
108
|
+
|
109
|
+
|
110
|
+
@safe_stale(def_val=None)
|
111
|
+
@safe_not_found(def_val=None)
|
112
|
+
@safe_not_interactable(def_val=None)
|
113
|
+
def get_element_url(element: WebElement):
|
114
|
+
"""Extracts the URL from an element's attributes.
|
115
|
+
:param element: The web element to check.
|
116
|
+
:return: The URL if found, None otherwise.
|
117
|
+
"""
|
118
|
+
tag_name = element.tag_name
|
119
|
+
|
120
|
+
if tag_name == "a":
|
121
|
+
url = element.get_attribute("href")
|
122
|
+
if url:
|
123
|
+
return url
|
124
|
+
|
125
|
+
elif tag_name == "button":
|
126
|
+
data_href = element.get_attribute("data-href")
|
127
|
+
if data_href:
|
128
|
+
return data_href
|
129
|
+
|
130
|
+
onclick = element.get_attribute("onclick")
|
131
|
+
if onclick:
|
132
|
+
match = re.search(WINDOW_LOCATION_HREF_JS_PATTERN, onclick)
|
133
|
+
if match:
|
134
|
+
return match.group(1)
|
135
|
+
|
136
|
+
data_href = element.get_attribute("data-href")
|
137
|
+
if data_href:
|
138
|
+
return data_href
|
139
|
+
|
140
|
+
return None
|
141
|
+
|
142
|
+
|
143
|
+
def selenium_random_vertical_scroll(driver, from_ratio=-0.5, to_ratio=0.5, step=10):
|
144
|
+
"""Performs a random vertical scroll within a ratio range.
|
145
|
+
:param driver: The Selenium WebDriver instance.
|
146
|
+
:param from_ratio: Minimum scroll ratio.
|
147
|
+
:param to_ratio: Maximum scroll ratio.
|
148
|
+
:param step: Amount of pixels for one scroll.
|
149
|
+
"""
|
150
|
+
doc_height = driver.execute_script("return document.body.scrollHeight")
|
151
|
+
aim_ratio = random.uniform(from_ratio, to_ratio)
|
152
|
+
logger.debug(f'Aim ratio for random human scroll: <{aim_ratio:.2f}>.')
|
153
|
+
on_delta = int(aim_ratio * doc_height)
|
154
|
+
|
155
|
+
selenium_human_vertical_scroll(on_delta, driver, step)
|
156
|
+
|
157
|
+
|
158
|
+
def selenium_human_vertical_scroll(y_delta, driver, step=10):
|
159
|
+
"""Performs a smooth vertical scroll by a specified delta.
|
160
|
+
:param y_delta: The vertical distance to scroll.
|
161
|
+
:param driver: The Selenium WebDriver instance.
|
162
|
+
:param step: Amount of pixels for one scroll.
|
163
|
+
"""
|
164
|
+
steps = int(y_delta / step)
|
165
|
+
|
166
|
+
actions = ActionChains(driver)
|
167
|
+
for i in range(steps):
|
168
|
+
actions.scroll_by_amount(0, step).pause(latency_time(*HUMAN_SCROLL_LATENCY))
|
169
|
+
|
170
|
+
actions.perform()
|
171
|
+
|
172
|
+
|
173
|
+
def js_random_human_scroll(driver, from_ratio=0.25, to_ratio=1.0, step=10, to_x=0):
|
174
|
+
"""Performs a random vertical scroll using JavaScript.
|
175
|
+
:param driver: The Selenium WebDriver instance.
|
176
|
+
:param from_ratio: Minimum scroll ratio.
|
177
|
+
:param to_ratio: Maximum scroll ratio.
|
178
|
+
:param step: Amount of pixels for one scroll.
|
179
|
+
:param to_x: Target x-coordinate for scroll.
|
180
|
+
"""
|
181
|
+
doc_height = driver.execute_script("return document.body.scrollHeight")
|
182
|
+
aim_ratio = random.uniform(from_ratio, to_ratio)
|
183
|
+
logger.debug(f'Aim ratio for random human scroll: <{aim_ratio:.2f}>.')
|
184
|
+
to_y = aim_ratio * doc_height
|
185
|
+
|
186
|
+
js_human_vertical_scroll(to_y, driver, step, to_x)
|
187
|
+
|
188
|
+
|
189
|
+
def js_human_vertical_scroll(to_y, driver, step=10, to_x=0):
|
190
|
+
"""Performs a smooth vertical scroll to a y-coordinate using JavaScript.
|
191
|
+
:param to_y: Target y-coordinate.
|
192
|
+
:param driver: The Selenium WebDriver instance.
|
193
|
+
:param step: Amount of pixels for one scroll.
|
194
|
+
:param to_x: Target x-coordinate.
|
195
|
+
"""
|
196
|
+
steps = int(to_y / step)
|
197
|
+
|
198
|
+
for i in range(steps):
|
199
|
+
js_smooth_scroll(to_x, i * step, driver)
|
200
|
+
human_scroll_latency()
|
201
|
+
|
202
|
+
|
203
|
+
def selenium_find_close_buttons(parent: WebElement | WebDriver, text_similarity_threshold=0.75):
|
204
|
+
"""Finds close buttons within a parent element or driver.
|
205
|
+
:param parent: The parent element or WebDriver to search in.
|
206
|
+
:param text_similarity_threshold: Similarity threshold for button text.
|
207
|
+
:return: List of close button elements.
|
208
|
+
"""
|
209
|
+
css_selector = "button, [role='button'], [type='button']"
|
210
|
+
close_text_variants = {'закрыть', 'close', '×', 'x'}
|
211
|
+
|
212
|
+
close_buttons = []
|
213
|
+
for button in parent.find_elements(By.CSS_SELECTOR, css_selector):
|
214
|
+
try:
|
215
|
+
btn_text = button.text
|
216
|
+
norm_btn_text = norm_string(btn_text)
|
217
|
+
norm_value = norm_string(button.get_attribute('value')) or ''
|
218
|
+
norm_aria_label = norm_string(button.get_attribute('aria-label')) or ''
|
219
|
+
norm_title = norm_string(button.get_attribute('title')) or ''
|
220
|
+
norm_class_name = norm_string(button.get_attribute('class'))
|
221
|
+
data_attrs = button.get_attribute('data-dismiss') or button.get_attribute('data-close') or ''
|
222
|
+
|
223
|
+
if (
|
224
|
+
any(map(
|
225
|
+
lambda close_text_var:
|
226
|
+
fuzz.ratio(norm_btn_text, close_text_var) / 100 > text_similarity_threshold,
|
227
|
+
close_text_variants
|
228
|
+
)) or
|
229
|
+
'close' in norm_class_name or
|
230
|
+
'close' in norm_value or
|
231
|
+
'close' in norm_aria_label or
|
232
|
+
'close' in norm_title or
|
233
|
+
data_attrs.lower() in {'close', 'modal', 'dialog'}
|
234
|
+
):
|
235
|
+
close_buttons.append(button)
|
236
|
+
logger.debug(f'Button <{btn_text}> was classified as a close button.')
|
237
|
+
except Exception as e:
|
238
|
+
continue
|
239
|
+
|
240
|
+
return close_buttons
|
241
|
+
|
242
|
+
|
243
|
+
@safe_not_found(def_val=0)
|
244
|
+
@safe_stale(def_val=0)
|
245
|
+
def soup_count_children(element: WebElement):
|
246
|
+
"""Counts the number of child elements using BeautifulSoup.
|
247
|
+
:param element: The web element to analyze.
|
248
|
+
:return: Number of child elements.
|
249
|
+
"""
|
250
|
+
el_html = element.get_attribute('innerHTML')
|
251
|
+
soup = BeautifulSoup(el_html, 'lxml')
|
252
|
+
all_elements = soup.find_all()
|
253
|
+
return len(all_elements)
|
254
|
+
|
255
|
+
|
256
|
+
def bypass_popup_banners_with_iframes(
|
257
|
+
driver, observation_capacity=100, success_capacity=5,
|
258
|
+
try_close=True, allow_removing_when_closing=True
|
259
|
+
):
|
260
|
+
"""Bypasses popup banners, including those in iframes.
|
261
|
+
:param driver: The Selenium WebDriver instance.
|
262
|
+
:param observation_capacity: Maximum elements to observe.
|
263
|
+
:param success_capacity: Maximum successful actions.
|
264
|
+
:param try_close: Whether to attempt closing popups.
|
265
|
+
:param allow_removing_when_closing: Whether to allow element removal.
|
266
|
+
:return: True if successful, False otherwise.
|
267
|
+
"""
|
268
|
+
body = find_element_safely(By.TAG_NAME, 'body', driver, timeout=1.0)
|
269
|
+
if body is None:
|
270
|
+
logger.debug('Body element not found for bypassing popup banners.')
|
271
|
+
return False
|
272
|
+
|
273
|
+
iframes = find_available_elements_gtr(By.TAG_NAME, 'iframe', body)
|
274
|
+
for iframe in iframes:
|
275
|
+
if not element_displayed_enabled(iframe):
|
276
|
+
continue
|
277
|
+
|
278
|
+
driver.switch_to.frame(iframe)
|
279
|
+
logger.debug('Switched to iframe to search for popup banners.')
|
280
|
+
bypass_popup_banners_with_iframes(
|
281
|
+
driver, observation_capacity, success_capacity,
|
282
|
+
try_close, allow_removing_when_closing
|
283
|
+
)
|
284
|
+
logger.debug('Returned from iframe after searching for popup banners.')
|
285
|
+
driver.switch_to.parent_frame()
|
286
|
+
|
287
|
+
bypass_popup_banners(driver, observation_capacity, success_capacity, try_close, allow_removing_when_closing)
|
288
|
+
|
289
|
+
return True
|
290
|
+
|
291
|
+
|
292
|
+
def bypass_popup_banners(
|
293
|
+
driver, observation_capacity=100, success_capacity=5,
|
294
|
+
try_close=True, allow_removing_when_closing=True
|
295
|
+
):
|
296
|
+
"""Bypasses popup banners by accepting or closing them.
|
297
|
+
:param driver: The Selenium WebDriver instance.
|
298
|
+
:param observation_capacity: Maximum elements to observe.
|
299
|
+
:param success_capacity: Maximum successful actions.
|
300
|
+
:param try_close: Whether to attempt closing popups.
|
301
|
+
:param allow_removing_when_closing: Whether to allow element removal.
|
302
|
+
"""
|
303
|
+
elements = find_top_available_elements(By.CSS_SELECTOR, 'div', driver)
|
304
|
+
logger.debug(f'Found <{len(elements)}> elements as potential popup banners.')
|
305
|
+
cnt = 0
|
306
|
+
suc_cnt = 0
|
307
|
+
for el in elements:
|
308
|
+
cnt += 1
|
309
|
+
if not element_displayed_enabled(el):
|
310
|
+
continue
|
311
|
+
|
312
|
+
if accept_popup_banner(el, driver):
|
313
|
+
suc_cnt += 1
|
314
|
+
elif try_close:
|
315
|
+
logger.debug('Failed to accept popup banner. Attempting to close.')
|
316
|
+
close_element(el, driver, allow_removing=allow_removing_when_closing)
|
317
|
+
|
318
|
+
if cnt > observation_capacity:
|
319
|
+
logger.debug(f'Observation limit of <{observation_capacity}> elements reached.')
|
320
|
+
break
|
321
|
+
|
322
|
+
if suc_cnt > success_capacity:
|
323
|
+
logger.debug(f'Confirmation limit of <{success_capacity}> reached.')
|
324
|
+
break
|
325
|
+
|
326
|
+
|
327
|
+
@safe_not_found(def_val=True)
|
328
|
+
@safe_stale(def_val=True)
|
329
|
+
def close_element(element, driver, allow_removing=True, close_btn_text_threshold=0.75):
|
330
|
+
"""Closes an element by clicking a close button or removing it.
|
331
|
+
:param element: The web element to close.
|
332
|
+
:param driver: The Selenium WebDriver instance.
|
333
|
+
:param allow_removing: Whether to allow element removal.
|
334
|
+
:param close_btn_text_threshold: Similarity threshold for close button text.
|
335
|
+
:return: True if the element was closed or removed.
|
336
|
+
"""
|
337
|
+
close_buttons = selenium_find_close_buttons(element, close_btn_text_threshold)
|
338
|
+
for close_btn in close_buttons:
|
339
|
+
if not element_displayed_enabled(close_btn):
|
340
|
+
logger.debug('Close button is no longer available in the element.')
|
341
|
+
continue
|
342
|
+
|
343
|
+
rect = element.rect
|
344
|
+
move_radius = 2 * max(rect['width'], rect['height'])
|
345
|
+
x, y = selenium_element_center(element)
|
346
|
+
logger.debug(f'Close button center coordinates: x=<{x}>, y=<{y}>.')
|
347
|
+
|
348
|
+
if complex_click(close_btn, driver):
|
349
|
+
logger.debug('Successfully clicked the close button in the element.')
|
350
|
+
human_click_latency()
|
351
|
+
else:
|
352
|
+
logger.debug('Failed to click the close button.')
|
353
|
+
continue
|
354
|
+
|
355
|
+
logger.debug('Starting random mouse movement near the close button.')
|
356
|
+
random_mouse_move_in_vicinity(int(move_radius), driver, int(x), int(y))
|
357
|
+
|
358
|
+
if not element_displayed_enabled(element):
|
359
|
+
logger.debug('Element successfully disappeared after clicking the close button.')
|
360
|
+
return True
|
361
|
+
|
362
|
+
logger.debug('Failed to close the element.')
|
363
|
+
if not allow_removing:
|
364
|
+
return False
|
365
|
+
|
366
|
+
logger.debug('Attempting to remove the element.')
|
367
|
+
return js_remove_element(element, driver)
|
368
|
+
|
369
|
+
|
370
|
+
def accept_popup_banner(banner, driver):
|
371
|
+
"""Accepts a popup banner by clicking checkboxes or confirmation buttons.
|
372
|
+
:param banner: The popup banner element.
|
373
|
+
:param driver: The Selenium WebDriver instance.
|
374
|
+
:return: True if the banner was accepted.
|
375
|
+
"""
|
376
|
+
avb_checkboxes = find_available_elements_gtr(
|
377
|
+
By.CSS_SELECTOR,
|
378
|
+
'input[type="checkbox"], [role="input"][type="checkbox"]',
|
379
|
+
banner
|
380
|
+
)
|
381
|
+
for avb_checkbox in avb_checkboxes:
|
382
|
+
logger.debug('Found an available checkbox in the popup banner.')
|
383
|
+
if complex_click(avb_checkbox, driver, True):
|
384
|
+
logger.debug('Successfully clicked the checkbox in the popup banner.')
|
385
|
+
human_click_latency()
|
386
|
+
|
387
|
+
button_elements = choose_confirmation_buttons(banner, 0.75, True)
|
388
|
+
logger.debug(f'Found <{len(button_elements)}> confirmation buttons in the popup banner.')
|
389
|
+
for btn in button_elements:
|
390
|
+
if not element_displayed_enabled(btn):
|
391
|
+
logger.debug('Confirmation button is no longer available in the popup banner.')
|
392
|
+
continue
|
393
|
+
|
394
|
+
rect = btn.rect
|
395
|
+
move_radius = 2 * max(rect['width'], rect['height'])
|
396
|
+
x, y = selenium_element_center(btn)
|
397
|
+
logger.debug(f'Confirmation button center coordinates: x=<{x}>, y=<{y}>.')
|
398
|
+
|
399
|
+
if selenium_scroll_to_element(btn, driver):
|
400
|
+
human_observe_view_latency()
|
401
|
+
else:
|
402
|
+
logger.debug('Failed to scroll to the confirmation button via Selenium.')
|
403
|
+
|
404
|
+
if complex_click(btn, driver):
|
405
|
+
logger.debug('Successfully clicked the confirmation button in the popup banner.')
|
406
|
+
human_click_latency()
|
407
|
+
else:
|
408
|
+
logger.debug('Failed to click the confirmation button.')
|
409
|
+
continue
|
410
|
+
|
411
|
+
logger.debug('Starting random mouse movement near the confirmation button.')
|
412
|
+
random_mouse_move_in_vicinity(int(move_radius), driver, int(x), int(y))
|
413
|
+
|
414
|
+
if not element_displayed_enabled(btn):
|
415
|
+
logger.debug('Confirmation button successfully disappeared after clicking.')
|
416
|
+
return True
|
417
|
+
|
418
|
+
return False
|
419
|
+
|
420
|
+
|
421
|
+
def complex_click(element: WebElement, driver, prevent_unselect=True):
|
422
|
+
"""Performs a complex click operation with scrolling if needed.
|
423
|
+
:param element: The web element to click.
|
424
|
+
:param driver: The Selenium WebDriver instance.
|
425
|
+
:param prevent_unselect: Whether to avoid clicking already selected elements.
|
426
|
+
:return: True if the click was successful.
|
427
|
+
"""
|
428
|
+
if not element_displayed_enabled(element):
|
429
|
+
logger.debug('Click failed because the element is not available.')
|
430
|
+
return False
|
431
|
+
|
432
|
+
if prevent_unselect and element.is_selected():
|
433
|
+
logger.debug('No click needed; the element is already selected.')
|
434
|
+
return True
|
435
|
+
|
436
|
+
if js_need_scroll_to_element(element, driver):
|
437
|
+
logger.debug('Scrolling required for element click. Using JavaScript.')
|
438
|
+
js_scroll_to_element(element, driver)
|
439
|
+
human_observe_view_latency()
|
440
|
+
|
441
|
+
if not selenium_click(element):
|
442
|
+
logger.debug('Selenium click failed. Attempting JavaScript click.')
|
443
|
+
human_click_latency()
|
444
|
+
return js_click(element, driver)
|
445
|
+
|
446
|
+
return True
|
447
|
+
|
448
|
+
|
449
|
+
def random_mouse_move_in_vicinity(radius, driver, x1=0, y1=0, mouse_step=10.0, eps=0.1):
|
450
|
+
"""Moves the mouse randomly within a radius of a point.
|
451
|
+
:param radius: The radius for random movement.
|
452
|
+
:param driver: The Selenium WebDriver instance.
|
453
|
+
:param x1: Starting x-coordinate.
|
454
|
+
:param y1: Starting y-coordinate.
|
455
|
+
:param mouse_step: Amount of pixels for one scroll.
|
456
|
+
:param eps: Minimum movement threshold.
|
457
|
+
"""
|
458
|
+
window_size = driver.get_window_size()
|
459
|
+
max_x = window_size['width']
|
460
|
+
max_y = window_size['height']
|
461
|
+
|
462
|
+
x2, y2 = random.randint(x1 - radius, x1 + radius), random.randint(y1 - radius, y1 + radius)
|
463
|
+
x2 = min(max_x, max(0, x2))
|
464
|
+
y2 = min(max_y, max(0, y2))
|
465
|
+
|
466
|
+
actions = ActionChains(driver)
|
467
|
+
path = generate_curved_path(x1, y1, x2, y2, mouse_step)
|
468
|
+
logger.debug(f'Generated mouse path: <{path}>.')
|
469
|
+
suc_step_cnt, total_step_cnt = move_mouse_by_path(path, actions, x1, y1, eps)
|
470
|
+
|
471
|
+
logger.debug(f'Mouse movements in vicinity: <{suc_step_cnt}>/<{total_step_cnt}>.')
|
472
|
+
|
473
|
+
|
474
|
+
def find_top_available_elements(by, value, driver, skip_under_one=True, sort_z_children_desc=True) -> List[WebElement]:
|
475
|
+
"""Finds topmost available elements by z-index and visibility.
|
476
|
+
:param by: The method to locate elements.
|
477
|
+
:param value: The locator value.
|
478
|
+
:param driver: The Selenium WebDriver instance.
|
479
|
+
:param skip_under_one: Whether to skip elements with z-index < 1.
|
480
|
+
:param sort_z_children_desc: Whether to sort by z-index and children count.
|
481
|
+
:return: List of available web elements.
|
482
|
+
"""
|
483
|
+
sort_values = []
|
484
|
+
avb_elements = []
|
485
|
+
|
486
|
+
@safe_stale(def_val=None)
|
487
|
+
@safe_not_found(def_val=None)
|
488
|
+
def _el_score(el):
|
489
|
+
if not element_displayed_enabled(el):
|
490
|
+
return None
|
491
|
+
z_index = el.value_of_css_property("z-index")
|
492
|
+
z_index_value = int(z_index) if z_index.isdigit() else -1
|
493
|
+
if skip_under_one and z_index_value < 1:
|
494
|
+
return None
|
495
|
+
|
496
|
+
els_amount = soup_count_children(el)
|
497
|
+
|
498
|
+
return z_index_value, els_amount
|
499
|
+
|
500
|
+
elements = None
|
501
|
+
if by == By.CSS_SELECTOR and skip_under_one:
|
502
|
+
try:
|
503
|
+
elements = js_choose_elements_above_z_index(1, driver, value)
|
504
|
+
except Exception as e:
|
505
|
+
logger.debug(f'Failed to find top elements via JavaScript: <{str(e)}>.')
|
506
|
+
pass
|
507
|
+
|
508
|
+
if not elements:
|
509
|
+
elements = driver.find_elements(by, value)
|
510
|
+
|
511
|
+
for element in elements:
|
512
|
+
score = _el_score(element)
|
513
|
+
if score is None:
|
514
|
+
continue
|
515
|
+
sort_values.append(score)
|
516
|
+
avb_elements.append(element)
|
517
|
+
|
518
|
+
if sort_z_children_desc:
|
519
|
+
return [el for _, el in sorted(zip(sort_values, avb_elements), key=itemgetter(0), reverse=True)]
|
520
|
+
|
521
|
+
return avb_elements
|
522
|
+
|
523
|
+
|
524
|
+
@safe_stale(def_val=False)
|
525
|
+
@safe_out_of_bound(def_val=False)
|
526
|
+
def wander_between_2_elements(
|
527
|
+
from_element: WebElement, to_element: WebElement, driver: WebDriver, mouse_step=10.0, eps=0.01
|
528
|
+
):
|
529
|
+
"""Moves the mouse between two elements along a curved path.
|
530
|
+
:param from_element: Starting web element.
|
531
|
+
:param to_element: Ending web element.
|
532
|
+
:param driver: The Selenium WebDriver instance.
|
533
|
+
:param mouse_step: Amount of pixels for one scroll.
|
534
|
+
:param eps: Minimum movement threshold.
|
535
|
+
:return: True if the movement was successful.
|
536
|
+
"""
|
537
|
+
actions = ActionChains(driver)
|
538
|
+
|
539
|
+
actions.move_to_element(from_element).perform()
|
540
|
+
human_focus_element_latency()
|
541
|
+
|
542
|
+
x1, y1 = selenium_element_center(from_element)
|
543
|
+
x2, y2 = selenium_element_center(to_element)
|
544
|
+
|
545
|
+
path = generate_curved_path(x1, y1, x2, y2, mouse_step)
|
546
|
+
suc_step_cnt, total_step_cnt = move_mouse_by_path(path, actions, x1, y1, eps)
|
547
|
+
|
548
|
+
actions.move_to_element(to_element).perform()
|
549
|
+
|
550
|
+
logger.debug(f'Mouse movements while wandering between elements: <{suc_step_cnt}>/<{total_step_cnt}>.')
|
551
|
+
return True
|
552
|
+
|
553
|
+
|
554
|
+
def move_mouse_by_path(path, actions, x1=0, y1=0, eps=0.01):
|
555
|
+
"""Moves the mouse along a specified path.
|
556
|
+
:param path: List of (x, y) coordinates.
|
557
|
+
:param actions: ActionChains instance.
|
558
|
+
:param x1: Starting x-coordinate.
|
559
|
+
:param y1: Starting y-coordinate.
|
560
|
+
:param eps: Minimum movement threshold.
|
561
|
+
:return: Tuple of successful and total steps.
|
562
|
+
"""
|
563
|
+
|
564
|
+
@safe_out_of_bound(def_val=False)
|
565
|
+
def _safely_move(_dx, _dy):
|
566
|
+
actions.move_by_offset(_dx, _dy).perform()
|
567
|
+
return True
|
568
|
+
|
569
|
+
suc_step_cnt = 0
|
570
|
+
total_step_cnt = 0
|
571
|
+
prev_x, prev_y = x1, y1
|
572
|
+
for x, y in path:
|
573
|
+
total_step_cnt += 1
|
574
|
+
|
575
|
+
dx, dy = x - prev_x, y - prev_y
|
576
|
+
logger.debug(f'Mouse moving: dx=<{dx}>, dy=<{dy}>.')
|
577
|
+
if abs(dx) < eps and abs(dy) < eps:
|
578
|
+
continue
|
579
|
+
|
580
|
+
move_suc = _safely_move(dx, dy)
|
581
|
+
suc_step_cnt += int(move_suc)
|
582
|
+
|
583
|
+
if not move_suc:
|
584
|
+
continue
|
585
|
+
|
586
|
+
human_mouse_move_latency()
|
587
|
+
prev_x, prev_y = x, y
|
588
|
+
|
589
|
+
return suc_step_cnt, total_step_cnt
|
590
|
+
|
591
|
+
|
592
|
+
@safe_timeout(def_val=False)
|
593
|
+
def wait_for_page_load(driver, timeout=30):
|
594
|
+
"""Waits for the page to fully load.
|
595
|
+
:param driver: The Selenium WebDriver instance.
|
596
|
+
:param timeout: Maximum time to wait.
|
597
|
+
:return: True if the page loaded successfully.
|
598
|
+
"""
|
599
|
+
WebDriverWait(driver, timeout).until(
|
600
|
+
expected_conditions.presence_of_element_located((By.TAG_NAME, "body"))
|
601
|
+
)
|
602
|
+
|
603
|
+
WebDriverWait(driver, timeout).until(
|
604
|
+
lambda d: d.execute_script("return document.readyState") == "complete"
|
605
|
+
)
|
606
|
+
|
607
|
+
return True
|
608
|
+
|
609
|
+
|
610
|
+
@safe_not_found(def_val=False)
|
611
|
+
@safe_stale(def_val=False)
|
612
|
+
def element_displayed_enabled(element: WebElement):
|
613
|
+
"""Checks if an element is displayed and enabled.
|
614
|
+
:param element: The web element to check.
|
615
|
+
:return: True if the element is displayed and enabled.
|
616
|
+
"""
|
617
|
+
return element.is_displayed() and element.is_enabled()
|
618
|
+
|
619
|
+
|
620
|
+
def find_available_elements_gtr(by, value, parent: WebElement) -> Generator[WebElement, Any, None]:
|
621
|
+
"""Yields available elements from a parent element.
|
622
|
+
:param by: The method to locate elements.
|
623
|
+
:param value: The locator value.
|
624
|
+
:param parent: The parent web element.
|
625
|
+
:return: Generator of available web elements.
|
626
|
+
"""
|
627
|
+
skipped_cnt = 0
|
628
|
+
for el in parent.find_elements(by, value):
|
629
|
+
if element_displayed_enabled(el):
|
630
|
+
yield el
|
631
|
+
else:
|
632
|
+
skipped_cnt += 1
|
633
|
+
if skipped_cnt:
|
634
|
+
logger.debug(f'Skipped <{skipped_cnt}> elements of <{value}> because they are not available.')
|
635
|
+
|
636
|
+
|
637
|
+
@safe_stale(def_val=[])
|
638
|
+
@safe_not_found(def_val=[])
|
639
|
+
def choose_confirmation_buttons(parent: WebElement, threshold=0.75, sort_similarity_desc=False) -> List[WebElement]:
|
640
|
+
"""Selects confirmation buttons based on text similarity.
|
641
|
+
:param parent: The parent web element.
|
642
|
+
:param threshold: Similarity threshold for button text.
|
643
|
+
:param sort_similarity_desc: Whether to sort by similarity.
|
644
|
+
:return: List of confirmation button elements.
|
645
|
+
"""
|
646
|
+
confirmation_buttons = []
|
647
|
+
confirmation_probs = []
|
648
|
+
for btn in find_available_elements_gtr(By.CSS_SELECTOR, "button, [role='button']", parent):
|
649
|
+
btn_text = btn.text
|
650
|
+
btn_confirmation_prob = is_confirmation_text(btn_text, threshold)
|
651
|
+
if btn_confirmation_prob is None:
|
652
|
+
logger.debug(f'Button <{btn_text}> was classified as not a confirmation button.')
|
653
|
+
continue
|
654
|
+
|
655
|
+
logger.debug(f'Found confirmation button <{btn.text}> with probability <{btn_confirmation_prob}>.')
|
656
|
+
confirmation_buttons.append(btn)
|
657
|
+
confirmation_probs.append(btn_confirmation_prob)
|
658
|
+
|
659
|
+
if sort_similarity_desc:
|
660
|
+
return [el for _, el in sorted(zip(confirmation_probs, confirmation_buttons), key=itemgetter(0), reverse=True)]
|
661
|
+
|
662
|
+
return confirmation_buttons
|