seleniumbase 4.41.3__py3-none-any.whl → 4.45.10__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.
Files changed (64) hide show
  1. sbase/steps.py +9 -0
  2. seleniumbase/__version__.py +1 -1
  3. seleniumbase/behave/behave_helper.py +2 -0
  4. seleniumbase/behave/behave_sb.py +21 -8
  5. seleniumbase/common/decorators.py +3 -1
  6. seleniumbase/console_scripts/run.py +1 -0
  7. seleniumbase/console_scripts/sb_caseplans.py +3 -4
  8. seleniumbase/console_scripts/sb_install.py +142 -11
  9. seleniumbase/console_scripts/sb_mkchart.py +1 -2
  10. seleniumbase/console_scripts/sb_mkdir.py +99 -29
  11. seleniumbase/console_scripts/sb_mkfile.py +1 -2
  12. seleniumbase/console_scripts/sb_mkpres.py +1 -2
  13. seleniumbase/console_scripts/sb_mkrec.py +26 -2
  14. seleniumbase/console_scripts/sb_objectify.py +4 -5
  15. seleniumbase/console_scripts/sb_print.py +1 -1
  16. seleniumbase/console_scripts/sb_recorder.py +40 -3
  17. seleniumbase/core/browser_launcher.py +474 -151
  18. seleniumbase/core/detect_b_ver.py +258 -16
  19. seleniumbase/core/log_helper.py +15 -21
  20. seleniumbase/core/mysql.py +1 -1
  21. seleniumbase/core/recorder_helper.py +3 -0
  22. seleniumbase/core/report_helper.py +9 -12
  23. seleniumbase/core/sb_cdp.py +734 -215
  24. seleniumbase/core/sb_driver.py +46 -5
  25. seleniumbase/core/session_helper.py +2 -4
  26. seleniumbase/core/tour_helper.py +1 -2
  27. seleniumbase/drivers/atlas_drivers/__init__.py +0 -0
  28. seleniumbase/drivers/brave_drivers/__init__.py +0 -0
  29. seleniumbase/drivers/chromium_drivers/__init__.py +0 -0
  30. seleniumbase/drivers/comet_drivers/__init__.py +0 -0
  31. seleniumbase/drivers/opera_drivers/__init__.py +0 -0
  32. seleniumbase/fixtures/base_case.py +448 -251
  33. seleniumbase/fixtures/constants.py +36 -9
  34. seleniumbase/fixtures/js_utils.py +77 -18
  35. seleniumbase/fixtures/page_actions.py +41 -13
  36. seleniumbase/fixtures/page_utils.py +19 -12
  37. seleniumbase/fixtures/shared_utils.py +64 -6
  38. seleniumbase/masterqa/master_qa.py +16 -2
  39. seleniumbase/plugins/base_plugin.py +8 -0
  40. seleniumbase/plugins/basic_test_info.py +2 -3
  41. seleniumbase/plugins/driver_manager.py +131 -5
  42. seleniumbase/plugins/page_source.py +2 -3
  43. seleniumbase/plugins/pytest_plugin.py +244 -79
  44. seleniumbase/plugins/sb_manager.py +143 -20
  45. seleniumbase/plugins/selenium_plugin.py +144 -12
  46. seleniumbase/translate/translator.py +2 -3
  47. seleniumbase/undetected/__init__.py +17 -13
  48. seleniumbase/undetected/cdp.py +1 -12
  49. seleniumbase/undetected/cdp_driver/browser.py +330 -129
  50. seleniumbase/undetected/cdp_driver/cdp_util.py +328 -61
  51. seleniumbase/undetected/cdp_driver/config.py +110 -14
  52. seleniumbase/undetected/cdp_driver/connection.py +18 -48
  53. seleniumbase/undetected/cdp_driver/element.py +105 -33
  54. seleniumbase/undetected/cdp_driver/tab.py +414 -39
  55. seleniumbase/utilities/selenium_grid/download_selenium_server.py +1 -1
  56. seleniumbase/utilities/selenium_grid/grid_hub.py +1 -2
  57. seleniumbase/utilities/selenium_grid/grid_node.py +2 -3
  58. seleniumbase/utilities/selenium_ide/convert_ide.py +2 -3
  59. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/METADATA +193 -166
  60. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/RECORD +64 -59
  61. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/licenses/LICENSE +1 -1
  62. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/WHEEL +0 -0
  63. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/entry_points.txt +0 -0
  64. {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,14 @@
1
1
  """Add CDP methods to extend the driver"""
2
2
  import asyncio
3
3
  import fasteners
4
+ import mycdp
4
5
  import os
6
+ import random
5
7
  import re
6
8
  import sys
7
9
  import time
8
10
  from contextlib import suppress
11
+ from filelock import FileLock
9
12
  from seleniumbase import config as sb_config
10
13
  from seleniumbase.config import settings
11
14
  from seleniumbase.fixtures import constants
@@ -13,6 +16,7 @@ from seleniumbase.fixtures import js_utils
13
16
  from seleniumbase.fixtures import page_utils
14
17
  from seleniumbase.fixtures import shared_utils
15
18
  from seleniumbase.undetected.cdp_driver import cdp_util
19
+ from seleniumbase.undetected.cdp_driver import tab as cdp_tab
16
20
 
17
21
 
18
22
  class CDPMethods():
@@ -63,6 +67,11 @@ class CDPMethods():
63
67
  )
64
68
  element.highlight_overlay = lambda: self.__highlight_overlay(element)
65
69
  element.mouse_click = lambda: self.__mouse_click(element)
70
+ element.click_with_offset = (
71
+ lambda *args, **kwargs: self.__mouse_click_with_offset_async(
72
+ element, *args, **kwargs
73
+ )
74
+ )
66
75
  element.mouse_drag = (
67
76
  lambda destination: self.__mouse_drag(element, destination)
68
77
  )
@@ -106,7 +115,18 @@ class CDPMethods():
106
115
  driver = self.driver
107
116
  if hasattr(driver, "cdp_base"):
108
117
  driver = driver.cdp_base
109
- self.loop.run_until_complete(self.page.get(url, **kwargs))
118
+ load_timeout = 60.0
119
+ wait_timeout = 30.0
120
+ if hasattr(sb_config, "_cdp_proxy") and sb_config._cdp_proxy:
121
+ load_timeout = 90.0
122
+ wait_timeout = 45.0
123
+ try:
124
+ task = self.page.get(url, **kwargs)
125
+ self.loop.run_until_complete(
126
+ asyncio.wait_for(task, timeout=load_timeout)
127
+ )
128
+ except asyncio.TimeoutError:
129
+ print("Timeout loading %s" % url)
110
130
  url_protocol = url.split(":")[0]
111
131
  safe_url = True
112
132
  if url_protocol not in ["about", "data", "chrome"]:
@@ -118,7 +138,14 @@ class CDPMethods():
118
138
  else:
119
139
  time.sleep(0.012)
120
140
  self.__slow_mode_pause_if_set()
121
- self.loop.run_until_complete(self.page.wait())
141
+ try:
142
+ self.loop.run_until_complete(
143
+ asyncio.wait_for(self.page.wait(), timeout=wait_timeout)
144
+ )
145
+ except asyncio.TimeoutError:
146
+ pass
147
+ except Exception:
148
+ pass
122
149
 
123
150
  def open(self, url, **kwargs):
124
151
  self.get(url, **kwargs)
@@ -137,6 +164,48 @@ class CDPMethods():
137
164
  def get_event_loop(self):
138
165
  return self.loop
139
166
 
