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