diff --git a/examples/raw_antibot_login.py b/examples/raw_antibot_login.py new file mode 100644 index 00000000000..0ee882616b1 --- /dev/null +++ b/examples/raw_antibot_login.py @@ -0,0 +1,15 @@ +"""UC Mode has PyAutoGUI methods for CAPTCHA-bypass.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/antibot/login" + sb.uc_open_with_disconnect(url, 2.15) + sb.uc_gui_write("\t" + "demo_user") + sb.uc_gui_write("\t" + "secret_pass") + sb.uc_gui_press_keys("\t" + " ") # For Single-char keys + sb.sleep(1.5) + sb.uc_gui_press_keys(["\t", "ENTER"]) # Multi-char keys + sb.reconnect(1.8) + sb.assert_text("Welcome!", "h1") + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/raw_block.py b/examples/raw_block.py new file mode 100644 index 00000000000..bb4ffc95dd9 --- /dev/null +++ b/examples/raw_block.py @@ -0,0 +1,11 @@ +"""If Brotector catches you, Gandalf blocks you!""" +from seleniumbase import SB + +with SB(test=True) as sb: + url = "https://seleniumbase.io/hobbit/login" + sb.open(url) + sb.click_if_visible("button") + sb.assert_text("Gandalf blocked you!", "h1") + sb.click("img") + sb.highlight("h1") + sb.sleep(3) # Gandalf: "You Shall Not Pass!" diff --git a/examples/raw_brotector_captcha.py b/examples/raw_brotector_captcha.py new file mode 100644 index 00000000000..b123b29f274 --- /dev/null +++ b/examples/raw_brotector_captcha.py @@ -0,0 +1,9 @@ +"""UC Mode has PyAutoGUI methods for CAPTCHA-bypass.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/apps/brotector" + sb.uc_open_with_disconnect(url, 2.2) + sb.uc_gui_press_key("\t") + sb.uc_gui_press_key(" ") + sb.reconnect(2.2) diff --git a/examples/raw_detection.py b/examples/raw_detection.py new file mode 100644 index 00000000000..de8d96c97c6 --- /dev/null +++ b/examples/raw_detection.py @@ -0,0 +1,12 @@ +"""The Brotector CAPTCHA in action.""" +from seleniumbase import SB + +with SB(test=True) as sb: + sb.open("https://seleniumbase.io/antibot/login") + sb.highlight("h4", loops=6) + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click_if_visible("button span") + sb.highlight("label#pText") + sb.highlight("table#detections") + sb.sleep(4.4) # Add time to read the table diff --git a/examples/raw_hobbit.py b/examples/raw_hobbit.py new file mode 100644 index 00000000000..51fbc98dd83 --- /dev/null +++ b/examples/raw_hobbit.py @@ -0,0 +1,13 @@ +"""UC Mode has PyAutoGUI methods for CAPTCHA-bypass.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/hobbit/login" + sb.uc_open_with_disconnect(url, 2.2) + sb.uc_gui_press_keys("\t ") + sb.reconnect(1.5) + sb.assert_text("Welcome to Middle Earth!", "h1") + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + sb.click("img") + sb.sleep(5.888) # Cool animation happening now! diff --git a/examples/raw_pyautogui.py b/examples/raw_pyautogui.py new file mode 100644 index 00000000000..2c06bbe6777 --- /dev/null +++ b/examples/raw_pyautogui.py @@ -0,0 +1,19 @@ +""" +UC Mode now has uc_gui_handle_cf(), which uses PyAutoGUI. +An incomplete UserAgent is used to force CAPTCHA-solving. +""" +import sys +from seleniumbase import SB + +agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0.0.0" +if "linux" in sys.platform: + agent = None # Use the default UserAgent + +with SB(uc=True, test=True, rtf=True, agent=agent) as sb: + url = "https://www.virtualmanager.com/en/login" + sb.uc_open_with_reconnect(url, 4) + sb.uc_gui_handle_cf() # Ready if needed! + sb.assert_element('input[name*="email"]') + sb.assert_element('input[name*="login"]') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index a577e33d0c9..d90701cbd90 100644 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -111,7 +111,7 @@ self.click_partial_link(partial_link_text, timeout=None) # Duplicates: # self.click_partial_link_text(partial_link_text, timeout=None) -self.get_text(selector, by="css selector", timeout=None) +self.get_text(selector="html", by="css selector", timeout=None) self.get_attribute(selector, attribute, by="css selector", timeout=None, hard_fail=True) @@ -127,7 +127,7 @@ self.remove_attributes(selector, attribute, by="css selector") self.get_property(selector, property, by="css selector", timeout=None) -self.get_text_content(selector, by="css selector", timeout=None) +self.get_text_content(selector="html", by="css selector", timeout=None) self.get_property_value(selector, property, by="css selector", timeout=None) @@ -229,7 +229,7 @@ self.set_window_size(width, height) self.maximize_window() -self.switch_to_frame(frame, timeout=None) +self.switch_to_frame(frame="iframe", timeout=None) self.switch_to_default_content() @@ -1032,7 +1032,7 @@ driver.get_page_source() driver.get_title() -driver.switch_to_frame(frame) +driver.switch_to_frame(frame="iframe") ############ @@ -1046,7 +1046,7 @@ driver.uc_open_with_tab(url) # (New tab with default reconnect_time) driver.uc_open_with_reconnect(url, reconnect_time=None) # (New tab) -driver.uc_open_with_disconnect(url) # Open in new tab + disconnect() +driver.uc_open_with_disconnect(url, timeout=None) # New tab + sleep() driver.reconnect(timeout) # disconnect() + sleep(timeout) + connect() @@ -1056,7 +1056,15 @@ driver.connect() # Starts the webdriver service to allow actions again driver.uc_click(selector) # A stealthy click for evading bot-detection -driver.uc_switch_to_frame(frame) # switch_to_frame() in a stealthy way +driver.uc_gui_press_key(key) # Use PyAutoGUI to press the keyboard key + +driver.uc_gui_press_keys(keys) # Use PyAutoGUI to press a list of keys + +driver.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster + +driver.uc_gui_handle_cf(frame="iframe") # PyAutoGUI click CF Turnstile + +driver.uc_switch_to_frame(frame="iframe") # Stealthy switch_to_frame() ``` -------- diff --git a/help_docs/uc_mode.md b/help_docs/uc_mode.md index 1980d9fb56a..7931f38456f 100644 --- a/help_docs/uc_mode.md +++ b/help_docs/uc_mode.md @@ -159,7 +159,7 @@ driver.uc_open_with_tab(url) driver.uc_open_with_reconnect(url, reconnect_time=None) -driver.uc_open_with_disconnect(url) +driver.uc_open_with_disconnect(url, timeout=None) driver.reconnect(timeout) @@ -171,6 +171,14 @@ driver.uc_click( selector, by="css selector", timeout=settings.SMALL_TIMEOUT, reconnect_time=None) +driver.uc_gui_press_key(key) + +driver.uc_gui_press_keys(keys) + +driver.uc_gui_write(text) + +driver.uc_gui_handle_cf(frame="iframe") + driver.uc_switch_to_frame(frame, reconnect_time=None) ``` @@ -211,6 +219,8 @@ driver.reconnect("breakpoint")
  • Timing. (UC Mode methods let you customize default values that aren't good enough for your environment.)
  • Not using driver.uc_click(selector) when you need to remain undetected while clicking something.
  • +👤 On Linux, you may need to use `driver.uc_gui_handle_cf()` to successfully bypass a Cloudflare CAPTCHA. If there's more than one iframe on that website (and Cloudflare isn't the first one) then put the CSS Selector of that iframe as the first arg to `driver.uc_gui_handle_cf()`. This method uses `pyautogui`. In order for `pyautogui` to focus on the correct element, use `xvfb=True` / `--xvfb` to activate a special virtual display on Linux. + 👤 To find out if UC Mode will work at all on a specific site (before adjusting for timing), load your site with the following script: ```python diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 6a932c7a84d..73f59a3b895 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -3,7 +3,7 @@ regex>=2024.5.15 pymdown-extensions>=10.8.1 -pipdeptree>=2.22.0 +pipdeptree>=2.23.0 python-dateutil>=2.8.2 Markdown==3.6 markdown2==2.4.13 @@ -12,7 +12,7 @@ Jinja2==3.1.4 click==8.1.7 ghp-import==2.1.0 watchdog==4.0.1 -cairocffi==1.7.0 +cairocffi==1.7.1 pathspec==0.12.1 Babel==2.15.0 paginate==0.5.6 @@ -20,7 +20,7 @@ lxml==5.2.2 pyquery==2.0.0 readtime==3.0.0 mkdocs==1.6.0 -mkdocs-material==9.5.26 +mkdocs-material==9.5.27 mkdocs-exclude-search==0.6.6 mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.3.1 diff --git a/requirements.txt b/requirements.txt index 4cba1a54db7..56b90063bb4 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,20 @@ -pip>=24.0 -packaging>=24.0 +pip>=24.0;python_version<"3.8" +pip>=24.1;python_version>="3.8" +packaging>=24.0;python_version<"3.8" +packaging>=24.1;python_version>="3.8" setuptools>=68.0.0;python_version<"3.8" -setuptools>=70.0.0;python_version>="3.8" +setuptools>=70.1.0;python_version>="3.8" wheel>=0.42.0;python_version<"3.8" wheel>=0.43.0;python_version>="3.8" attrs>=23.2.0 certifi>=2024.6.2 exceptiongroup>=1.2.1 filelock>=3.12.2;python_version<"3.8" -filelock>=3.14.0;python_version>="3.8" +filelock>=3.15.4;python_version>="3.8" platformdirs>=4.0.0;python_version<"3.8" platformdirs>=4.2.2;python_version>="3.8" typing-extensions>=4.12.2;python_version>="3.8" -parse>=1.20.1 +parse>=1.20.2 parse-type>=0.6.2 pyyaml>=6.0.1 six==1.16.0 @@ -30,8 +32,9 @@ trio==0.22.2;python_version<"3.8" trio==0.25.1;python_version>="3.8" trio-websocket==0.11.1 wsproto==1.2.0 +websocket-client==1.8.0;python_version>="3.8" selenium==4.11.2;python_version<"3.8" -selenium==4.21.0;python_version>="3.8" +selenium==4.22.0;python_version>="3.8" cssselect==1.2.0 sortedcontainers==2.4.0 fasteners==0.19 @@ -64,6 +67,7 @@ tabcompleter==1.3.0 pdbp==1.5.0 colorama==0.4.6 pyotp==2.9.0 +python-xlib==0.33;platform_system=="Linux" markdown-it-py==2.2.0;python_version<"3.8" markdown-it-py==3.0.0;python_version>="3.8" mdurl==0.1.2 @@ -73,13 +77,13 @@ rich==13.7.1 # ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.) coverage==7.2.7;python_version<"3.8" -coverage>=7.5.3;python_version>="3.8" +coverage>=7.5.4;python_version>="3.8" pytest-cov==4.1.0;python_version<"3.8" pytest-cov>=5.0.0;python_version>="3.8" flake8==5.0.4;python_version<"3.9" -flake8==7.0.0;python_version>="3.9" +flake8==7.1.0;python_version>="3.9" mccabe==0.7.0 pyflakes==2.5.0;python_version<"3.9" pyflakes==3.2.0;python_version>="3.9" pycodestyle==2.9.1;python_version<"3.9" -pycodestyle==2.11.1;python_version>="3.9" +pycodestyle==2.12.0;python_version>="3.9" diff --git a/seleniumbase/__init__.py b/seleniumbase/__init__.py index fe64cffabac..e84a7d3628c 100644 --- a/seleniumbase/__init__.py +++ b/seleniumbase/__init__.py @@ -1,4 +1,5 @@ import collections +import os import pdb try: import pdbp # (Pdb+) --- Python Debugger Plus @@ -34,11 +35,13 @@ pdb.DefaultConfig.truncate_long_lines = False pdb.DefaultConfig.sticky_by_default = True colored_traceback.add_hook() +os.environ["SE_AVOID_STATS"] = "true" # Disable Selenium Manager stats if sys.version_info >= (3, 7): webdriver.TouchActions = None # Lifeline for past selenium-wire versions if sys.version_info >= (3, 10): collections.Callable = collections.abc.Callable # Lifeline for nosetests del collections # Undo "import collections" / Simplify "dir(seleniumbase)" +del os # Undo "import os" / Simplify "dir(seleniumbase)" del sys # Undo "import sys" / Simplify "dir(seleniumbase)" del webdriver # Undo "import webdriver" / Simplify "dir(seleniumbase)" diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index e06fd45c16b..bab6b9fe392 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.27.5" +__version__ = "4.28.0" diff --git a/seleniumbase/behave/behave_sb.py b/seleniumbase/behave/behave_sb.py index bd54740ee1f..7fed5f88be6 100644 --- a/seleniumbase/behave/behave_sb.py +++ b/seleniumbase/behave/behave_sb.py @@ -862,14 +862,17 @@ def get_configured_sb(context): and not sb.headless2 and not sb.xvfb ): - print( - '(Linux uses "-D headless" by default. ' - 'To override, use "-D headed" / "-D gui". ' - 'For Xvfb mode instead, use "-D xvfb". ' - "Or you can hide this info by using" - '"-D headless" / "-D headless2".)' - ) - sb.headless = True + if not sb.undetectable: + print( + '(Linux uses "-D headless" by default. ' + 'To override, use "-D headed" / "-D gui". ' + 'For Xvfb mode instead, use "-D xvfb". ' + "Or you can hide this info by using" + '"-D headless" / "-D headless2" / "-D uc".)' + ) + sb.headless = True + else: + sb.xvfb = True # Recorder Mode can still optimize scripts in --headless2 mode. if sb.recorder_mode and sb.headless: sb.headless = False diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 4f37ea64004..fe9023a4ab2 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -11,6 +11,7 @@ import warnings from selenium import webdriver from selenium.common.exceptions import ElementClickInterceptedException +from selenium.common.exceptions import InvalidSessionIdException from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.common.options import ArgOptions from selenium.webdriver.common.service import utils as service_utils @@ -28,6 +29,7 @@ from seleniumbase.core import sb_driver from seleniumbase.fixtures import constants from seleniumbase.fixtures import js_utils +from seleniumbase.fixtures import page_actions from seleniumbase.fixtures import shared_utils urllib3.disable_warnings() @@ -130,9 +132,13 @@ def extend_driver(driver): page.assert_element_not_visible = DM.assert_element_not_visible page.assert_text = DM.assert_text page.assert_exact_text = DM.assert_exact_text + page.assert_non_empty_text = DM.assert_non_empty_text + page.assert_text_not_visible = DM.assert_text_not_visible page.wait_for_element = DM.wait_for_element page.wait_for_text = DM.wait_for_text page.wait_for_exact_text = DM.wait_for_exact_text + page.wait_for_non_empty_text = DM.wait_for_non_empty_text + page.wait_for_text_not_visible = DM.wait_for_text_not_visible page.wait_for_and_accept_alert = DM.wait_for_and_accept_alert page.wait_for_and_dismiss_alert = DM.wait_for_and_dismiss_alert page.is_element_present = DM.is_element_present @@ -140,12 +146,20 @@ def extend_driver(driver): page.is_text_visible = DM.is_text_visible page.is_exact_text_visible = DM.is_exact_text_visible page.is_attribute_present = DM.is_attribute_present + page.is_non_empty_text_visible = DM.is_non_empty_text_visible page.get_text = DM.get_text page.find_element = DM.find_element page.find_elements = DM.find_elements page.locator = DM.locator page.get_page_source = DM.get_page_source page.get_title = DM.get_title + page.switch_to_default_window = DM.switch_to_default_window + page.switch_to_newest_window = DM.switch_to_newest_window + page.open_new_window = DM.open_new_window + page.open_new_tab = DM.open_new_tab + page.switch_to_window = DM.switch_to_window + page.switch_to_tab = DM.switch_to_tab + page.switch_to_frame = DM.switch_to_frame driver.page = page js = types.SimpleNamespace() js.js_click = DM.js_click @@ -169,12 +183,16 @@ def extend_driver(driver): driver.assert_element_not_visible = DM.assert_element_not_visible driver.assert_text = DM.assert_text driver.assert_exact_text = DM.assert_exact_text + driver.assert_non_empty_text = DM.assert_non_empty_text + driver.assert_text_not_visible = DM.assert_text_not_visible driver.wait_for_element = DM.wait_for_element driver.wait_for_element_visible = DM.wait_for_element_visible driver.wait_for_element_present = DM.wait_for_element_present driver.wait_for_selector = DM.wait_for_selector driver.wait_for_text = DM.wait_for_text driver.wait_for_exact_text = DM.wait_for_exact_text + driver.wait_for_non_empty_text = DM.wait_for_non_empty_text + driver.wait_for_text_not_visible = DM.wait_for_text_not_visible driver.wait_for_and_accept_alert = DM.wait_for_and_accept_alert driver.wait_for_and_dismiss_alert = DM.wait_for_and_dismiss_alert driver.is_element_present = DM.is_element_present @@ -182,8 +200,10 @@ def extend_driver(driver): driver.is_text_visible = DM.is_text_visible driver.is_exact_text_visible = DM.is_exact_text_visible driver.is_attribute_present = DM.is_attribute_present - driver.get_text = DM.get_text + driver.is_non_empty_text_visible = DM.is_non_empty_text_visible + driver.is_online = DM.is_online driver.js_click = DM.js_click + driver.get_text = DM.get_text driver.get_active_element_css = DM.get_active_element_css driver.get_locale_code = DM.get_locale_code driver.get_origin = DM.get_origin @@ -195,6 +215,12 @@ def extend_driver(driver): driver.get_attribute = DM.get_attribute driver.get_page_source = DM.get_page_source driver.get_title = DM.get_title + driver.switch_to_default_window = DM.switch_to_default_window + driver.switch_to_newest_window = DM.switch_to_newest_window + driver.open_new_window = DM.open_new_window + driver.open_new_tab = DM.open_new_tab + driver.switch_to_window = DM.switch_to_window + driver.switch_to_tab = DM.switch_to_tab driver.switch_to_frame = DM.switch_to_frame if hasattr(driver, "proxy"): driver.set_wire_proxy = DM.set_wire_proxy @@ -409,7 +435,7 @@ def uc_open(driver, url): if (url.startswith("http:") or url.startswith("https:")): with driver: script = 'window.location.href = "%s";' % url - js_utils.call_me_later(driver, script, 33) + js_utils.call_me_later(driver, script, 5) else: driver.default_get(url) # The original one return None @@ -440,22 +466,28 @@ def uc_open_with_reconnect(driver, url, reconnect_time=None): url = "https://" + url if (url.startswith("http:") or url.startswith("https:")): script = 'window.open("%s","_blank");' % url - js_utils.call_me_later(driver, script, 3) - time.sleep(0.007) + driver.execute_script(script) + time.sleep(0.05) driver.close() if reconnect_time == "disconnect": driver.disconnect() - time.sleep(0.007) + time.sleep(0.008) else: driver.reconnect(reconnect_time) - driver.switch_to.window(driver.window_handles[-1]) + time.sleep(0.004) + try: + driver.switch_to.window(driver.window_handles[-1]) + except InvalidSessionIdException: + time.sleep(0.05) + driver.switch_to.window(driver.window_handles[-1]) else: driver.default_get(url) # The original one return None -def uc_open_with_disconnect(driver, url): +def uc_open_with_disconnect(driver, url, timeout=None): """Open a url and disconnect chromedriver. + Then waits for the duration of the timeout. Note: You can't perform Selenium actions again until after you've called driver.connect().""" if url.startswith("//"): @@ -464,11 +496,16 @@ def uc_open_with_disconnect(driver, url): url = "https://" + url if (url.startswith("http:") or url.startswith("https:")): script = 'window.open("%s","_blank");' % url - js_utils.call_me_later(driver, script, 3) - time.sleep(0.007) + driver.execute_script(script) + time.sleep(0.05) driver.close() driver.disconnect() - time.sleep(0.007) + min_timeout = 0.008 + if timeout and not str(timeout).replace(".", "", 1).isdigit(): + timeout = min_timeout + if not timeout or timeout < min_timeout: + timeout = min_timeout + time.sleep(timeout) else: driver.default_get(url) # The original one return None @@ -490,7 +527,7 @@ def uc_click( pass element = driver.wait_for_selector(selector, by=by, timeout=timeout) tag_name = element.tag_name - if not tag_name == "span": # Element must be "visible" + if not tag_name == "span" and not tag_name == "input": # Must be "visible" element = driver.wait_for_element(selector, by=by, timeout=timeout) try: element.uc_click( @@ -509,7 +546,154 @@ def uc_click( driver.reconnect(reconnect_time) -def uc_switch_to_frame(driver, frame, reconnect_time=None): +def verify_pyautogui_has_a_headed_browser(): + """PyAutoGUI requires a headed browser so that it can + focus on the correct element when performing actions.""" + if sb_config.headless or sb_config.headless2: + raise Exception( + "PyAutoGUI can't be used in headless mode!" + ) + + +def install_pyautogui_if_missing(): + verify_pyautogui_has_a_headed_browser() + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: # Prevent issues with multiple processes + try: + import pyautogui + try: + use_pyautogui_ver = constants.PyAutoGUI.VER + if pyautogui.__version__ != use_pyautogui_ver: + del pyautogui + shared_utils.pip_install( + "pyautogui", version=use_pyautogui_ver + ) + import pyautogui + except Exception: + pass + except Exception: + print("\nPyAutoGUI required! Installing now...") + shared_utils.pip_install( + "pyautogui", version=constants.PyAutoGUI.VER + ) + + +def get_configured_pyautogui(pyautogui_copy): + if ( + IS_LINUX + and hasattr(pyautogui_copy, "_pyautogui_x11") + and "DISPLAY" in os.environ.keys() + ): + if ( + hasattr(sb_config, "_pyautogui_x11_display") + and sb_config._pyautogui_x11_display + and hasattr(pyautogui_copy._pyautogui_x11, "_display") + and ( + sb_config._pyautogui_x11_display + == pyautogui_copy._pyautogui_x11._display + ) + ): + pass + else: + import Xlib.display + pyautogui_copy._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui_copy._pyautogui_x11._display + ) + return pyautogui_copy + + +def uc_gui_press_key(driver, key): + install_pyautogui_if_missing() + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: + pyautogui.press(key) + + +def uc_gui_press_keys(driver, keys): + install_pyautogui_if_missing() + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: + for key in keys: + pyautogui.press(key) + + +def uc_gui_write(driver, text): + install_pyautogui_if_missing() + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: + pyautogui.write(text) + + +def uc_gui_handle_cf(driver, frame="iframe"): + source = driver.get_page_source() + if ( + "//challenges.cloudflare.com" not in source + and 'aria-label="Cloudflare"' not in source + ): + return + install_pyautogui_if_missing() + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + needs_switch = False + is_in_frame = js_utils.is_in_frame(driver) + if is_in_frame and driver.is_element_present("#challenge-stage"): + driver.switch_to.parent_frame() + needs_switch = True + is_in_frame = js_utils.is_in_frame(driver) + if not is_in_frame: + # Make sure the window is on top + page_actions.switch_to_window( + driver, + driver.current_window_handle, + timeout=settings.SMALL_TIMEOUT, + ) + if not is_in_frame or needs_switch: + # Currently not in frame (or nested frame outside CF one) + try: + driver.switch_to_frame(frame) + except Exception: + if driver.is_element_present("iframe"): + driver.switch_to_frame("iframe") + else: + return + try: + driver.execute_script('document.querySelector("input").focus()') + except Exception: + try: + driver.switch_to.default_content() + except Exception: + return + driver.disconnect() + try: + pyautogui.press(" ") + except Exception: + pass + reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.5 + driver.reconnect(reconnect_time) + + +def uc_switch_to_frame(driver, frame="iframe", reconnect_time=None): from selenium.webdriver.remote.webelement import WebElement if isinstance(frame, WebElement): if not reconnect_time: @@ -847,7 +1031,7 @@ def _set_chrome_options( prefs["download.prompt_for_download"] = False prefs["credentials_enable_service"] = False prefs["local_discovery.notifications_enabled"] = False - prefs["safebrowsing.enabled"] = False + prefs["safebrowsing.enabled"] = False # Prevent PW "data breach" pop-ups prefs["safebrowsing.disable_download_protection"] = True prefs["omnibox-max-zero-suggest-matches"] = 0 prefs["omnibox-use-existing-autocomplete-client"] = 0 @@ -1145,6 +1329,7 @@ def _set_chrome_options( chrome_options.add_argument("--auto-open-devtools-for-tabs") if user_agent: chrome_options.add_argument("--user-agent=%s" % user_agent) + chrome_options.add_argument("--safebrowsing-disable-download-protection") chrome_options.add_argument("--disable-browser-side-navigation") chrome_options.add_argument("--disable-save-password-bubble") chrome_options.add_argument("--disable-single-click-autofill") @@ -1183,10 +1368,9 @@ def _set_chrome_options( included_disabled_features.append("DownloadBubbleV2") included_disabled_features.append("InsecureDownloadWarnings") included_disabled_features.append("InterestFeedContentSuggestions") - if user_data_dir: - included_disabled_features.append("PrivacySandboxSettings4") - if not is_using_uc(undetectable, browser_name) or user_data_dir: - included_disabled_features.append("SidePanelPinning") + included_disabled_features.append("PrivacySandboxSettings4") + included_disabled_features.append("SidePanelPinning") + included_disabled_features.append("UserAgentClientHint") for item in extra_disabled_features: if item not in included_disabled_features: included_disabled_features.append(item) @@ -1374,7 +1558,7 @@ def _set_firefox_options( f_pref_value = False elif f_pref_value.isdigit(): f_pref_value = int(f_pref_value) - elif f_pref_value.isdecimal(): + elif f_pref_value.replace(".", "", 1).isdigit(): f_pref_value = float(f_pref_value) else: pass # keep as string @@ -2438,7 +2622,7 @@ def get_local_driver( "credentials_enable_service": False, "local_discovery.notifications_enabled": False, "safebrowsing.disable_download_protection": True, - "safebrowsing.enabled": False, + "safebrowsing.enabled": False, # Prevent PW "data breach" pop-ups "omnibox-max-zero-suggest-matches": 0, "omnibox-use-existing-autocomplete-client": 0, "omnibox-trending-zero-prefix-suggestions-on-ntp": 0, @@ -2707,6 +2891,7 @@ def get_local_driver( edge_options.add_argument( "--disable-autofill-keyboard-accessory-view[8]" ) + edge_options.add_argument("--safebrowsing-disable-download-protection") edge_options.add_argument("--disable-browser-side-navigation") edge_options.add_argument("--disable-translate") if not enable_ws: @@ -2848,10 +3033,9 @@ def get_local_driver( included_disabled_features.append("OptimizationGuideModelDownloading") included_disabled_features.append("InsecureDownloadWarnings") included_disabled_features.append("InterestFeedContentSuggestions") - if user_data_dir: - included_disabled_features.append("PrivacySandboxSettings4") - if not is_using_uc(undetectable, browser_name) or user_data_dir: - included_disabled_features.append("SidePanelPinning") + included_disabled_features.append("PrivacySandboxSettings4") + included_disabled_features.append("SidePanelPinning") + included_disabled_features.append("UserAgentClientHint") for item in extra_disabled_features: if item not in included_disabled_features: included_disabled_features.append(item) @@ -3822,6 +4006,26 @@ def get_local_driver( driver.uc_click = lambda *args, **kwargs: uc_click( driver, *args, **kwargs ) + driver.uc_gui_press_key = ( + lambda *args, **kwargs: uc_gui_press_key( + driver, *args, **kwargs + ) + ) + driver.uc_gui_press_keys = ( + lambda *args, **kwargs: uc_gui_press_keys( + driver, *args, **kwargs + ) + ) + driver.uc_gui_write = ( + lambda *args, **kwargs: uc_gui_write( + driver, *args, **kwargs + ) + ) + driver.uc_gui_handle_cf = ( + lambda *args, **kwargs: uc_gui_handle_cf( + driver, *args, **kwargs + ) + ) driver.uc_switch_to_frame = ( lambda *args, **kwargs: uc_switch_to_frame( driver, *args, **kwargs diff --git a/seleniumbase/core/sb_driver.py b/seleniumbase/core/sb_driver.py index f5054173fd8..3ca7a4955f1 100644 --- a/seleniumbase/core/sb_driver.py +++ b/seleniumbase/core/sb_driver.py @@ -94,6 +94,16 @@ def assert_text(self, *args, **kwargs): def assert_exact_text(self, *args, **kwargs): page_actions.assert_exact_text(self.driver, *args, **kwargs) + def assert_non_empty_text(self, *args, **kwargs): + return page_actions.assert_non_empty_text( + self.driver, *args, **kwargs + ) + + def assert_text_not_visible(self, *args, **kwargs): + return page_actions.assert_text_not_visible( + self.driver, *args, **kwargs + ) + def wait_for_element(self, *args, **kwargs): return page_actions.wait_for_element(self.driver, *args, **kwargs) @@ -112,6 +122,16 @@ def wait_for_text(self, *args, **kwargs): def wait_for_exact_text(self, *args, **kwargs): return page_actions.wait_for_exact_text(self.driver, *args, **kwargs) + def wait_for_non_empty_text(self, *args, **kwargs): + return page_actions.wait_for_non_empty_text( + self.driver, *args, **kwargs + ) + + def wait_for_text_not_visible(self, *args, **kwargs): + return page_actions.wait_for_text_not_visible( + self.driver, *args, **kwargs + ) + def wait_for_and_accept_alert(self, *args, **kwargs): return page_actions.wait_for_and_accept_alert( self.driver, *args, **kwargs @@ -134,14 +154,22 @@ def is_text_visible(self, *args, **kwargs): def is_exact_text_visible(self, *args, **kwargs): return page_actions.is_exact_text_visible(self.driver, *args, **kwargs) - def get_text(self, *args, **kwargs): - return page_actions.get_text(self.driver, *args, **kwargs) + def is_attribute_present(self, *args, **kwargs): + return page_actions.has_attribute(self.driver, *args, **kwargs) + + def is_non_empty_text_visible(self, *args, **kwargs): + return page_actions.is_non_empty_text_visible( + self.driver, *args, **kwargs + ) + + def is_online(self): + return self.driver.execute_script("return navigator.onLine;") def js_click(self, *args, **kwargs): return page_actions.js_click(self.driver, *args, **kwargs) - def is_attribute_present(self, *args, **kwargs): - return page_actions.has_attribute(self.driver, *args, **kwargs) + def get_text(self, *args, **kwargs): + return page_actions.get_text(self.driver, *args, **kwargs) def get_active_element_css(self, *args, **kwargs): return js_utils.get_active_element_css(self.driver, *args, **kwargs) @@ -182,7 +210,32 @@ def highlight_if_visible( if self.is_element_visible(selector, by=by): self.highlight(selector, by=by, loops=loops, scroll=scroll) - def switch_to_frame(self, frame): + def switch_to_default_window(self): + self.driver.switch_to.window(self.driver.window_handles[0]) + + def switch_to_newest_window(self): + self.driver.switch_to.window(self.driver.window_handles[-1]) + + def open_new_window(self, switch_to=True): + if switch_to: + try: + self.driver.switch_to.new_window("tab") + except Exception: + self.driver.execute_script("window.open('');") + self.switch_to_newest_window() + else: + self.driver.execute_script("window.open('');") + + def open_new_tab(self, switch_to=True): + self.open_new_window(switch_to=switch_to) + + def switch_to_window(self, *args, **kwargs): + page_actions.switch_to_window(self.driver, *args, **kwargs) + + def switch_to_tab(self, *args, **kwargs): + self.switch_to_window(*args, **kwargs) + + def switch_to_frame(self, frame="iframe"): if isinstance(frame, WebElement): self.driver.switch_to.frame(frame) else: diff --git a/seleniumbase/extensions/recorder.zip b/seleniumbase/extensions/recorder.zip index 31f243ad84c..257fcd562d8 100644 Binary files a/seleniumbase/extensions/recorder.zip and b/seleniumbase/extensions/recorder.zip differ diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 8a7df9bc917..1af4806f8a5 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1251,9 +1251,17 @@ def focus(self, selector, by="css selector", timeout=None): if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) - element = self.wait_for_element_visible( + element = self.wait_for_element_present( selector, by=by, timeout=timeout ) + if not element.is_displayed(): + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = """document.querySelector('%s').focus();""" % css_selector + self.execute_script(script) + self.__demo_mode_pause_if_active() + return self.scroll_to(selector, by=by, timeout=timeout) try: element.send_keys(Keys.NULL) @@ -1828,7 +1836,7 @@ def click_partial_link_text(self, partial_link_text, timeout=None): elif self.slow_mode: self.__slow_mode_pause_if_active() - def get_text(self, selector, by="css selector", timeout=None): + def get_text(self, selector="html", by="css selector", timeout=None): self.__check_scope() if not timeout: timeout = settings.LARGE_TIMEOUT @@ -2083,7 +2091,9 @@ def get_property( return "" return property_value - def get_text_content(self, selector, by="css selector", timeout=None): + def get_text_content( + self, selector="html", by="css selector", timeout=None + ): """Returns the text that appears in the HTML for an element. This is different from "self.get_text(selector, by="css selector")" because that only returns the visible text on a page for an element, @@ -3401,7 +3411,7 @@ def maximize_window(self): self.driver.maximize_window() self.__demo_mode_pause_if_active() - def switch_to_frame(self, frame, timeout=None): + def switch_to_frame(self, frame="iframe", timeout=None): """Wait for an iframe to appear, and switch to it. This should be usable as a drop-in replacement for driver.switch_to.frame(). The iframe identifier can be a selector, an index, an id, a name, @@ -3728,15 +3738,11 @@ def open_new_window(self, switch_to=True): """Opens a new browser tab/window and switches to it by default.""" self.wait_for_ready_state_complete() if switch_to: - if self.undetectable: - self.driver.execute_script("window.open('data:,');") + try: + self.driver.switch_to.new_window("tab") + except Exception: + self.driver.execute_script("window.open('');") self.switch_to_newest_window() - else: - try: - self.driver.switch_to.new_window("tab") - except Exception: - self.driver.execute_script("window.open('');") - self.switch_to_newest_window() else: self.driver.execute_script("window.open('');") time.sleep(0.01) @@ -4166,6 +4172,14 @@ def get_new_driver( self.connect = new_driver.connect if hasattr(new_driver, "uc_click"): self.uc_click = new_driver.uc_click + if hasattr(new_driver, "uc_gui_press_key"): + self.uc_gui_press_key = new_driver.uc_gui_press_key + if hasattr(new_driver, "uc_gui_press_keys"): + self.uc_gui_press_keys = new_driver.uc_gui_press_keys + if hasattr(new_driver, "uc_gui_write"): + self.uc_gui_write = new_driver.uc_gui_write + if hasattr(new_driver, "uc_gui_handle_cf"): + self.uc_gui_handle_cf = new_driver.uc_gui_handle_cf if hasattr(new_driver, "uc_switch_to_frame"): self.uc_switch_to_frame = new_driver.uc_switch_to_frame return new_driver @@ -6808,9 +6822,11 @@ def get_pdf_text( try: import cryptography if cryptography.__version__ != "39.0.2": + del cryptography # To get newer ver shared_utils.pip_install( "cryptography", version="39.0.2" ) + import cryptography except Exception: shared_utils.pip_install( "cryptography", version="39.0.2" @@ -13681,23 +13697,95 @@ def __highlight_with_assert_success( pass # JQuery probably couldn't load. Skip highlighting. time.sleep(0.065) + def __activate_standard_virtual_display(self): + from sbvirtualdisplay import Display + width = settings.HEADLESS_START_WIDTH + height = settings.HEADLESS_START_HEIGHT + try: + self._xvfb_display = Display( + visible=0, size=(width, height) + ) + self._xvfb_display.start() + sb_config._virtual_display = self._xvfb_display + self.headless_active = True + sb_config.headless_active = True + except Exception: + pass + def __activate_virtual_display_as_needed(self): """Should be needed only on Linux. The "--xvfb" arg is still useful, as it prevents headless mode, which is the default mode on Linux unless using another arg.""" if "linux" in sys.platform and (not self.headed or self.xvfb): - width = settings.HEADLESS_START_WIDTH - height = settings.HEADLESS_START_HEIGHT - try: - from sbvirtualdisplay import Display - - self._xvfb_display = Display(visible=0, size=(width, height)) - self._xvfb_display.start() - sb_config._virtual_display = self._xvfb_display - self.headless_active = True - sb_config.headless_active = True - except Exception: - pass + from sbvirtualdisplay import Display + if self.undetectable and not (self.headless or self.headless2): + import Xlib.display + try: + self._xvfb_display = Display( + visible=True, + size=(1366, 768), + backend="xvfb", + use_xauth=True, + ) + self._xvfb_display.start() + if "DISPLAY" not in os.environ.keys(): + print("\nX11 display failed! Will use regular xvfb!") + self.__activate_standard_virtual_display() + except Exception as e: + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + print("\nX11 display failed! Will use regular xvfb!") + self.__activate_standard_virtual_display() + return + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: # Prevent issues with multiple processes + pyautogui_is_installed = False + try: + import pyautogui + try: + use_pyautogui_ver = constants.PyAutoGUI.VER + if pyautogui.__version__ != use_pyautogui_ver: + del pyautogui # To get newer ver + shared_utils.pip_install( + "pyautogui", version=use_pyautogui_ver + ) + import pyautogui + except Exception: + pass + pyautogui_is_installed = True + except Exception: + message = ( + "PyAutoGUI is required for UC Mode on Linux! " + "Installing now..." + ) + print("\n" + message) + shared_utils.pip_install( + "pyautogui", version=constants.PyAutoGUI.VER + ) + import pyautogui + pyautogui_is_installed = True + if ( + pyautogui_is_installed + and hasattr(pyautogui, "_pyautogui_x11") + ): + try: + pyautogui._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui._pyautogui_x11._display + ) + except Exception as e: + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + else: + self.__activate_standard_virtual_display() def __ad_block_as_needed(self): """This is an internal method for handling ad-blocking. @@ -15898,7 +15986,7 @@ def tearDown(self): if self.undetectable: try: self.driver.window_handles - except urllib3.exceptions.MaxRetryError: + except Exception: self.driver.connect() self.__slow_mode_pause_if_active() has_exception = self.__has_exception() diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index 8f9b805c0ac..94c8c19068a 100644 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -164,6 +164,7 @@ class MultiBrowser: CERT_FIXING_LOCK = Files.DOWNLOADS_FOLDER + "/cert_fixing.lock" DOWNLOAD_FILE_LOCK = Files.DOWNLOADS_FOLDER + "/downloading.lock" FILE_IO_LOCK = Files.DOWNLOADS_FOLDER + "/file_io.lock" + PYAUTOGUILOCK = Files.DOWNLOADS_FOLDER + "/pyautogui.lock" class SavedCookies: @@ -354,6 +355,11 @@ class SeleniumWire: BLINKER_VER = "1.7.0" # The "blinker" dependency version +class PyAutoGUI: + # The version installed if PyAutoGUI is not installed + VER = "0.9.54" + + class Mobile: # Default values for mobile settings WIDTH = 390 diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index 3aaa2ccf98a..9564763388c 100644 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -1017,7 +1017,7 @@ def wait_for_element_not_visible( def wait_for_text_not_visible( driver, text, - selector, + selector="html", by="css selector", timeout=settings.LARGE_TIMEOUT, ): @@ -1060,7 +1060,7 @@ def wait_for_text_not_visible( def wait_for_exact_text_not_visible( driver, text, - selector, + selector="html", by="css selector", timeout=settings.LARGE_TIMEOUT, ): @@ -1686,19 +1686,50 @@ def assert_text( by="css selector", timeout=settings.SMALL_TIMEOUT, ): + selector, by = page_utils.recalculate_selector(selector, by) wait_for_text_visible( driver, text.strip(), selector, by=by, timeout=timeout ) def assert_exact_text( - driver, text, selector, by="css selector", timeout=settings.SMALL_TIMEOUT + driver, + text, + selector="html", + by="css selector", + timeout=settings.SMALL_TIMEOUT, ): + selector, by = page_utils.recalculate_selector(selector, by) wait_for_exact_text_visible( driver, text.strip(), selector, by=by, timeout=timeout ) +def assert_non_empty_text( + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_non_empty_text_visible( + driver, selector, by=by, timeout=timeout + ) + + +def assert_text_not_visible( + driver, + text, + selector="html", + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_text_not_visible( + driver, text.strip(), selector, by=by, timeout=timeout + ) + + def wait_for_element( driver, selector, @@ -1748,6 +1779,7 @@ def wait_for_text( by="css selector", timeout=settings.LARGE_TIMEOUT, ): + selector, by = page_utils.recalculate_selector(selector, by) return wait_for_text_visible( driver=driver, text=text, @@ -1764,6 +1796,7 @@ def wait_for_exact_text( by="css selector", timeout=settings.LARGE_TIMEOUT, ): + selector, by = page_utils.recalculate_selector(selector, by) return wait_for_exact_text_visible( driver=driver, text=text, @@ -1779,6 +1812,7 @@ def wait_for_non_empty_text( by="css selector", timeout=settings.LARGE_TIMEOUT, ): + selector, by = page_utils.recalculate_selector(selector, by) return wait_for_non_empty_text_visible( driver=driver, selector=selector, diff --git a/seleniumbase/js_code/active_css_js.py b/seleniumbase/js_code/active_css_js.py index 7186956a293..996a00b3cec 100644 --- a/seleniumbase/js_code/active_css_js.py +++ b/seleniumbase/js_code/active_css_js.py @@ -103,7 +103,7 @@ return /\d/.test(str); }; function isGen(str) { - return /[_-]\d/.test(str); + return /[_-]\d/.test(str) || /\d[a-z]/.test(str); }; function tagName(el) { return el.tagName.toLowerCase(); diff --git a/seleniumbase/js_code/recorder_js.py b/seleniumbase/js_code/recorder_js.py index d009e301f17..a127e98abb7 100644 --- a/seleniumbase/js_code/recorder_js.py +++ b/seleniumbase/js_code/recorder_js.py @@ -103,7 +103,7 @@ return /\d/.test(str); }; function isGen(str) { - return /[_-]\d/.test(str); + return /[_-]\d/.test(str) || /\d[a-z]/.test(str); }; function tagName(el) { return el.tagName.toLowerCase(); diff --git a/seleniumbase/plugins/driver_manager.py b/seleniumbase/plugins/driver_manager.py index 90e532d45a4..598077f362c 100644 --- a/seleniumbase/plugins/driver_manager.py +++ b/seleniumbase/plugins/driver_manager.py @@ -36,6 +36,7 @@ ########################################################################### """ +import os import sys @@ -124,6 +125,7 @@ def Driver( uc_cdp=None, # Shortcut / Duplicate of "uc_cdp_events". uc_sub=None, # Shortcut / Duplicate of "uc_subprocess". log_cdp=None, # Shortcut / Duplicate of "log_cdp_events". + server=None, # Shortcut / Duplicate of "servername". wire=None, # Shortcut / Duplicate of "use_wire". pls=None, # Shortcut / Duplicate of "page_load_strategy". ): @@ -246,6 +248,8 @@ def Driver( headless2 = False if protocol is None: protocol = "http" # For the Selenium Grid only! + if server is not None and servername is None: + servername = server if servername is None: servername = "localhost" # For the Selenium Grid only! use_grid = False @@ -328,37 +332,6 @@ def Driver( ): recorder_mode = True recorder_ext = True - if headed is None: - # Override the default headless mode on Linux if set. - if "--gui" in sys_argv or "--headed" in sys_argv: - headed = True - else: - headed = False - if ( - shared_utils.is_linux() - and not headed - and not headless - and not headless2 - ): - headless = True - if recorder_mode and headless: - headless = False - headless2 = True - if headless2 and browser == "firefox": - headless2 = False # Only for Chromium browsers - headless = True # Firefox has regular headless - elif browser not in ["chrome", "edge"]: - headless2 = False # Only for Chromium browsers - if disable_csp is None: - disable_csp = False - if ( - (enable_ws is None and disable_ws is None) - or (disable_ws is not None and not disable_ws) - or (enable_ws is not None and enable_ws) - ): - enable_ws = True - else: - enable_ws = False if ( undetectable or undetected @@ -414,6 +387,50 @@ def Driver( uc_cdp_events = True else: uc_cdp_events = False + if undetectable and browser != "chrome": + message = ( + '\n Undetected-Chromedriver Mode ONLY supports Chrome!' + '\n ("uc=True" / "undetectable=True" / "--uc")' + '\n (Your browser choice was: "%s".)' + '\n (Will use "%s" without UC Mode.)\n' % (browser, browser) + ) + print(message) + if headed is None: + # Override the default headless mode on Linux if set. + if "--gui" in sys_argv or "--headed" in sys_argv: + headed = True + else: + headed = False + if ( + shared_utils.is_linux() + and not headed + and not headless + and not headless2 + and ( + not undetectable + or "DISPLAY" not in os.environ.keys() + or not os.environ["DISPLAY"] + ) + ): + headless = True + if recorder_mode and headless: + headless = False + headless2 = True + if headless2 and browser == "firefox": + headless2 = False # Only for Chromium browsers + headless = True # Firefox has regular headless + elif browser not in ["chrome", "edge"]: + headless2 = False # Only for Chromium browsers + if disable_csp is None: + disable_csp = False + if ( + (enable_ws is None and disable_ws is None) + or (disable_ws is not None and not disable_ws) + or (enable_ws is not None and enable_ws) + ): + enable_ws = True + else: + enable_ws = False if log_cdp_events is None and log_cdp is None: if ( "--log-cdp-events" in sys_argv diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index a6004c6f55b..6e9e803790c 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -1703,14 +1703,17 @@ def pytest_configure(config): and not sb_config.headless2 and not sb_config.xvfb ): - print( - "(Linux uses --headless by default. " - "To override, use --headed / --gui. " - "For Xvfb mode instead, use --xvfb. " - "Or you can hide this info by using " - "--headless / --headless2.)" - ) - sb_config.headless = True + if not sb_config.undetectable: + print( + "(Linux uses --headless by default. " + "To override, use --headed / --gui. " + "For Xvfb mode instead, use --xvfb. " + "Or you can hide this info by using " + "--headless / --headless2 / --uc.)" + ) + sb_config.headless = True + else: + sb_config.xvfb = True # Recorder Mode can still optimize scripts in --headless2 mode. if sb_config.recorder_mode and sb_config.headless: diff --git a/seleniumbase/plugins/sb_manager.py b/seleniumbase/plugins/sb_manager.py index 2fd8d9133e0..b823d564c2d 100644 --- a/seleniumbase/plugins/sb_manager.py +++ b/seleniumbase/plugins/sb_manager.py @@ -103,6 +103,7 @@ def SB( uc_cdp=None, # Shortcut / Duplicate of "uc_cdp_events". uc_sub=None, # Shortcut / Duplicate of "uc_subprocess". log_cdp=None, # Shortcut / Duplicate of "log_cdp_events". + server=None, # Shortcut / Duplicate of "servername". wire=None, # Shortcut / Duplicate of "use_wire". pls=None, # Shortcut / Duplicate of "page_load_strategy". sjw=None, # Shortcut / Duplicate of "skip_js_waits". @@ -282,6 +283,8 @@ def SB( headless2 = False if protocol is None: protocol = "http" # For the Selenium Grid only! + if server is not None and servername is None: + servername = server if servername is None: servername = "localhost" # For the Selenium Grid only! if port is None: @@ -386,6 +389,69 @@ def SB( if not shared_utils.is_linux(): # The Xvfb virtual display server is for Linux OS Only! xvfb = False + if ( + undetectable + or undetected + or uc + or uc_cdp_events + or uc_cdp + or uc_subprocess + or uc_sub + ): + undetectable = True + if ( + (undetectable or undetected or uc) + and (uc_subprocess is None) + and (uc_sub is None) + ): + uc_subprocess = True # Use UC as a subprocess by default. + elif ( + "--undetectable" in sys_argv + or "--undetected" in sys_argv + or "--uc" in sys_argv + or "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + undetectable = True + if uc_subprocess is None and uc_sub is None: + uc_subprocess = True # Use UC as a subprocess by default. + else: + undetectable = False + if uc_subprocess or uc_sub: + uc_subprocess = True + elif ( + "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + uc_subprocess = True + else: + uc_subprocess = False + if uc_cdp_events or uc_cdp: + undetectable = True + uc_cdp_events = True + elif ( + "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc_cdp" in sys_argv + ): + undetectable = True + uc_cdp_events = True + else: + uc_cdp_events = False + if undetectable and browser != "chrome": + message = ( + '\n Undetected-Chromedriver Mode ONLY supports Chrome!' + '\n ("uc=True" / "undetectable=True" / "--uc")' + '\n (Your browser choice was: "%s".)' + '\n (Will use "%s" without UC Mode.)\n' % (browser, browser) + ) + print(message) if headed is None: # Override the default headless mode on Linux if set. if "--gui" in sys_argv or "--headed" in sys_argv: @@ -399,7 +465,10 @@ def SB( and not headless2 and not xvfb ): - headless = True + if not undetectable: + headless = True + else: + xvfb = True if headless2 and browser == "firefox": headless2 = False # Only for Chromium browsers headless = True # Firefox has regular headless @@ -456,61 +525,6 @@ def SB( else: enable_ws = False disable_ws = True - if ( - undetectable - or undetected - or uc - or uc_cdp_events - or uc_cdp - or uc_subprocess - or uc_sub - ): - undetectable = True - if ( - (undetectable or undetected or uc) - and (uc_subprocess is None) - and (uc_sub is None) - ): - uc_subprocess = True # Use UC as a subprocess by default. - elif ( - "--undetectable" in sys_argv - or "--undetected" in sys_argv - or "--uc" in sys_argv - or "--uc-cdp-events" in sys_argv - or "--uc_cdp_events" in sys_argv - or "--uc-cdp" in sys_argv - or "--uc-subprocess" in sys_argv - or "--uc_subprocess" in sys_argv - or "--uc-sub" in sys_argv - ): - undetectable = True - if uc_subprocess is None and uc_sub is None: - uc_subprocess = True # Use UC as a subprocess by default. - else: - undetectable = False - if uc_subprocess or uc_sub: - uc_subprocess = True - elif ( - "--uc-subprocess" in sys_argv - or "--uc_subprocess" in sys_argv - or "--uc-sub" in sys_argv - ): - uc_subprocess = True - else: - uc_subprocess = False - if uc_cdp_events or uc_cdp: - undetectable = True - uc_cdp_events = True - elif ( - "--uc-cdp-events" in sys_argv - or "--uc_cdp_events" in sys_argv - or "--uc-cdp" in sys_argv - or "--uc_cdp" in sys_argv - ): - undetectable = True - uc_cdp_events = True - else: - uc_cdp_events = False if log_cdp_events is None and log_cdp is None: if ( "--log-cdp-events" in sys_argv diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index c3cf01f5fc9..b169316c616 100644 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -1204,15 +1204,19 @@ def beforeTest(self, test): and not self.options.headless2 and not self.options.xvfb ): - print( - "(Linux uses --headless by default. " - "To override, use --headed / --gui. " - "For Xvfb mode instead, use --xvfb. " - "Or you can hide this info by using " - "--headless / --headless2.)" - ) - self.options.headless = True - test.test.headless = True + if not self.options.undetectable: + print( + "(Linux uses --headless by default. " + "To override, use --headed / --gui. " + "For Xvfb mode instead, use --xvfb. " + "Or you can hide this info by using " + "--headless / --headless2 / --uc.)" + ) + self.options.headless = True + test.test.headless = True + else: + self.options.xvfb = True + test.test.xvfb = True if self.options.use_wire and self.options.undetectable: print( "\n" diff --git a/seleniumbase/undetected/__init__.py b/seleniumbase/undetected/__init__.py index 7e58bc3786f..be6f4fb0766 100644 --- a/seleniumbase/undetected/__init__.py +++ b/seleniumbase/undetected/__init__.py @@ -437,10 +437,12 @@ def reconnect(self, timeout=0.1): self.service.start() except Exception: pass + time.sleep(0.012) try: self.start_session() except Exception: pass + time.sleep(0.012) def disconnect(self): """Stops the chromedriver service that runs in the background. @@ -450,6 +452,7 @@ def disconnect(self): self.service.stop() except Exception: pass + time.sleep(0.012) def connect(self): """Starts the chromedriver service that runs in the background @@ -459,10 +462,12 @@ def connect(self): self.service.start() except Exception: pass + time.sleep(0.012) try: self.start_session() except Exception: pass + time.sleep(0.012) def start_session(self, capabilities=None): if not capabilities: diff --git a/setup.py b/setup.py index 79f7d56935d..fb73d9e309a 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ print("\nERROR! Publishing to PyPI requires Python>=3.9") sys.exit() print("\n*** Checking code health with flake8:\n") - os.system("python -m pip install 'flake8==7.0.0'") + os.system("python -m pip install 'flake8==7.1.0'") flake8_status = os.system("flake8 --exclude=recordings,temp") if flake8_status != 0: print("\nERROR! Fix flake8 issues before publishing to PyPI!\n") @@ -146,21 +146,23 @@ ], python_requires=">=3.7", install_requires=[ - 'pip>=24.0', - 'packaging>=24.0', + 'pip>=24.0;python_version<"3.8"', + 'pip>=24.1;python_version>="3.8"', + 'packaging>=24.0;python_version<"3.8"', + 'packaging>=24.1;python_version>="3.8"', 'setuptools>=68.0.0;python_version<"3.8"', - 'setuptools>=70.0.0;python_version>="3.8"', + 'setuptools>=70.1.0;python_version>="3.8"', 'wheel>=0.42.0;python_version<"3.8"', 'wheel>=0.43.0;python_version>="3.8"', 'attrs>=23.2.0', "certifi>=2024.6.2", "exceptiongroup>=1.2.1", 'filelock>=3.12.2;python_version<"3.8"', - 'filelock>=3.14.0;python_version>="3.8"', + 'filelock>=3.15.4;python_version>="3.8"', 'platformdirs>=4.0.0;python_version<"3.8"', 'platformdirs>=4.2.2;python_version>="3.8"', 'typing-extensions>=4.12.2;python_version>="3.8"', - 'parse>=1.20.1', + 'parse>=1.20.2', 'parse-type>=0.6.2', 'pyyaml>=6.0.1', "six==1.16.0", @@ -178,8 +180,9 @@ 'trio==0.25.1;python_version>="3.8"', 'trio-websocket==0.11.1', 'wsproto==1.2.0', + 'websocket-client==1.8.0;python_version>="3.8"', 'selenium==4.11.2;python_version<"3.8"', - 'selenium==4.21.0;python_version>="3.8"', + 'selenium==4.22.0;python_version>="3.8"', 'cssselect==1.2.0', "sortedcontainers==2.4.0", 'fasteners==0.19', @@ -212,6 +215,7 @@ "pdbp==1.5.0", 'colorama==0.4.6', 'pyotp==2.9.0', + 'python-xlib==0.33;platform_system=="Linux"', 'markdown-it-py==2.2.0;python_version<"3.8"', 'markdown-it-py==3.0.0;python_version>="3.8"', 'mdurl==0.1.2', @@ -230,7 +234,7 @@ # Usage: coverage run -m pytest; coverage html; coverage report "coverage": [ 'coverage==7.2.7;python_version<"3.8"', - 'coverage>=7.5.3;python_version>="3.8"', + 'coverage>=7.5.4;python_version>="3.8"', 'pytest-cov==4.1.0;python_version<"3.8"', 'pytest-cov>=5.0.0;python_version>="3.8"', ], @@ -238,12 +242,12 @@ # Usage: flake8 "flake8": [ 'flake8==5.0.4;python_version<"3.9"', - 'flake8==7.0.0;python_version>="3.9"', + 'flake8==7.1.0;python_version>="3.9"', "mccabe==0.7.0", 'pyflakes==2.5.0;python_version<"3.9"', 'pyflakes==3.2.0;python_version>="3.9"', 'pycodestyle==2.9.1;python_version<"3.9"', - 'pycodestyle==2.11.1;python_version>="3.9"', + 'pycodestyle==2.12.0;python_version>="3.9"', ], # pip install -e .[ipdb] # (Not needed for debugging anymore. SeleniumBase now includes "pdbp".) @@ -284,6 +288,10 @@ "psutil": [ "psutil==5.9.8", ], + # pip install -e .[pyautogui] + "pyautogui": [ + "PyAutoGUI==0.9.54", + ], # pip install -e .[selenium-stealth] "selenium-stealth": [ 'selenium-stealth==1.0.6',