167
+ def get_rd_host(self):
168
+ """Returns the remote-debugging host (likely 127.0.0.1)"""
169
+ driver = self.driver
170
+ if hasattr(driver, "cdp_base"):
171
+ driver = driver.cdp_base
172
+ return driver.config.host
173
+
174
+ def get_rd_port(self):
175
+ """Returns the remote-debugging port (commonly 9222)"""
176
+ driver = self.driver
177
+ if hasattr(driver, "cdp_base"):
178
+ driver = driver.cdp_base
179
+ return driver.config.port
180
+
181
+ def get_rd_url(self):
182
+ """Returns the remote-debugging URL, which is used for
183
+ allowing the Playwright integration to launch stealthy,
184
+ and also applies nest-asyncio for nested event loops so
185
+ that SeleniumBase methods can be called from Playwright
186
+ without encountering event loop error messages such as:
187
+ Cannot run the event loop while another loop is running.
188
+ Also sets an environment variable to hide this warning:
189
+ Deprecation: "url.parse() behavior is not standardized".
190
+ (github.com/microsoft/playwright-python/issues/3016)"""
191
+ import nest_asyncio
192
+ nest_asyncio.apply()
193
+ os.environ["NODE_NO_WARNINGS"] = "1"
194
+ driver = self.driver
195
+ if hasattr(driver, "cdp_base"):
196
+ driver = driver.cdp_base
197
+ host = driver.config.host
198
+ port = driver.config.port
199
+ return f"http://{host}:{port}"
200
+
201
+ def get_endpoint_url(self):
202
+ """Same as get_rd_url(), which returns the remote-debugging URL."""
203
+ return self.get_rd_url()
204
+
205
+ def get_port(self):
206
+ """Same as get_rd_port(), which returns the remote-debugging port."""
207
+ return self.get_rd_port()
208
+
140
209
  def add_handler(self, event, handler):
141
210
  self.page.add_handler(event, handler)
142
211
 
