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',