Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand All @@ -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.)

--------

<p align="left">📗 Here's <a href="https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_get_swag.py">SeleniumBase/examples/test_get_swag.py</a>, which tests an e-commerce site:</p>
Expand Down
4 changes: 4 additions & 0 deletions examples/cdp_mode/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion examples/cdp_mode/raw_cdp_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion examples/cdp_mode/raw_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
12 changes: 12 additions & 0 deletions examples/cdp_mode/raw_mfa_login.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions examples/cdp_mode/raw_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion examples/presenter/uc_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions examples/presenter/uc_presentation_3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -91,7 +91,7 @@ def test_presentation_3(self):
'<mk-2> sb.assert_element(\'[for="user_login"]\')</mk-2>\n'
'<mk-3> sb.highlight(\'button:contains("Sign in")\')'
'</mk-3>\n'
'<mk-4> sb.highlight(\'h1:contains("GitLab.com")\')'
'<mk-4> sb.highlight(\'h1:contains("GitLab")\')'
'</mk-4>\n'
'<mk-5> sb.post_message("SeleniumBase wasn\'t detected",'
' duration=4)</mk-5>\n'
Expand Down
8 changes: 7 additions & 1 deletion examples/presenter/uc_presentation_4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions examples/proxy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion examples/raw_uc_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion help_docs/uc_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion seleniumbase/__version__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# seleniumbase package
__version__ = "4.42.1"
__version__ = "4.42.2"
4 changes: 4 additions & 0 deletions seleniumbase/core/browser_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions seleniumbase/core/sb_cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 7 additions & 27 deletions seleniumbase/fixtures/base_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions seleniumbase/fixtures/page_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions seleniumbase/fixtures/shared_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand Down
14 changes: 6 additions & 8 deletions seleniumbase/undetected/cdp_driver/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
Expand Down