@@ -186,46 +255,23 @@ class CDPMethods():
186
255
  element with the given tag. (Eg: a, button, div, script, span)"""
187
256
  if not timeout:
188
257
  timeout = settings.SMALL_TIMEOUT
189
- self.__add_light_pause()
190
- time_now = time.time()
191
- self.assert_text(text, timeout=timeout)
192
- spent = int(time.time() - time_now)
193
- remaining = 1 + timeout - spent
194
- if tag_name:
195
- self.assert_element(tag_name, timeout=remaining)
196
- elements = self.loop.run_until_complete(
197
- self.page.find_elements_by_text(text=text)
198
- )
199
258
  if tag_name:
200
- tag_name = tag_name.lower().strip()
201
- for element in elements:
202
- if element and not tag_name:
203
- element = self.__add_sync_methods(element)
204
- return self.__add_sync_methods(element)
205
- elif (
206
- element
207
- and tag_name in element.tag_name.lower()
208
- and text.strip() in element.text
209
- ):
210
- element = self.__add_sync_methods(element)
211
- return self.__add_sync_methods(element)
212
- elif (
213
- element
214
- and element.parent
215
- and tag_name in element.parent.tag_name.lower()
216
- and text.strip() in element.parent.text
217
- ):
218
- element = self.__add_sync_methods(element.parent)
219
- return self.__add_sync_methods(element)
220
- elif (
221
- element
222
- and element.parent
223
- and element.parent.parent
224
- and tag_name in element.parent.parent.tag_name.lower()
225
- and text.strip() in element.parent.parent.text
226
- ):
227
- element = self.__add_sync_methods(element.parent.parent)
228
- return self.__add_sync_methods(element)
259
+ try:
260
+ return self.find_element(
261
+ '%s:contains("%s")' % (tag_name, text), timeout=timeout
262
+ )
263
+ except Exception:
264
+ pass # The exception will be raised later
265
+ else:
266
+ self.__add_light_pause()
267
+ self.assert_text(text, timeout=timeout)
268
+ elements = self.loop.run_until_complete(
269
+ self.page.find_elements_by_text(text=text)
270
+ )
271
+ for element in elements:
272
+ if element:
273
+ element = self.__add_sync_methods(element)
274
+ return self.__add_sync_methods(element)
229
275
  plural = "s"
230
276
  if timeout == 1:
231
277
  plural = ""
@@ -300,28 +346,7 @@ class CDPMethods():
300
346
  self.__add_light_pause()
301
347
  selector = self.__convert_to_css_if_xpath(selector)
302
348
  if (":contains(" in selector):
303
- tag_name = selector.split(":contains(")[0].split(" ")[-1]
304
- text = selector.split(":contains(")[1].split(")")[0][1:-1]
305
- with suppress(Exception):
306
- new_timeout = timeout
307
- if new_timeout < 1:
308
- new_timeout = 1
309
- self.loop.run_until_complete(
310
- self.page.select(tag_name, timeout=new_timeout)
311
- )
312
- self.loop.run_until_complete(
313
- self.page.find(text, timeout=new_timeout)
314
- )
315
- elements = self.find_elements_by_text(text, tag_name=tag_name)
316
- if not elements:
317
- plural = "s"
318
- if timeout == 1:
319
- plural = ""
320
- msg = "\n Element {%s} was not found after %s second%s!"
321
- message = msg % (selector, timeout, plural)
322
- raise Exception(message)
323
- element = self.__add_sync_methods(elements[0])
324
- return element
349
+ return self.find_element(selector, timeout=timeout)
325
350
  failure = False
326
351
  try:
327
352
  element = self.loop.run_until_complete(
@@ -490,6 +515,15 @@ class CDPMethods():
490
515
  self.loop.run_until_complete(self.page.wait())
491
516
  return result
492
517
 
518
+ def __mouse_click_with_offset_async(self, element, *args, **kwargs):
519
+ result = (
520
+ self.loop.run_until_complete(
521
+ element.mouse_click_with_offset_async(*args, **kwargs)
522
+ )
523
+ )
524
+ self.loop.run_until_complete(self.page.wait())
525
+ return result
526
+
493
527
  def __mouse_drag(self, element, destination):
494
528
  return (
495
529
  self.loop.run_until_complete(element.mouse_drag_async(destination))
@@ -508,7 +542,7 @@ class CDPMethods():
508
542
  text = text[:-1]
509
543
  for key in text:
510
544
  element.send_keys(key)
511
- time.sleep(0.044)
545
+ time.sleep(float(0.042 + (random.random() / 110.0)))
512
546
  if submit:
513
547
  element.send_keys("\r\n")
514
548
  time.sleep(0.044)
@@ -732,7 +766,21 @@ class CDPMethods():
732
766
  self.__slow_mode_pause_if_set()
733
767
  element = self.find_element(selector, timeout=timeout)
734
768
  element.scroll_into_view()
735
- element.click()
769
+ tag_name = element.tag_name
770
+ if tag_name:
771
+ tag_name = tag_name.lower().strip()
772
+ if (
773
+ tag_name in [
774
+ "a", "button", "canvas", "div", "input", "li", "span", "label"
775
+ ]
776
+ and "contains(" not in selector
777
+ ):
778
+ try:
779
+ element.mouse_click() # Simulated click (NOT PyAutoGUI)
780
+ except Exception:
781
+ element.click() # Standard CDP click
782
+ else:
783
+ element.click() # Standard CDP click
736
784
  self.__slow_mode_pause_if_set()
737
785
  self.loop.run_until_complete(self.page.wait())
738
786
 
@@ -778,7 +826,7 @@ class CDPMethods():
778
826
  element.scroll_into_view()
779
827
  element.click()
780
828
  click_count += 1
781
- time.sleep(0.042)
829
+ time.sleep(0.044)
782
830
  self.__slow_mode_pause_if_set()
783
831
  self.loop.run_until_complete(self.page.wait())
784
832
  except Exception:
@@ -823,6 +871,37 @@ class CDPMethods():
823
871
  % (dropdown_selector, option)
824
872
  )
825
873
 
874
+ def select_option_by_index(self, dropdown_selector, option):
875
+ element = self.find_element(dropdown_selector)
876
+ element.scroll_into_view()
877
+ options = element.query_selector_all("option")
878
+ count = 0
879
+ for found_option in options:
880
+ if count == int(option):
881
+ found_option.select_option()
882
+ return
883
+ count += 1
884
+ raise Exception(
885
+ "Unable to find index option {%s} in dropdown {%s}!"
886
+ % (dropdown_selector, option)
887
+ )
888
+
889
+ def select_option_by_value(self, dropdown_selector, option):
890
+ element = self.find_element(dropdown_selector)
891
+ element.scroll_into_view()
892
+ options = element.query_selector_all("option")
893
+ for found_option in options:
894
+ if (
895
+ "value" in found_option.attrs
896
+ and str(found_option.attrs["value"]) == str(option)
897
+ ):
898
+ found_option.select_option()
899
+ return
900
+ raise Exception(
901
+ "Unable to find value option {%s} in dropdown {%s}!"
902
+ % (dropdown_selector, option)
903
+ )
904
+
826
905
  def flash(
827
906
  self,
828
907
  selector, # The CSS Selector to flash
@@ -898,6 +977,12 @@ class CDPMethods():
898
977
  element.scroll_into_view()
899
978
  if text.endswith("\n") or text.endswith("\r"):
900
979
  text = text[:-1] + "\r\n"
980
+ elif (
981
+ element.tag_name == "textarea"
982
+ and "\n" in text
983
+ and "\r" not in text
984
+ ):
985
+ text = text.replace("\n", "\r")
901
986
  element.send_keys(text)
902
987
  self.__slow_mode_pause_if_set()
903
988
  self.loop.run_until_complete(self.page.sleep(0.025))
@@ -913,9 +998,15 @@ class CDPMethods():
913
998
  if text.endswith("\n") or text.endswith("\r"):
914
999
  submit = True
915
1000
  text = text[:-1]
1001
+ elif (
1002
+ element.tag_name == "textarea"
1003
+ and "\n" in text
1004
+ and "\r" not in text
1005
+ ):
1006
+ text = text.replace("\n", "\r")
916
1007
  for key in text:
917
1008
  element.send_keys(key)
918
- time.sleep(0.044)
1009
+ time.sleep(float(0.042 + (random.random() / 110.0)))
919
1010
  if submit:
920
1011
  element.send_keys("\r\n")
921
1012
  time.sleep(0.044)
@@ -933,6 +1024,12 @@ class CDPMethods():
933
1024
  element.clear_input()
934
1025
  if text.endswith("\n") or text.endswith("\r"):
935
1026
  text = text[:-1] + "\r\n"
1027
+ elif (
1028
+ element.tag_name == "textarea"
1029
+ and "\n" in text
1030
+ and "\r" not in text
1031
+ ):
1032
+ text = text.replace("\n", "\r")
936
1033
  element.send_keys(text)
937
1034
  self.__slow_mode_pause_if_set()
938
1035
  self.loop.run_until_complete(self.page.sleep(0.025))
@@ -1010,6 +1107,9 @@ class CDPMethods():
1010
1107
  ).strip()
1011
1108
  return self.loop.run_until_complete(self.page.evaluate(expression))
1012
1109
 
1110
+ def execute_script(self, expression):
1111
+ return self.evaluate(expression)
1112
+
1013
1113
  def js_dumps(self, obj_name):
1014
1114
  """Similar to evaluate(), but for dictionary results."""
1015
1115
  if obj_name.startswith("return "):
@@ -1017,12 +1117,19 @@ class CDPMethods():
1017
1117
  return self.loop.run_until_complete(self.page.js_dumps(obj_name))
1018
1118
 
1019
1119
  def maximize(self):
1020
- if self.get_window()[1].window_state.value == "maximized":
1021
- return
1022
- elif self.get_window()[1].window_state.value == "minimized":
1023
- self.loop.run_until_complete(self.page.maximize())
1024
- time.sleep(0.044)
1025
- return self.loop.run_until_complete(self.page.maximize())
1120
+ try:
1121
+ if self.get_window()[1].window_state.value == "maximized":
1122
+ return
1123
+ elif self.get_window()[1].window_state.value == "minimized":
1124
+ self.loop.run_until_complete(self.page.maximize())
1125
+ time.sleep(0.044)
1126
+ return self.loop.run_until_complete(self.page.maximize())
1127
+ except Exception:
1128
+ with suppress(Exception):
1129
+ width = self.evaluate("screen.availWidth;")
1130
+ height = self.evaluate("screen.availHeight;")
1131
+ self.__set_window_rect(0, 0, width, height)
1132
+ return
1026
1133
 
1027
1134
  def minimize(self):
1028
1135
  if self.get_window()[1].window_state.value != "minimized":
@@ -1034,24 +1141,42 @@ class CDPMethods():
1034
1141
  time.sleep(0.044)
1035
1142
  return self.loop.run_until_complete(self.page.medimize())
1036
1143
 
1037
- def set_window_rect(self, x, y, width, height):
1038
- if self.get_window()[1].window_state.value == "minimized":
1039
- self.loop.run_until_complete(
1144
+ def __set_window_rect(self, x, y, width, height, uc_lock=False):
1145
+ if uc_lock:
1146
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1147
+ with gui_lock:
1148
+ self.__make_sure_pyautogui_lock_is_writable()
1149
+ if self.get_window()[1].window_state.value == "minimized":
1150
+ self.loop.run_until_complete(
1151
+ self.page.set_window_size(
1152
+ left=x, top=y, width=width, height=height)
1153
+ )
1154
+ time.sleep(0.044)
1155
+ return self.loop.run_until_complete(
1156
+ self.page.set_window_size(
1157
+ left=x, top=y, width=width, height=height)
1158
+ )
1159
+ else:
1160
+ if self.get_window()[1].window_state.value == "minimized":
1161
+ self.loop.run_until_complete(
1162
+ self.page.set_window_size(
1163
+ left=x, top=y, width=width, height=height)
1164
+ )
1165
+ time.sleep(0.044)
1166
+ return self.loop.run_until_complete(
1040
1167
  self.page.set_window_size(
1041
1168
  left=x, top=y, width=width, height=height)
1042
1169
  )
1043
- time.sleep(0.044)
1044
- return self.loop.run_until_complete(
1045
- self.page.set_window_size(
1046
- left=x, top=y, width=width, height=height)
1047
- )
1170
+
1171
+ def set_window_rect(self, x, y, width, height):
1172
+ return self.__set_window_rect(x, y, width, height, uc_lock=True)
1048
1173
 
1049
1174
  def reset_window_size(self):
1050
1175
  x = settings.WINDOW_START_X
1051
1176
  y = settings.WINDOW_START_Y
1052
1177
  width = settings.CHROME_START_WIDTH
1053
1178
  height = settings.CHROME_START_HEIGHT
1054
- self.set_window_rect(x, y, width, height)
1179
+ self.__set_window_rect(x, y, width, height, uc_lock=True)
1055
1180
  self.__add_light_pause()
1056
1181
 
1057
1182
  def open_new_window(self, url=None, switch_to=True):
@@ -1063,10 +1188,74 @@ class CDPMethods():
1063
1188
  def switch_to_newest_window(self):
1064
1189
  self.switch_to_tab(-1)
1065
1190
 
1066
- def open_new_tab(self, url=None, switch_to=True):
1191
+ def open_new_tab(self, url=None, switch_to=True, **kwargs):
1192
+ driver = self.driver
1067
1193
  if not isinstance(url, str):
1068
1194
  url = "about:blank"
1069
- self.loop.run_until_complete(self.page.get(url, new_tab=True))
1195
+ if hasattr(driver, "cdp_base"):
1196
+ try:
1197
+ self.loop.run_until_complete(
1198
+ self.page.get(url, new_tab=True, **kwargs)
1199
+ )
1200
+ except Exception:
1201
+ original_targets = self.loop.run_until_complete(
1202
+ self.page.send(mycdp.target.get_targets())
1203
+ )
1204
+ tab_url = driver.cdp_base.tabs[0].websocket_url
1205
+ if not self.driver.is_connected():
1206
+ self.driver.connect()
1207
+ self.driver.open_new_tab()
1208
+ targets = self.loop.run_until_complete(
1209
+ self.page.send(mycdp.target.get_targets())
1210
+ )
1211
+ new_targets = []
1212
+ for target in targets:
1213
+ if target not in original_targets:
1214
+ new_targets.append(target)
1215
+ if new_targets:
1216
+ found_target = new_targets[0]
1217
+ t_str = str(new_targets[0])
1218
+ target_id = (
1219
+ t_str.split("target_id=TargetID('")[-1].split("')")[0]
1220
+ )
1221
+ pre_tab_url = tab_url.split("/page/")[0] + "/page/"
1222
+ new_tab_url = pre_tab_url + target_id
1223
+ new_tab = cdp_tab.Tab(
1224
+ new_tab_url, found_target, driver.cdp_base
1225
+ )
1226
+ driver.cdp_base.targets.append(new_tab)
1227
+ driver.cdp_base.tabs.append(new_tab)
1228
+ self.driver.disconnect()
1229
+ self.switch_to_newest_tab()
1230
+ self.open(url)
1231
+ return
1232
+ elif getattr(sb_config, "guest_mode", None):
1233
+ print(" open_new_tab() failed! (Known Guest Mode issue)")
1234
+ if switch_to:
1235
+ self.switch_to_newest_tab()
1236
+ return
1237
+
1238
+ target_id = self.loop.run_until_complete(
1239
+ self.page.send(mycdp.target.create_target(url))
1240
+ )
1241
+ if not target_id and getattr(sb_config, "guest_mode", None):
1242
+ print(" open_new_tab() failed! (Known Guest Mode issue)")
1243
+ found_target = None
1244
+ targets = self.loop.run_until_complete(
1245
+ self.page.send(mycdp.target.get_targets())
1246
+ )
1247
+ if target_id:
1248
+ for target in targets:
1249
+ if str(target_id) in str(target):
1250
+ found_target = target
1251
+ break
1252
+ if found_target:
1253
+ tab_url = driver.tabs[0].websocket_url
1254
+ pre_tab_url = tab_url.split("/page/")[0] + "/page/"
1255
+ new_tab_url = pre_tab_url + target_id
1256
+ new_tab = cdp_tab.Tab(new_tab_url, found_target, driver)
1257
+ driver.targets.append(new_tab)
1258
+ driver.tabs.append(new_tab)
1070
1259
  if switch_to:
1071
1260
  self.switch_to_newest_tab()
1072
1261
 
@@ -1128,7 +1317,14 @@ class CDPMethods():
1128
1317
  self.page.evaluate("window.location.origin")
1129
1318
  )
1130
1319
 
1131
- def get_page_source(self):
1320
+ def get_html(self, include_shadow_dom=True):
1321
+ return self.get_page_source(
1322
+ include_shadow_dom=include_shadow_dom
1323
+ )
1324
+
1325
+ def get_page_source(self, include_shadow_dom=True):
1326
+ if include_shadow_dom:
1327
+ return self.find_element("html").get_html()
1132
1328
  try:
1133
1329
  source = self.loop.run_until_complete(
1134
1330
  self.page.evaluate("document.documentElement.outerHTML")
@@ -1303,6 +1499,17 @@ class CDPMethods():
1303
1499
  x = window_rect["x"] + element_rect["x"]
1304
1500
  y = w_bottom_y - viewport_height + element_rect["y"]
1305
1501
  y_scroll_offset = window_rect["pageYOffset"]
1502
+ if (
1503
+ hasattr(sb_config, "_cdp_browser")
1504
+ and sb_config._cdp_browser == "opera"
1505
+ ):
1506
+ # Handle special case where Opera side panel shifts coordinates
1507
+ x_offset = window_rect["outerWidth"] - window_rect["innerWidth"]
1508
+ if x_offset > 56:
1509
+ x_offset = 56
1510
+ elif x_offset < 22:
1511
+ x_offset = 0
1512
+ x = x + x_offset
1306
1513
  y = y - y_scroll_offset
1307
1514
  x = x + window_rect["scrollX"]
1308
1515
  y = y + window_rect["scrollY"]
@@ -1364,6 +1571,75 @@ class CDPMethods():
1364
1571
  )
1365
1572
  )
1366
1573
 
1574
+ def get_mfa_code(self, totp_key=None):
1575
+ """Returns a time-based one-time password based on the Google
1576
+ Authenticator algorithm for multi-factor authentication."""
1577
+ return shared_utils.get_mfa_code(totp_key)
1578
+
1579
+ def enter_mfa_code(self, selector, totp_key=None, timeout=None):
1580
+ if not timeout:
1581
+ timeout = settings.SMALL_TIMEOUT
1582
+ mfa_code = self.get_mfa_code(totp_key)
1583
+ self.type(selector, mfa_code + "\n", timeout=timeout)
1584
+
1585
+ def activate_messenger(self):
1586
+ js_utils.activate_messenger(self)
1587
+ self.__add_light_pause()
1588
+
1589
+ def set_messenger_theme(
1590
+ self, theme="default", location="default", max_messages="default"
1591
+ ):
1592
+ """Sets a theme for posting messages.
1593
+ Themes: ["flat", "future", "block", "air", "ice"]
1594
+ Locations: ["top_left", "top_center", "top_right",
1595
+ "bottom_left", "bottom_center", "bottom_right"]
1596
+ max_messages: The limit of concurrent messages to display."""
1597
+ if not theme:
1598
+ theme = "default" # "flat"
1599
+ if not location:
1600
+ location = "default" # "bottom_right"
1601
+ if not max_messages:
1602
+ max_messages = "default" # "8"
1603
+ else:
1604
+ max_messages = str(max_messages) # Value must be in string format
1605
+ js_utils.set_messenger_theme(
1606
+ self,
1607
+ theme=theme,
1608
+ location=location,
1609
+ max_messages=max_messages,
1610
+ )
1611
+ self.__add_light_pause()
1612
+
1613
+ def post_message(self, message, duration=None, pause=True, style="info"):
1614
+ """Post a message on the screen with Messenger.
1615
+ Arguments:
1616
+ message: The message to display.
1617
+ duration: The time until the message vanishes. (Default: 2.55s)
1618
+ pause: If True, the program waits until the message completes.
1619
+ style: "info", "success", or "error"."""
1620
+ driver = self.driver
1621
+ if hasattr(driver, "cdp_base"):
1622
+ driver = driver.cdp_base
1623
+ if style not in ["info", "success", "error"]:
1624
+ style = "info"
1625
+ if not duration:
1626
+ duration = settings.DEFAULT_MESSAGE_DURATION
1627
+ if (
1628
+ (
1629
+ driver.config.headless
1630
+ or (hasattr(sb_config, "xvfb") and sb_config.xvfb)
1631
+ )
1632
+ and float(duration) > 0.75
1633
+ ):
1634
+ duration = 0.75
1635
+ try:
1636
+ js_utils.post_message(self, message, duration, style=style)
1637
+ except Exception:
1638
+ print(" * %s message: %s" % (style.upper(), message))
1639
+ if pause:
1640
+ duration = float(duration) + 0.15
1641
+ time.sleep(float(duration))
1642
+
1367
1643
  def set_locale(self, locale):
1368
1644
  """(Settings will take effect on the next page load)"""
1369
1645
  self.loop.run_until_complete(self.page.set_locale(locale))
@@ -1390,17 +1666,39 @@ class CDPMethods():
1390
1666
  css_selector = self.__convert_to_css_if_xpath(selector)
1391
1667
  css_selector = re.escape(css_selector) # Add "\\" to special chars
1392
1668
  css_selector = js_utils.escape_quotes_if_needed(css_selector)
1393
- js_code = """var $elements = document.querySelectorAll('%s');
1394
- var index = 0, length = $elements.length;
1395
- for(; index < length; index++){
1396
- $elements[index].setAttribute('%s','%s');}""" % (
1397
- css_selector,
1398
- attribute,
1399
- value,
1669
+ js_code = (
1670
+ """var $elements = document.querySelectorAll('%s');
1671
+ var index = 0, length = $elements.length;
1672
+ for(; index < length; index++){
1673
+ $elements[index].setAttribute('%s','%s');}""" % (
1674
+ css_selector,
1675
+ attribute,
1676
+ value,
1677
+ )
1400
1678
  )
1401
1679
  with suppress(Exception):
1402
1680
  self.loop.run_until_complete(self.page.evaluate(js_code))
1403
1681
 
1682
+ def is_attribute_present(self, selector, attribute, value=None):
1683
+ try:
1684
+ element = self.find_element(selector, timeout=0.1)
1685
+ found_value = element.get_attribute(attribute)
1686
+ if found_value is None:
1687
+ return False
1688
+ if value is not None:
1689
+ if found_value == value:
1690
+ return True
1691
+ else:
1692
+ return False
1693
+ else:
1694
+ return True
1695
+ except Exception:
1696
+ return False
1697
+
1698
+ def is_online(self):
1699
+ js_code = "navigator.onLine;"
1700
+ return self.loop.run_until_complete(self.page.evaluate(js_code))
1701
+
1404
1702
  def __make_sure_pyautogui_lock_is_writable(self):
1405
1703
  with suppress(Exception):
1406
1704
  shared_utils.make_writable(constants.MultiBrowser.PYAUTOGUILOCK)
@@ -1431,23 +1729,24 @@ class CDPMethods():
1431
1729
  import pyautogui
1432
1730
  with suppress(Exception):
1433
1731
  use_pyautogui_ver = constants.PyAutoGUI.VER
1434
- if pyautogui.__version__ != use_pyautogui_ver:
1435
- del pyautogui
1436
- shared_utils.pip_install(
1437
- "pyautogui", version=use_pyautogui_ver
1438
- )
1732
+ u_pv = shared_utils.make_version_tuple(use_pyautogui_ver)
1733
+ pv = shared_utils.make_version_tuple(pyautogui.__version__)
1734
+ if pv < u_pv:
1735
+ del pyautogui # To get newer ver
1736
+ shared_utils.pip_install("pyautogui", version="Latest")
1439
1737
  import pyautogui
1440
1738
  except Exception:
1441
1739
  print("\nPyAutoGUI required! Installing now...")
1442
- shared_utils.pip_install(
1443
- "pyautogui", version=constants.PyAutoGUI.VER
1444
- )
1740
+ shared_utils.pip_install("pyautogui", version="Latest")
1445
1741
  try:
1446
1742
  import pyautogui
1447
1743
  except Exception:
1448
1744
  if (
1449
1745
  shared_utils.is_linux()
1450
- and (not sb_config.headed or sb_config.xvfb)
1746
+ and (
1747
+ not sb_config.headed
1748
+ or (hasattr(sb_config, "xvfb") and sb_config.xvfb)
1749
+ )
1451
1750
  and not driver.config.headless
1452
1751
  and (
1453
1752
  not hasattr(sb_config, "_virtual_display")
@@ -1517,9 +1816,7 @@ class CDPMethods():
1517
1816
  self.__install_pyautogui_if_missing()
1518
1817
  import pyautogui
1519
1818
  pyautogui = self.__get_configured_pyautogui(pyautogui)
1520
- gui_lock = fasteners.InterProcessLock(
1521
- constants.MultiBrowser.PYAUTOGUILOCK
1522
- )
1819
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1523
1820
  with gui_lock:
1524
1821
  self.__make_sure_pyautogui_lock_is_writable()
1525
1822
  pyautogui.press(key)
@@ -1531,14 +1828,12 @@ class CDPMethods():
1531
1828
  self.__install_pyautogui_if_missing()
1532
1829
  import pyautogui
1533
1830
  pyautogui = self.__get_configured_pyautogui(pyautogui)
1534
- gui_lock = fasteners.InterProcessLock(
1535
- constants.MultiBrowser.PYAUTOGUILOCK
1536
- )
1831
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1537
1832
  with gui_lock:
1538
1833
  self.__make_sure_pyautogui_lock_is_writable()
1539
1834
  for key in keys:
1540
1835
  pyautogui.press(key)
1541
- time.sleep(0.044)
1836
+ time.sleep(float(0.042 + (random.random() / 110.0)))
1542
1837
  self.__slow_mode_pause_if_set()
1543
1838
  self.loop.run_until_complete(self.page.sleep(0.025))
1544
1839
 
@@ -1546,9 +1841,7 @@ class CDPMethods():
1546
1841
  self.__install_pyautogui_if_missing()
1547
1842
  import pyautogui
1548
1843
  pyautogui = self.__get_configured_pyautogui(pyautogui)
1549
- gui_lock = fasteners.InterProcessLock(
1550
- constants.MultiBrowser.PYAUTOGUILOCK
1551
- )
1844
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1552
1845
  with gui_lock:
1553
1846
  self.__make_sure_pyautogui_lock_is_writable()
1554
1847
  pyautogui.write(text)
@@ -1567,9 +1860,7 @@ class CDPMethods():
1567
1860
  % (x, y, screen_width, screen_height)
1568
1861
  )
1569
1862
  if uc_lock:
1570
- gui_lock = fasteners.InterProcessLock(
1571
- constants.MultiBrowser.PYAUTOGUILOCK
1572
- )
1863
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1573
1864
  with gui_lock: # Prevent issues with multiple processes
1574
1865
  self.__make_sure_pyautogui_lock_is_writable()
1575
1866
  pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
@@ -1588,9 +1879,7 @@ class CDPMethods():
1588
1879
  pyautogui.click(x=x, y=y)
1589
1880
 
1590
1881
  def gui_click_x_y(self, x, y, timeframe=0.25):
1591
- gui_lock = fasteners.InterProcessLock(
1592
- constants.MultiBrowser.PYAUTOGUILOCK
1593
- )
1882
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1594
1883
  with gui_lock: # Prevent issues with multiple processes
1595
1884
  self.__make_sure_pyautogui_lock_is_writable()
1596
1885
  self.__install_pyautogui_if_missing()
@@ -1604,20 +1893,20 @@ class CDPMethods():
1604
1893
  win_x = window_rect["x"]
1605
1894
  win_y = window_rect["y"]
1606
1895
  scr_width = pyautogui.size().width
1607
- self.maximize()
1608
- self.__add_light_pause()
1609
- win_width = self.get_window_size()["width"]
1896
+ win_width = self.evaluate("screen.availWidth;")
1610
1897
  width_ratio = round(float(scr_width) / float(win_width), 2)
1611
1898
  width_ratio += 0.01
1612
1899
  if width_ratio < 0.45 or width_ratio > 2.55:
1613
1900
  width_ratio = 1.01
1614
1901
  sb_config._saved_width_ratio = width_ratio
1615
- self.minimize()
1616
- self.__add_light_pause()
1617
- self.set_window_rect(win_x, win_y, width, height)
1618
- self.__add_light_pause()
1619
- x = x * width_ratio
1620
- y = y * width_ratio
1902
+ with suppress(Exception):
1903
+ self.get_window() # If this fails, skip the rest
1904
+ self.minimize()
1905
+ self.__add_light_pause()
1906
+ self.__set_window_rect(win_x, win_y, width, height)
1907
+ self.__add_light_pause()
1908
+ x = x * (width_ratio + 0.03)
1909
+ y = y * (width_ratio - 0.03)
1621
1910
  self.bring_active_window_to_front()
1622
1911
  self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False)
1623
1912
 
@@ -1629,78 +1918,181 @@ class CDPMethods():
1629
1918
  self.__slow_mode_pause_if_set()
1630
1919
  self.loop.run_until_complete(self.page.wait())
1631
1920
 
1632
- def _on_a_cf_turnstile_page(self):
1633
- source = self.get_page_source()
1921
+ def gui_click_with_offset(
1922
+ self, selector, x, y, timeframe=0.25, center=False
1923
+ ):
1924
+ """Click an element at an {X,Y}-offset location.
1925
+ {0,0} is the top-left corner of the element.
1926
+ If center==True, {0,0} becomes the center of the element.
1927
+ The timeframe is the time spent moving the mouse."""
1928
+ if center:
1929
+ px, py = self.get_gui_element_center(selector)
1930
+ self.gui_click_x_y(px + x, py + y, timeframe=timeframe)
1931
+ else:
1932
+ element_rect = self.get_gui_element_rect(selector)
1933
+ px = element_rect["x"]
1934
+ py = element_rect["y"]
1935
+ self.gui_click_x_y(px + x, py + y, timeframe=timeframe)
1936
+
1937
+ def click_with_offset(self, selector, x, y, center=False):
1938
+ element = self.find_element(selector)
1939
+ element.scroll_into_view()
1940
+ if "--debug" in sys.argv:
1941
+ displayed_selector = "`%s`" % selector
1942
+ if '"' not in selector:
1943
+ displayed_selector = '"%s"' % selector
1944
+ elif "'" not in selector:
1945
+ displayed_selector = "'%s'" % selector
1946
+ print(
1947
+ " <DEBUG> sb.click_with_offset(%s, %s, %s, center=%s)"
1948
+ % (displayed_selector, x, y, center)
1949
+ )
1950
+ element.click_with_offset(x=x, y=y, center=center)
1951
+ self.__slow_mode_pause_if_set()
1952
+ self.loop.run_until_complete(self.page.wait())
1953
+
1954
+ def _on_a_cf_turnstile_page(self, source=None):
1955
+ if not source or len(source) < 400:
1956
+ time.sleep(0.2)
1957
+ source = self.get_page_source()
1634
1958
  if (
1635
- 'data-callback="onCaptchaSuccess"' in source
1636
- or "/challenge-platform/scripts/" in source
1959
+ (
1960
+ 'data-callback="onCaptchaSuccess"' in source
1961
+ and 'title="reCAPTCHA"' not in source
1962
+ and 'id="recaptcha-token"' not in source
1963
+ )
1964
+ or "/challenge-platform/h/b/" in source
1637
1965
  or 'id="challenge-widget-' in source
1966
+ or "challenges.cloudf" in source
1638
1967
  or "cf-turnstile-" in source
1639
1968
  ):
1640
1969
  return True
1641
1970
  return False
1642
1971
 
1972
+ def _on_a_g_recaptcha_page(self, *args, **kwargs):
1973
+ time.sleep(0.4) # reCAPTCHA may need a moment to appear
1974
+ self.loop.run_until_complete(self.page.wait())
1975
+ source = self.get_page_source()
1976
+ if (
1977
+ (
1978
+ 'id="recaptcha-token"' in source
1979
+ or 'title="reCAPTCHA"' in source
1980
+ )
1981
+ and self.is_element_visible('iframe[title="reCAPTCHA"]')
1982
+ ):
1983
+ try:
1984
+ self.loop.run_until_complete(self.page.wait(0.1))
1985
+ except Exception:
1986
+ time.sleep(0.1)
1987
+ return True
1988
+ elif "com/recaptcha/api.js" in source:
1989
+ time.sleep(1.6) # Still loading
1990
+ try:
1991
+ self.loop.run_until_complete(self.page.wait(0.1))
1992
+ except Exception:
1993
+ time.sleep(0.1)
1994
+ return True
1995
+ return False
1996
+
1997
+ def __gui_click_recaptcha(self, use_cdp=False):
1998
+ selector = None
1999
+ if self.is_element_visible('iframe[title="reCAPTCHA"]'):
2000
+ selector = 'iframe[title="reCAPTCHA"]'
2001
+ else:
2002
+ return
2003
+ time.sleep(0.25)
2004
+ self.loop.run_until_complete(self.page.wait())
2005
+ time.sleep(0.25)
2006
+ with suppress(Exception):
2007
+ element_rect = self.get_gui_element_rect(selector, timeout=1)
2008
+ e_x = element_rect["x"]
2009
+ e_y = element_rect["y"]
2010
+ x_offset = 26
2011
+ y_offset = 35
2012
+ if shared_utils.is_windows():
2013
+ x_offset = 29
2014
+ x = e_x + x_offset
2015
+ y = e_y + y_offset
2016
+ sb_config._saved_cf_x_y = (x, y)
2017
+ time.sleep(0.08)
2018
+ if use_cdp:
2019
+ self.sleep(0.03)
2020
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
2021
+ with gui_lock: # Prevent issues with multiple processes
2022
+ self.bring_active_window_to_front()
2023
+ time.sleep(0.056)
2024
+ self.click_with_offset(selector, x_offset, y_offset)
2025
+ time.sleep(0.056)
2026
+ else:
2027
+ self.gui_click_x_y(x, y)
2028
+
2029
+ def solve_captcha(self):
2030
+ self.__click_captcha(use_cdp=True)
2031
+
2032
+ def click_captcha(self):
2033
+ """Same as solve_captcha()"""
2034
+ self.__click_captcha(use_cdp=True)
2035
+
1643
2036
  def gui_click_captcha(self):
1644
- if not self._on_a_cf_turnstile_page():
2037
+ """Use PyAutoGUI to click the CAPTCHA"""
2038
+ self.__click_captcha(use_cdp=False)
2039
+
2040
+ def __click_captcha(self, use_cdp=False):
2041
+ """Uses PyAutoGUI unless use_cdp == True"""
2042
+ self.sleep(0.075)
2043
+ self.loop.run_until_complete(self.page.wait())
2044
+ self.sleep(0.025)
2045
+ source = self.get_page_source()
2046
+ if self._on_a_cf_turnstile_page(source):
2047
+ pass
2048
+ elif self._on_a_g_recaptcha_page(source):
2049
+ self.__gui_click_recaptcha(use_cdp)
2050
+ return
2051
+ else:
1645
2052
  return
1646
2053
  selector = None
1647
- if (
1648
- self.is_element_present('[name*="cf-turnstile-"]')
1649
- and self.is_element_present("#challenge-form div > div")
1650
- ):
2054
+ if self.is_element_present('[class="cf-turnstile"]'):
2055
+ selector = '[class="cf-turnstile"]'
2056
+ elif self.is_element_present("#challenge-form div > div"):
1651
2057
  selector = "#challenge-form div > div"
1652
- elif (
1653
- self.is_element_present('[name*="cf-turnstile-"]')
1654
- and self.is_element_present(
1655
- '[style="display: grid;"] div div'
1656
- )
1657
- ):
2058
+ elif self.is_element_present('[style="display: grid;"] div div'):
1658
2059
  selector = '[style="display: grid;"] div div'
1659
- elif (
1660
- self.is_element_present('[name*="cf-turnstile-"]')
1661
- and self.is_element_present("[class*=spacer] + div div")
1662
- ):
2060
+ elif self.is_element_present("[class*=spacer] + div div"):
1663
2061
  selector = '[class*=spacer] + div div'
1664
- elif (
1665
- self.is_element_present('[name*="cf-turnstile-"]')
1666
- and self.is_element_present("div.spacer div")
1667
- ):
1668
- selector = "div.spacer div"
1669
- elif (
1670
- self.is_element_present('script[src*="challenges.c"]')
1671
- and self.is_element_present(
1672
- '[data-testid*="challenge-"] div'
1673
- )
1674
- ):
2062
+ elif self.is_element_present(".spacer div:not([class])"):
2063
+ selector = ".spacer div:not([class])"
2064
+ elif self.is_element_present('[data-testid*="challenge-"] div'):
1675
2065
  selector = '[data-testid*="challenge-"] div'
1676
- elif self.is_element_present(
1677
- "div#turnstile-widget div:not([class])"
1678
- ):
2066
+ elif self.is_element_present("div#turnstile-widget div:not([class])"):
1679
2067
  selector = "div#turnstile-widget div:not([class])"
2068
+ elif self.is_element_present("ngx-turnstile div:not([class])"):
2069
+ selector = "ngx-turnstile div:not([class])"
1680
2070
  elif self.is_element_present(
1681
2071
  'form div:not([class]):has(input[name*="cf-turn"])'
1682
2072
  ):
1683
2073
  selector = 'form div:not([class]):has(input[name*="cf-turn"])'
1684
- elif (
1685
- self.is_element_present('[src*="/turnstile/"]')
1686
- and self.is_element_present("form div:not(:has(*))")
1687
- ):
2074
+ elif self.is_element_present("form div:not(:has(*))"):
1688
2075
  selector = "form div:not(:has(*))"
1689
- elif (
1690
- self.is_element_present('[src*="/turnstile/"]')
1691
- and self.is_element_present(
1692
- "body > div#check > div:not([class])"
1693
- )
1694
- ):
2076
+ elif self.is_element_present("body > div#check > div:not([class])"):
1695
2077
  selector = "body > div#check > div:not([class])"
1696
2078
  elif self.is_element_present(".cf-turnstile-wrapper"):
1697
2079
  selector = ".cf-turnstile-wrapper"
1698
- elif self.is_element_present('[class="cf-turnstile"]'):
1699
- selector = '[class="cf-turnstile"]'
2080
+ elif self.is_element_present(
2081
+ '[id*="turnstile"] div:not([class])'
2082
+ ):
2083
+ selector = '[id*="turnstile"] div:not([class])'
2084
+ elif self.is_element_present(
2085
+ '[class*="turnstile"] div:not([class])'
2086
+ ):
2087
+ selector = '[class*="turnstile"] div:not([class])'
1700
2088
  elif self.is_element_present(
1701
2089
  '[data-callback="onCaptchaSuccess"]'
1702
2090
  ):
1703
2091
  selector = '[data-callback="onCaptchaSuccess"]'
2092
+ elif self.is_element_present(
2093
+ "div:not([class]) > div:not([class])"
2094
+ ):
2095
+ selector = "div:not([class]) > div:not([class])"
1704
2096
  else:
1705
2097
  return
1706
2098
  if not selector:
@@ -1748,9 +2140,11 @@ class CDPMethods():
1748
2140
  self.loop.run_until_complete(self.page.evaluate(script))
1749
2141
  self.loop.run_until_complete(self.page.wait())
1750
2142
  elif (
1751
- self.is_element_present("form")
1752
- and self.is_element_present(
1753
- 'form [id*="turnstile"] > div:not([class])'
2143
+ self.is_element_present(
2144
+ 'form [id*="turnstile"] div:not([class])'
2145
+ )
2146
+ or self.is_element_present(
2147
+ 'form [class*="turnstile"] div:not([class])'
1754
2148
  )
1755
2149
  ):
1756
2150
  script = (
@@ -1758,22 +2152,58 @@ class CDPMethods():
1758
2152
  'form [id*="turnstile"]');
1759
2153
  var index = 0, length = $elements.length;
1760
2154
  for(; index < length; index++){
2155
+ $elements[index].setAttribute('align', 'left');}
2156
+ var $elements = document.querySelectorAll(
2157
+ 'form [class*="turnstile"]');
2158
+ var index = 0, length = $elements.length;
2159
+ for(; index < length; index++){
1761
2160
  $elements[index].setAttribute('align', 'left');}"""
