diff --git a/README.md b/README.md
index 20505d65ac9..12302f96b57 100755
--- a/README.md
+++ b/README.md
@@ -103,7 +103,7 @@ with SB(uc=True, test=True, locale="en") as sb:
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('label[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=4)
```
@@ -118,13 +118,11 @@ url = "https://gitlab.com/users/sign_in"
sb = sb_cdp.Chrome(url)
sb.sleep(2.5)
sb.gui_click_captcha()
-sb.highlight('h1:contains("GitLab.com")')
+sb.highlight('h1:contains("GitLab")')
sb.highlight('button:contains("Sign in")')
sb.driver.stop()
```
-> (Due to changes in Chrome 137 where the `--load-extension` switch was removed, you can't load extensions directly from this format.)
-
--------
📗 Here's SeleniumBase/examples/test_get_swag.py, which tests an e-commerce site:
diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md
index 5eaf8925295..c111ad04aef 100644
--- a/examples/cdp_mode/ReadMe.md
+++ b/examples/cdp_mode/ReadMe.md
@@ -460,10 +460,14 @@ sb.cdp.get_element_attributes(selector)
sb.cdp.get_element_attribute(selector, attribute)
sb.cdp.get_attribute(selector, attribute)
sb.cdp.get_element_html(selector)
+sb.cdp.get_mfa_code(totp_key=None)
+sb.cdp.enter_mfa_code(selector, totp_key=None, timeout=None)
sb.cdp.set_locale(locale)
sb.cdp.set_local_storage_item(key, value)
sb.cdp.set_session_storage_item(key, value)
sb.cdp.set_attributes(selector, attribute, value)
+sb.cdp.is_attribute_present(selector, attribute, value=None)
+sb.cdp.is_online()
sb.cdp.gui_press_key(key)
sb.cdp.gui_press_keys(keys)
sb.cdp.gui_write(text)
diff --git a/examples/cdp_mode/raw_cdp_gitlab.py b/examples/cdp_mode/raw_cdp_gitlab.py
index 9ff36aaa194..893753aefd5 100644
--- a/examples/cdp_mode/raw_cdp_gitlab.py
+++ b/examples/cdp_mode/raw_cdp_gitlab.py
@@ -4,6 +4,6 @@
sb = sb_cdp.Chrome(url, incognito=True)
sb.sleep(2.2)
sb.gui_click_captcha()
-sb.highlight('h1:contains("GitLab.com")')
+sb.highlight('h1:contains("GitLab")')
sb.highlight('button:contains("Sign in")')
sb.driver.stop()
diff --git a/examples/cdp_mode/raw_gitlab.py b/examples/cdp_mode/raw_gitlab.py
index 5c043fa6b2b..e5aa6293af7 100644
--- a/examples/cdp_mode/raw_gitlab.py
+++ b/examples/cdp_mode/raw_gitlab.py
@@ -9,5 +9,5 @@
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('label[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=4)
diff --git a/examples/cdp_mode/raw_mfa_login.py b/examples/cdp_mode/raw_mfa_login.py
new file mode 100644
index 00000000000..def87484d01
--- /dev/null
+++ b/examples/cdp_mode/raw_mfa_login.py
@@ -0,0 +1,12 @@
+from seleniumbase import sb_cdp
+
+url = "https://seleniumbase.io/realworld/login"
+sb = sb_cdp.Chrome(url)
+sb.type("#username", "demo_user")
+sb.type("#password", "secret_pass")
+sb.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG")
+sb.assert_text("Welcome!", "h1")
+sb.click('a:contains("This Page")')
+sb.highlight("h1")
+sb.highlight("img#image1")
+sb.driver.stop()
diff --git a/examples/cdp_mode/raw_proxy.py b/examples/cdp_mode/raw_proxy.py
index 1e3cb562fa8..ce403806d88 100644
--- a/examples/cdp_mode/raw_proxy.py
+++ b/examples/cdp_mode/raw_proxy.py
@@ -43,6 +43,7 @@ def main():
if row.strip() != "":
data.append(row.strip())
print("\n".join(data).replace('\n"', ' "'))
+ sb.click_if_visible(pop_up)
sb.sleep(3)
sb.driver.stop()
diff --git a/examples/presenter/uc_presentation.py b/examples/presenter/uc_presentation.py
index 554cefcfa90..c9f88546b79 100644
--- a/examples/presenter/uc_presentation.py
+++ b/examples/presenter/uc_presentation.py
@@ -353,7 +353,7 @@ def test_presentation(self):
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=4)
self.create_presentation(theme="serif", transition="fade")
diff --git a/examples/presenter/uc_presentation_3.py b/examples/presenter/uc_presentation_3.py
index 99815938ebb..cf1b1a2ade6 100644
--- a/examples/presenter/uc_presentation_3.py
+++ b/examples/presenter/uc_presentation_3.py
@@ -63,7 +63,7 @@ def test_presentation_3(self):
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=4)
self.create_presentation(theme="serif", transition="none")
@@ -91,7 +91,7 @@ def test_presentation_3(self):
' sb.assert_element(\'[for="user_login"]\')\n'
' sb.highlight(\'button:contains("Sign in")\')'
'\n'
- ' sb.highlight(\'h1:contains("GitLab.com")\')'
+ ' sb.highlight(\'h1:contains("GitLab")\')'
'\n'
' sb.post_message("SeleniumBase wasn\'t detected",'
' duration=4)\n'
diff --git a/examples/presenter/uc_presentation_4.py b/examples/presenter/uc_presentation_4.py
index a5889899ebb..496c1e1281c 100644
--- a/examples/presenter/uc_presentation_4.py
+++ b/examples/presenter/uc_presentation_4.py
@@ -390,7 +390,7 @@ def test_presentation_4(self):
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('label[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=8)
self.create_presentation(theme="serif", transition="none")
@@ -528,6 +528,12 @@ def test_presentation_4(self):
required_text = "Catan"
sb.cdp.press_keys('input[aria-label="Search"]', search + "\n")
sb.sleep(3.8)
+ if sb.is_element_visible("#px-captcha"):
+ sb.cdp.gui_click_and_hold("#px-captcha", 12)
+ sb.sleep(3.2)
+ if sb.is_element_visible("#px-captcha"):
+ sb.cdp.gui_click_and_hold("#px-captcha", 12)
+ sb.sleep(3.2)
sb.cdp.remove_elements('[data-testid="skyline-ad"]')
print('*** Walmart Search for "%s":' % search)
print(' (Results must contain "%s".)' % required_text)
diff --git a/examples/proxy_test.py b/examples/proxy_test.py
index d683512703b..909b9d3fa04 100644
--- a/examples/proxy_test.py
+++ b/examples/proxy_test.py
@@ -45,4 +45,5 @@ def test_proxy(self):
if row.strip() != "":
data.append(row.strip())
print("\n".join(data).replace('\n"', ' "'))
+ self.click_if_visible(pop_up)
self.sleep(3)
diff --git a/examples/raw_uc_mode.py b/examples/raw_uc_mode.py
index 90dee460726..3634bd605d7 100644
--- a/examples/raw_uc_mode.py
+++ b/examples/raw_uc_mode.py
@@ -8,5 +8,5 @@
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('label[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=4)
diff --git a/help_docs/uc_mode.md b/help_docs/uc_mode.md
index 53e6f78cfe7..611f04290c9 100644
--- a/help_docs/uc_mode.md
+++ b/help_docs/uc_mode.md
@@ -73,7 +73,7 @@ with SB(uc=True, test=True) as sb:
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('label[for="user_login"]')
sb.highlight('button:contains("Sign in")')
- sb.highlight('h1:contains("GitLab.com")')
+ sb.highlight('h1:contains("GitLab")')
sb.post_message("SeleniumBase wasn't detected", duration=4)
```
diff --git a/requirements.txt b/requirements.txt
index 28f95bedb0f..fd06570103f 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,8 @@ packaging>=25.0
setuptools~=70.2;python_version<"3.10"
setuptools>=80.9.0;python_version>="3.10"
wheel>=0.45.1
-attrs>=25.3.0
+attrs~=25.3.0;python_version<"3.9"
+attrs>=25.4.0;python_version>="3.9"
certifi>=2025.10.5
exceptiongroup>=1.3.0
websockets~=13.1;python_version<"3.9"
diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py
index 03340df3a52..f731a2b60ad 100755
--- a/seleniumbase/__version__.py
+++ b/seleniumbase/__version__.py
@@ -1,2 +1,2 @@
# seleniumbase package
-__version__ = "4.42.1"
+__version__ = "4.42.2"
diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py
index 18d1983fe4d..1edd5b9c28b 100644
--- a/seleniumbase/core/browser_launcher.py
+++ b/seleniumbase/core/browser_launcher.py
@@ -766,6 +766,8 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs):
cdp.set_local_storage_item = CDPM.set_local_storage_item
cdp.set_session_storage_item = CDPM.set_session_storage_item
cdp.set_attributes = CDPM.set_attributes
+ cdp.is_attribute_present = CDPM.is_attribute_present
+ cdp.is_online = CDPM.is_online
cdp.gui_press_key = CDPM.gui_press_key
cdp.gui_press_keys = CDPM.gui_press_keys
cdp.gui_write = CDPM.gui_write
@@ -815,10 +817,12 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs):
cdp.get_screen_rect = CDPM.get_screen_rect
cdp.get_window_rect = CDPM.get_window_rect
cdp.get_window_size = CDPM.get_window_size
+ cdp.get_mfa_code = CDPM.get_mfa_code
cdp.nested_click = CDPM.nested_click
cdp.select_option_by_text = CDPM.select_option_by_text
cdp.select_option_by_index = CDPM.select_option_by_index
cdp.select_option_by_value = CDPM.select_option_by_value
+ cdp.enter_mfa_code = CDPM.enter_mfa_code
cdp.flash = CDPM.flash
cdp.highlight = CDPM.highlight
cdp.focus = CDPM.focus
diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py
index b98f38b7d70..20e2573f6aa 100644
--- a/seleniumbase/core/sb_cdp.py
+++ b/seleniumbase/core/sb_cdp.py
@@ -1393,6 +1393,17 @@ def get_element_html(self, selector):
)
)
+ def get_mfa_code(self, totp_key=None):
+ """Returns a time-based one-time password based on the Google
+ Authenticator algorithm for multi-factor authentication."""
+ return shared_utils.get_mfa_code(totp_key)
+
+ def enter_mfa_code(self, selector, totp_key=None, timeout=None):
+ if not timeout:
+ timeout = settings.SMALL_TIMEOUT
+ mfa_code = self.get_mfa_code(totp_key)
+ self.type(selector, mfa_code + "\n", timeout=timeout)
+
def set_locale(self, locale):
"""(Settings will take effect on the next page load)"""
self.loop.run_until_complete(self.page.set_locale(locale))
@@ -1430,6 +1441,26 @@ def set_attributes(self, selector, attribute, value):
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
+ def is_attribute_present(self, selector, attribute, value=None):
+ try:
+ element = self.find_element(selector, timeout=0.1)
+ found_value = element.get_attribute(attribute)
+ if found_value is None:
+ return False
+ if value is not None:
+ if found_value == value:
+ return True
+ else:
+ return False
+ else:
+ return True
+ except Exception:
+ return False
+
+ def is_online(self):
+ js_code = "navigator.onLine;"
+ return self.loop.run_until_complete(self.page.evaluate(js_code))
+
def __make_sure_pyautogui_lock_is_writable(self):
with suppress(Exception):
shared_utils.make_writable(constants.MultiBrowser.PYAUTOGUILOCK)
diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py
index 75f26938bb9..5803b437c6e 100644
--- a/seleniumbase/fixtures/base_case.py
+++ b/seleniumbase/fixtures/base_case.py
@@ -1503,6 +1503,10 @@ def is_attribute_present(
):
"""Returns True if the element attribute/value is found.
If the value is not specified, the attribute only needs to exist."""
+ if self.__is_cdp_swap_needed():
+ return self.cdp.is_attribute_present(
+ selector, attribute, value=value
+ )
self.wait_for_ready_state_complete()
time.sleep(0.01)
selector, by = self.__recalculate_selector(selector, by)
@@ -8510,33 +8514,9 @@ def get_chromium_driver_version(self):
def get_mfa_code(self, totp_key=None):
"""Same as get_totp_code() and get_google_auth_password().
- Returns a time-based one-time password based on the
- Google Authenticator algorithm for multi-factor authentication.
- If the "totp_key" is not specified, this method defaults
- to using the one provided in [seleniumbase/config/settings.py].
- Google Authenticator codes expire & change at 30-sec intervals.
- If the fetched password expires in the next 1.2 seconds, waits
- for a new one before returning it (may take up to 1.2 seconds).
- See https://pyotp.readthedocs.io/en/latest/ for details."""
- import pyotp
-
- if not totp_key:
- totp_key = settings.TOTP_KEY
-
- epoch_interval = time.time() / 30.0
- cycle_lifespan = float(epoch_interval) - int(epoch_interval)
- if float(cycle_lifespan) > 0.96:
- # Password expires in the next 1.2 seconds. Wait for a new one.
- for i in range(30):
- time.sleep(0.04)
- epoch_interval = time.time() / 30.0
- cycle_lifespan = float(epoch_interval) - int(epoch_interval)
- if not float(cycle_lifespan) > 0.96:
- # The new password cycle has begun
- break
-
- totp = pyotp.TOTP(totp_key)
- return str(totp.now())
+ Returns a time-based one-time password based on the Google
+ Authenticator algorithm for multi-factor authentication."""
+ return shared_utils.get_mfa_code(totp_key)
def enter_mfa_code(
self, selector, totp_key=None, by="css selector", timeout=None
diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py
index a1c28c17ef8..5400250970f 100644
--- a/seleniumbase/fixtures/page_actions.py
+++ b/seleniumbase/fixtures/page_actions.py
@@ -193,6 +193,10 @@ def is_attribute_present(
@Returns
Boolean (is attribute present)
"""
+ if __is_cdp_swap_needed(driver):
+ return driver.cdp.is_attribute_present(
+ selector, attribute, value=value
+ )
_reconnect_if_disconnected(driver)
try:
element = driver.find_element(by=by, value=selector)
diff --git a/seleniumbase/fixtures/shared_utils.py b/seleniumbase/fixtures/shared_utils.py
index e49e904a93b..3b76f65f688 100644
--- a/seleniumbase/fixtures/shared_utils.py
+++ b/seleniumbase/fixtures/shared_utils.py
@@ -7,6 +7,7 @@
import time
from contextlib import suppress
from seleniumbase import config as sb_config
+from seleniumbase.config import settings
from seleniumbase.fixtures import constants
@@ -29,6 +30,34 @@ def pip_install(package, version=None):
)
+def get_mfa_code(totp_key=None):
+ """Returns a time-based one-time password based on the
+ Google Authenticator algorithm for multi-factor authentication.
+ If the "totp_key" is not specified, this method defaults
+ to using the one provided in [seleniumbase/config/settings.py].
+ Google Authenticator codes expire & change at 30-sec intervals.
+ If the fetched password expires in the next 1.2 seconds, waits
+ for a new one before returning it (may take up to 1.2 seconds).
+ See https://pyotp.readthedocs.io/en/latest/ for details."""
+ import pyotp
+
+ if not totp_key:
+ totp_key = settings.TOTP_KEY
+ epoch_interval = time.time() / 30.0
+ cycle_lifespan = float(epoch_interval) - int(epoch_interval)
+ if float(cycle_lifespan) > 0.96:
+ # Password expires in the next 1.2 seconds. Wait for a new one.
+ for i in range(30):
+ time.sleep(0.04)
+ epoch_interval = time.time() / 30.0
+ cycle_lifespan = float(epoch_interval) - int(epoch_interval)
+ if not float(cycle_lifespan) > 0.96:
+ # The new password cycle has begun
+ break
+ totp = pyotp.TOTP(totp_key)
+ return str(totp.now())
+
+
def is_arm_linux():
"""Returns True if machine is ARM Linux.
This will be useful once Google adds
diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py
index 019843f078c..f048087f920 100644
--- a/seleniumbase/undetected/cdp_driver/browser.py
+++ b/seleniumbase/undetected/cdp_driver/browser.py
@@ -350,7 +350,8 @@ async def get(
if _cdp_geolocation:
await connection.send(cdp.page.navigate("about:blank"))
await connection.set_geolocation(_cdp_geolocation)
- # Use the tab to navigate to new url
+ # This part isn't needed now, but may be needed later
+ """
if (
hasattr(sb_config, "_cdp_proxy")
and "@" in sb_config._cdp_proxy
@@ -360,17 +361,14 @@ async def get(
username_and_password = sb_config._cdp_proxy.split("@")[0]
proxy_user = username_and_password.split(":")[0]
proxy_pass = username_and_password.split(":")[1]
- await connection.set_auth(
- proxy_user, proxy_pass, self.tabs[0]
- )
+ await connection.set_auth(proxy_user, proxy_pass, self.tabs[0])
time.sleep(0.25)
- elif "auth" in kwargs and kwargs["auth"] and ":" in kwargs["auth"]:
+ """
+ if "auth" in kwargs and kwargs["auth"] and ":" in kwargs["auth"]:
username_and_password = kwargs["auth"]
proxy_user = username_and_password.split(":")[0]
proxy_pass = username_and_password.split(":")[1]
- await connection.set_auth(
- proxy_user, proxy_pass, self.tabs[0]
- )
+ await connection.set_auth(proxy_user, proxy_pass, self.tabs[0])
time.sleep(0.25)
frame_id, loader_id, *_ = await connection.send(
cdp.page.navigate(url)
diff --git a/setup.py b/setup.py
index 6aaef5b4569..ee5fdc07f5c 100755
--- a/setup.py
+++ b/setup.py
@@ -152,7 +152,8 @@
'setuptools~=70.2;python_version<"3.10"', # Newer ones had issues
'setuptools>=80.9.0;python_version>="3.10"',
'wheel>=0.45.1',
- 'attrs>=25.3.0',
+ 'attrs~=25.3.0;python_version<"3.9"',
+ 'attrs>=25.4.0;python_version>="3.9"',
"certifi>=2025.10.5",
"exceptiongroup>=1.3.0",
'websockets~=13.1;python_version<"3.9"',