1762
2161
  )
1763
2162
  with suppress(Exception):
1764
2163
  self.loop.run_until_complete(self.page.evaluate(script))
1765
2164
  self.loop.run_until_complete(self.page.wait())
2165
+ elif (
2166
+ self.is_element_present(
2167
+ '[style*="text-align: center;"] div:not([class])'
2168
+ )
2169
+ ):
2170
+ script = (
2171
+ """var $elements = document.querySelectorAll(
2172
+ '[style*="text-align: center;"]');
2173
+ var index = 0, length = $elements.length;
2174
+ for(; index < length; index++){
2175
+ the_style = $elements[index].getAttribute('style');
2176
+ new_style = the_style.replaceAll('center', 'left');
2177
+ $elements[index].setAttribute('style', new_style);}"""
2178
+ )
2179
+ with suppress(Exception):
2180
+ self.loop.run_until_complete(self.page.evaluate(script))
2181
+ self.loop.run_until_complete(self.page.wait())
1766
2182
  with suppress(Exception):
2183
+ time.sleep(0.05)
1767
2184
  element_rect = self.get_gui_element_rect(selector, timeout=1)
1768
2185
  e_x = element_rect["x"]
1769
2186
  e_y = element_rect["y"]
1770
- x = e_x + 32
1771
- if not shared_utils.is_windows():
1772
- y = e_y + 32
1773
- else:
1774
- y = e_y + 22
2187
+ x_offset = 32
2188
+ y_offset = 32
2189
+ if shared_utils.is_windows():
2190
+ y_offset = 28
2191
+ x = e_x + x_offset
2192
+ y = e_y + y_offset
1775
2193
  sb_config._saved_cf_x_y = (x, y)
1776
- self.gui_click_x_y(x, y)
2194
+ time.sleep(0.05)
2195
+ if hasattr(sb_config, "_cdp_proxy") and sb_config._cdp_proxy:
2196
+ time.sleep(0.22) # CAPTCHA may load slower with proxy
2197
+ if use_cdp:
2198
+ self.sleep(0.03)
2199
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
2200
+ with gui_lock: # Prevent issues with multiple processes
2201
+ self.bring_active_window_to_front()
2202
+ time.sleep(0.05)
2203
+ self.click_with_offset(selector, x_offset, y_offset)
2204
+ time.sleep(0.05)
2205
+ else:
2206
+ self.gui_click_x_y(x, y)
1777
2207
 
1778
2208
  def __gui_drag_drop(self, x1, y1, x2, y2, timeframe=0.25, uc_lock=False):
1779
2209
  self.__install_pyautogui_if_missing()
@@ -1793,17 +2223,19 @@ class CDPMethods():
1793
2223
  % (x2, y2, screen_width, screen_height)
1794
2224
  )
1795
2225
  if uc_lock:
1796
- gui_lock = fasteners.InterProcessLock(
1797
- constants.MultiBrowser.PYAUTOGUILOCK
1798
- )
2226
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1799
2227
  with gui_lock: # Prevent issues with multiple processes
2228
+ if "--debug" in sys.argv:
2229
+ print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x1, y1))
1800
2230
  pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad)
1801
2231
  self.__add_light_pause()
1802
2232
  if "--debug" in sys.argv:
1803
- print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x1, y1))
2233
+ print(" <DEBUG> pyautogui.dragTo(%s, %s)" % (x2, y2))
1804
2234
  pyautogui.dragTo(x2, y2, button="left", duration=timeframe)
1805
2235
  else:
1806
2236
  # Called from a method where the gui_lock is already active
2237
+ if "--debug" in sys.argv:
2238
+ print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x1, y1))
1807
2239
  pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad)
1808
2240
  self.__add_light_pause()
1809
2241
  if "--debug" in sys.argv:
@@ -1813,9 +2245,7 @@ class CDPMethods():
1813
2245
  def gui_drag_drop_points(self, x1, y1, x2, y2, timeframe=0.35):
1814
2246
  """Use PyAutoGUI to drag-and-drop from one point to another.
1815
2247
  Can simulate click-and-hold when using the same point twice."""
1816
- gui_lock = fasteners.InterProcessLock(
1817
- constants.MultiBrowser.PYAUTOGUILOCK
1818
- )
2248
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1819
2249
  with gui_lock: # Prevent issues with multiple processes
1820
2250
  self.__install_pyautogui_if_missing()
1821
2251
  import pyautogui
@@ -1828,18 +2258,18 @@ class CDPMethods():
1828
2258
  win_x = window_rect["x"]
1829
2259
  win_y = window_rect["y"]
1830
2260
  scr_width = pyautogui.size().width
1831
- self.maximize()
1832
- self.__add_light_pause()
1833
- win_width = self.get_window_size()["width"]
2261
+ win_width = self.evaluate("screen.availWidth;")
1834
2262
  width_ratio = round(float(scr_width) / float(win_width), 2)
1835
2263
  width_ratio += 0.01
1836
2264
  if width_ratio < 0.45 or width_ratio > 2.55:
1837
2265
  width_ratio = 1.01
1838
2266
  sb_config._saved_width_ratio = width_ratio
1839
- self.minimize()
1840
- self.__add_light_pause()
1841
- self.set_window_rect(win_x, win_y, width, height)
1842
- self.__add_light_pause()
2267
+ with suppress(Exception):
2268
+ self.get_window() # If this fails, skip the rest
2269
+ self.minimize()
2270
+ self.__add_light_pause()
2271
+ self.__set_window_rect(win_x, win_y, width, height)
2272
+ self.__add_light_pause()
1843
2273
  x1 = x1 * width_ratio
1844
2274
  y1 = y1 * (width_ratio - 0.02)
1845
2275
  x2 = x2 * width_ratio
@@ -1882,25 +2312,21 @@ class CDPMethods():
1882
2312
  % (x, y, screen_width, screen_height)
1883
2313
  )
1884
2314
  if uc_lock:
1885
- gui_lock = fasteners.InterProcessLock(
1886
- constants.MultiBrowser.PYAUTOGUILOCK
1887
- )
2315
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1888
2316
  with gui_lock: # Prevent issues with multiple processes
1889
- pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
1890
- time.sleep(0.056)
1891
2317
  if "--debug" in sys.argv:
1892
2318
  print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x, y))
2319
+ pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
2320
+ time.sleep(0.056)
1893
2321
  else:
1894
2322
  # Called from a method where the gui_lock is already active
1895
- pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
1896
- time.sleep(0.056)
1897
2323
  if "--debug" in sys.argv:
1898
2324
  print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x, y))
2325
+ pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
2326
+ time.sleep(0.056)
1899
2327
 
1900
2328
  def gui_hover_x_y(self, x, y, timeframe=0.25):
1901
- gui_lock = fasteners.InterProcessLock(
1902
- constants.MultiBrowser.PYAUTOGUILOCK
1903
- )
2329
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1904
2330
  with gui_lock: # Prevent issues with multiple processes
1905
2331
  self.__install_pyautogui_if_missing()
1906
2332
  import pyautogui
@@ -1925,15 +2351,14 @@ class CDPMethods():
1925
2351
  width_ratio = sb_config._saved_width_ratio
1926
2352
  else:
1927
2353
  scr_width = pyautogui.size().width
1928
- self.maximize()
1929
- self.__add_light_pause()
1930
- win_width = self.get_window_size()["width"]
2354
+ win_width = self.evaluate("screen.availWidth;")
1931
2355
  width_ratio = round(float(scr_width) / float(win_width), 2)
1932
2356
  width_ratio += 0.01
1933
2357
  if width_ratio < 0.45 or width_ratio > 2.55:
1934
2358
  width_ratio = 1.01
1935
2359
  sb_config._saved_width_ratio = width_ratio
1936
- self.set_window_rect(win_x, win_y, width, height)
2360
+ with suppress(Exception):
2361
+ self.__set_window_rect(win_x, win_y, width, height)
1937
2362
  self.__add_light_pause()
1938
2363
  self.bring_active_window_to_front()
1939
2364
  elif (
@@ -1959,14 +2384,40 @@ class CDPMethods():
1959
2384
  if width > 0 and height > 0:
1960
2385
  x, y = self.get_gui_element_center(selector)
1961
2386
  self.bring_active_window_to_front()
1962
- self.__gui_hover_x_y(x, y, timeframe=timeframe)
2387
+ self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False)
1963
2388
  self.__slow_mode_pause_if_set()
1964
2389
  self.loop.run_until_complete(self.page.wait())
1965
2390
 
2391
+ def hover_element(self, selector, timeframe=0.25):
2392
+ element = self.select(selector)
2393
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
2394
+ with gui_lock:
2395
+ self.bring_active_window_to_front()
2396
+ self.sleep(0.02)
2397
+ element.mouse_move()
2398
+ self.sleep(timeframe)
2399
+
2400
+ def hover_and_click(self, hover_selector, click_selector):
2401
+ if getattr(sb_config, "_cdp_mobile_mode", None):
2402
+ self.select(click_selector).click()
2403
+ return
2404
+ hover_element = self.select(hover_selector)
2405
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
2406
+ with gui_lock:
2407
+ self.bring_active_window_to_front()
2408
+ self.sleep(0.02)
2409
+ hover_element.mouse_move()
2410
+ self.sleep(0.25)
2411
+ try:
2412
+ self.click(click_selector, timeout=0.5)
2413
+ except Exception:
2414
+ self.select(click_selector, timeout=2).click()
2415
+
1966
2416
  def gui_hover_and_click(self, hover_selector, click_selector):
1967
- gui_lock = fasteners.InterProcessLock(
1968
- constants.MultiBrowser.PYAUTOGUILOCK
1969
- )
2417
+ if getattr(sb_config, "_cdp_mobile_mode", None):
2418
+ self.select(click_selector).click()
2419
+ return
2420
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
1970
2421
  with gui_lock:
1971
2422
  self.__make_sure_pyautogui_lock_is_writable()
1972
2423
  self.bring_active_window_to_front()
@@ -2126,6 +2577,10 @@ class CDPMethods():
2126
2577
  time.sleep(0.1)
2127
2578
  raise Exception("Element {%s} was not visible!" % selector)
2128
2579
 
2580
+ def wait_for_element(self, selector, **kwargs):
2581
+ """Same as wait_for_element_visible()"""
2582
+ return self.wait_for_element_visible(selector, **kwargs)
2583
+
2129
2584
  def wait_for_element_not_visible(self, selector, timeout=None):
2130
2585
  """Wait for element to not be visible on page. (May still be in DOM)"""
2131
2586
  if not timeout:
@@ -2503,6 +2958,13 @@ class CDPMethods():
2503
2958
  self.loop.run_until_complete(self.page.evaluate(js_code))
2504
2959
  self.loop.run_until_complete(self.page.wait())
2505
2960
 
2961
+ def scroll_by_y(self, y):
2962
+ y = int(y)
2963
+ js_code = "window.scrollBy(0, %s);" % y
2964
+ with suppress(Exception):
2965
+ self.loop.run_until_complete(self.page.evaluate(js_code))
2966
+ self.loop.run_until_complete(self.page.wait())
2967
+
2506
2968
  def scroll_to_top(self):
2507
2969
  js_code = "window.scrollTo(0, 0);"
2508
2970
  with suppress(Exception):
@@ -2516,13 +2978,56 @@ class CDPMethods():
2516
2978
  self.loop.run_until_complete(self.page.wait())
2517
2979
 
2518
2980
  def scroll_up(self, amount=25):
2519
- self.loop.run_until_complete(self.page.scroll_up(amount))
2981
+ """Scrolls up as a percentage of the page."""
2982
+ try:
2983
+ self.loop.run_until_complete(self.page.scroll_up(amount))
2984
+ except Exception:
2985
+ amount = self.get_window_size()["height"] * amount / 100
2986
+ self.execute_script("window.scrollBy(0, -%s);" % amount)
2520
2987
  self.loop.run_until_complete(self.page.wait())
2521
2988
 
2522
2989
  def scroll_down(self, amount=25):
2523
- self.loop.run_until_complete(self.page.scroll_down(amount))
2990
+ """Scrolls down as a percentage of the page."""
2991
+ try:
2992
+ self.loop.run_until_complete(self.page.scroll_down(amount))
2993
+ except Exception:
2994
+ amount = self.get_window_size()["height"] * amount / 100
2995
+ self.execute_script("window.scrollBy(0, %s);" % amount)
2524
2996
  self.loop.run_until_complete(self.page.wait())
2525
2997
 
2998
+ def save_page_source(self, name, folder=None):
2999
+ from seleniumbase.core import log_helper
3000
+ if not name.endswith(".html"):
3001
+ name = name + ".html"
3002
+ if folder:
3003
+ abs_path = os.path.abspath(".")
3004
+ file_path = os.path.join(abs_path, folder)
3005
+ if not os.path.exists(file_path):
3006
+ os.makedirs(file_path)
3007
+ html_file_path = os.path.join(file_path, name)
3008
+ else:
3009
+ html_file_path = name
3010
+ page_source = self.get_page_source()
3011
+ last_page = self.get_current_url()
3012
+ meta_charset = '<meta charset="utf-8">'
3013
+ rendered_source = ""
3014
+ if "://" in last_page:
3015
+ base_href_html = log_helper.get_base_href_html(last_page)
3016
+ if ' charset="' not in page_source:
3017
+ rendered_source = "%s\n%s\n%s" % (
3018
+ base_href_html, meta_charset, page_source
3019
+ )
3020
+ else:
3021
+ rendered_source = "%s\n%s" % (base_href_html, page_source)
3022
+ else:
3023
+ rendered_source = page_source
3024
+ html_file = open(html_file_path, mode="w+", encoding="utf-8")
3025
+ html_file.write(rendered_source)
3026
+ html_file.close()
3027
+
3028
+ def save_as_html(self, *args, **kwargs):
3029
+ self.save_page_source(*args, **kwargs)
3030
+
2526
3031
  def save_screenshot(self, name, folder=None, selector=None):
2527
3032
  filename = name
2528
3033
  if folder:
@@ -2540,12 +3045,26 @@ class CDPMethods():
2540
3045
  filename = os.path.join(folder, name)
2541
3046
  self.loop.run_until_complete(self.page.print_to_pdf(filename))
2542
3047
 
3048
+ def save_as_pdf(self, *args, **kwargs):
3049
+ self.print_to_pdf(*args, **kwargs)
3050
+
2543
3051
 
2544
3052
  class Chrome(CDPMethods):
2545
3053
  def __init__(self, url=None, **kwargs):
2546
3054
  if not url:
2547
3055
  url = "about:blank"
2548
- loop = asyncio.new_event_loop()
2549
3056
  driver = cdp_util.start_sync(**kwargs)
3057
+ loop = asyncio.new_event_loop()
2550
3058
  page = loop.run_until_complete(driver.get(url))
3059
+ wait_timeout = 30.0
3060
+ if hasattr(sb_config, "_cdp_proxy") and sb_config._cdp_proxy:
3061
+ wait_timeout = 45.0
3062
+ try:
3063
+ loop.run_until_complete(
3064
+ asyncio.wait_for(page.wait(), timeout=wait_timeout)
3065
+ )
3066
+ except asyncio.TimeoutError:
3067
+ pass
3068
+ except Exception:
3069
+ pass
2551
3070
  super().__init__(loop, page, driver)