diff --git a/examples/cdp_mode/raw_cdp_mobile.py b/examples/cdp_mode/raw_cdp_mobile.py new file mode 100644 index 00000000000..d872078fc6b --- /dev/null +++ b/examples/cdp_mode/raw_cdp_mobile.py @@ -0,0 +1,22 @@ +import mycdp +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +tab = sb.get_active_tab() +loop = sb.get_event_loop() +loop.run_until_complete( + tab.send( + mycdp.emulation.set_device_metrics_override( + width=411, height=731, device_scale_factor=3, mobile=True + ) + ) +) +url = "https://gitlab.com/users/sign_in" +sb.open(url) +sb.sleep(2) +sb.solve_captcha() +# (The rest is for testing and demo purposes) +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")') diff --git a/examples/cdp_mode/raw_mobile_async.py b/examples/cdp_mode/raw_mobile_async.py new file mode 100644 index 00000000000..68ba3121910 --- /dev/null +++ b/examples/cdp_mode/raw_mobile_async.py @@ -0,0 +1,34 @@ +import asyncio +import mycdp +import time +from seleniumbase import cdp_driver +from seleniumbase import decorators + + +async def main(): + url = "https://gitlab.com/users/sign_in" + driver = await cdp_driver.start_async() + await driver.main_tab.send( + mycdp.emulation.set_device_metrics_override( + width=411, height=731, device_scale_factor=3, mobile=True + ) + ) + page = await driver.get(url, lang="en") + time.sleep(3) + try: + element = await page.select('[style*="grid"] div div', timeout=1) + await element.mouse_click_with_offset_async(32, 28) + except Exception: + pass # Maybe CAPTCHA was already bypassed + element = await page.select('label[for="user_login"]') + await element.flash_async(duration=1.5, color="44EE44") + time.sleep(1) + element = await page.select('[data-testid="sign-in-button"]') + await element.flash_async(duration=2, color="44EE44") + time.sleep(2) + +if __name__ == "__main__": + # Call an async function with awaited methods + loop = asyncio.new_event_loop() + with decorators.print_runtime("raw_mobile_async.py"): + loop.run_until_complete(main()) diff --git a/examples/cdp_mode/raw_mobile_gitlab.py b/examples/cdp_mode/raw_mobile_gitlab.py new file mode 100644 index 00000000000..f3d856be48e --- /dev/null +++ b/examples/cdp_mode/raw_mobile_gitlab.py @@ -0,0 +1,24 @@ +import mycdp +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.activate_cdp_mode() + tab = sb.cdp.get_active_tab() + loop = sb.cdp.get_event_loop() + loop.run_until_complete( + tab.send( + mycdp.emulation.set_device_metrics_override( + width=411, height=731, device_scale_factor=3, mobile=True + ) + ) + ) + sb.open(url) + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + 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")') + sb.post_message("SeleniumBase wasn't detected", duration=4) diff --git a/examples/cdp_mode/raw_mobile_roblox.py b/examples/cdp_mode/raw_mobile_roblox.py new file mode 100644 index 00000000000..a67431b4d97 --- /dev/null +++ b/examples/cdp_mode/raw_mobile_roblox.py @@ -0,0 +1,24 @@ +import mycdp +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://www.roblox.com/" + agent = ( + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 " + "(KHTML, like Gecko) Mobile Safari/537.36" + ) + sb.activate_cdp_mode(agent=agent) + tab = sb.cdp.get_active_tab() + loop = sb.cdp.get_event_loop() + loop.run_until_complete( + tab.send( + mycdp.emulation.set_device_metrics_override( + width=411, height=731, device_scale_factor=3, mobile=True + ) + ) + ) + sb.open(url) + sb.assert_element("#download-the-app-container") + sb.assert_text("Roblox for Android") + sb.highlight('span:contains("Roblox for Android")', loops=8) + sb.highlight('span:contains("Continue in App")', loops=8) diff --git a/examples/cdp_mode/raw_stopandshop.py b/examples/cdp_mode/raw_stopandshop.py new file mode 100644 index 00000000000..f2ccf98c961 --- /dev/null +++ b/examples/cdp_mode/raw_stopandshop.py @@ -0,0 +1,35 @@ +"""Test Stop & Shop search. Non-US IPs might be blocked.""" +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True, ad_block=True) as sb: + url = "https://stopandshop.com/" + sb.activate_cdp_mode(url) + sb.sleep(2.6) + if not sb.is_element_present("#brand-logo_link"): + sb.refresh() + sb.sleep(2.6) + sb.wait_for_element("#brand-logo_link", timeout=3) + query = "Fresh Turkey" + required_text = "Turkey" + search_box = 'input[type="search"]' + sb.wait_for_element(search_box) + sb.sleep(1.2) + sb.press_keys(search_box, query) + sb.sleep(1.2) + sb.click("button.search-btn") + sb.sleep(3.2) + print('*** Stop & Shop Search for "%s":' % query) + print(' (Results must contain "%s".)' % required_text) + print(' (Results cannot contain "Out of Stock")') + unique_item_text = [] + item_selector = "div.product-tile_content" + items = sb.find_elements(item_selector) + for item in items: + sb.sleep(0.06) + if "Out of Stock" not in item.text: + if required_text in item.text: + item.flash(color="44CC88") + sb.sleep(0.025) + if item.text not in unique_item_text: + unique_item_text.append(item.text) + print("* " + item.text) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 75be3bc1a4b..c5c4406bf1b 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -2,7 +2,7 @@ # Minimum Python version: 3.10 (for generating docs only) regex>=2025.11.3 -pymdown-extensions>=10.17.1 +pymdown-extensions>=10.17.2 pipdeptree>=2.30.0 python-dateutil>=2.8.2 Markdown==3.10 diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 0287f511513..26308a72fba 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.44.18" +__version__ = "4.44.19" diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 9cb5047954f..d362d786ec1 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -1486,6 +1486,10 @@ def _uc_gui_click_captcha( '[data-callback="onCaptchaSuccess"]' ): frame = '[data-callback="onCaptchaSuccess"]' + elif driver.is_element_present( + "div:not([class]) > div:not([class])" + ): + frame = "div:not([class]) > div:not([class])" else: return if ( @@ -1829,6 +1833,10 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): frame = ".cf-turnstile-wrapper" elif driver.is_element_present('[class="cf-turnstile"]'): frame = '[class="cf-turnstile"]' + elif driver.is_element_present( + "div:not([class]) > div:not([class])" + ): + frame = "div:not([class]) > div:not([class])" else: return else: @@ -3048,6 +3056,11 @@ def get_driver( if _special_binary_exists(binary_location, "atlas"): driver_dir = DRIVER_DIR_ATLAS sb_config._cdp_browser = "atlas" + if undetectable and mobile_emulator: + # For stealthy mobile mode, see the CDP Mode examples + # to learn how to properly configure it. + user_agent = None # Undo the override + mobile_emulator = False # Instead, set from CDP Mode if ( hasattr(sb_config, "settings") and getattr(sb_config.settings, "NEW_DRIVER_DIR", None) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 66cedf1f29c..c0bb26a2137 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -2030,6 +2030,10 @@ def __click_captcha(self, use_cdp=False): '[data-callback="onCaptchaSuccess"]' ): selector = '[data-callback="onCaptchaSuccess"]' + elif self.is_element_present( + "div:not([class]) > div:not([class])" + ): + selector = "div:not([class]) > div:not([class])" else: return if not selector: diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 30755c50a54..cc41ef6f4cd 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -10136,6 +10136,21 @@ def assert_element(self, selector, by="css selector", timeout=None): if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) if self.__is_cdp_swap_needed(): + if self.demo_mode: + selector, by = self.__recalculate_selector( + selector, by, xp_ok=False + ) + a_t = "ASSERT" + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert(self._language) + messenger_post = "%s %s: %s" % ( + a_t, by.upper(), selector + ) + self.__highlight_with_assert_success( + messenger_post, selector, by + ) self.cdp.assert_element(selector, timeout=timeout) return True if isinstance(selector, list): @@ -10430,6 +10445,20 @@ def assert_text( messenger_post, selector, by ) elif self.__is_cdp_swap_needed(): + if self.demo_mode: + a_t = "ASSERT TEXT" + i_n = "in" + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert_text(self._language) + i_n = SD.translate_in(self._language) + messenger_post = "%s: {%s} %s %s: %s" % ( + a_t, text, i_n, by.upper(), selector + ) + self.__highlight_with_assert_success( + messenger_post, selector, by + ) self.cdp.assert_text(text, selector, timeout=timeout) return True elif self.__is_shadow_selector(selector): diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py index 0728f2dbd91..5a8915fd24a 100644 --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -282,6 +282,18 @@ def safe_execute_script(driver, script): This method will load jQuery if it wasn't already loaded.""" try: execute_script(driver, script) + except TypeError as e: + if ( + ( + shared_utils.is_cdp_swap_needed(driver) + or hasattr(driver, "_swap_driver") + ) + and "cannot unpack non-iterable" in str(e) + ): + pass + else: + activate_jquery(driver) # It's a good thing we can define it here + execute_script(driver, script) except Exception: # The likely reason this fails is because: "jQuery is not defined" activate_jquery(driver) # It's a good thing we can define it here @@ -1311,22 +1323,34 @@ def slow_scroll_to_element(driver, element, *args, **kwargs): element_location_y = None try: if shared_utils.is_cdp_swap_needed(driver): - element.get_position().y + element_location_y = element.get_position().y else: element_location_y = element.location["y"] except Exception: - element.location_once_scrolled_into_view + if shared_utils.is_cdp_swap_needed(driver): + element.scroll_into_view() + else: + element.location_once_scrolled_into_view return try: - element_location_x = element.location["x"] + if shared_utils.is_cdp_swap_needed(driver): + element_location_x = element.get_position().x + else: + element_location_x = element.location["x"] except Exception: element_location_x = 0 try: - element_width = element.size["width"] + if shared_utils.is_cdp_swap_needed(driver): + element_width = element.get_position().width + else: + element_width = element.size["width"] except Exception: element_width = 0 try: - screen_width = driver.get_window_size()["width"] + if shared_utils.is_cdp_swap_needed(driver): + screen_width = driver.cdp.get_window_size()["width"] + else: + screen_width = driver.get_window_size()["width"] except Exception: screen_width = execute_script(driver, "return window.innerWidth;") element_location_y = element_location_y - constants.Scroll.Y_OFFSET diff --git a/seleniumbase/undetected/cdp_driver/config.py b/seleniumbase/undetected/cdp_driver/config.py index 0051b49a1d1..c9d40098409 100644 --- a/seleniumbase/undetected/cdp_driver/config.py +++ b/seleniumbase/undetected/cdp_driver/config.py @@ -123,7 +123,6 @@ def __init__( self._default_browser_args = [ "--window-size=%s,%s" % (start_width, start_height), "--window-position=%s,%s" % (start_x, start_y), - "--remote-allow-origins=*", "--no-first-run", "--no-service-autorun", "--disable-auto-reload", @@ -138,8 +137,10 @@ def __init__( '--simulate-outdated-no-au="Tue, 31 Dec 2099 23:59:59 GMT"', "--password-store=basic", "--deny-permission-prompts", - "--disable-infobars", + "--disable-application-cache", + "--test-type", "--disable-breakpad", + "--disable-setuid-sandbox", "--disable-prompt-on-repost", "--disable-password-generation", "--disable-ipc-flooding-protection", @@ -149,8 +150,8 @@ def __init__( "--disable-client-side-phishing-detection", "--disable-top-sites", "--disable-translate", + "--dns-prefetch-disable", "--disable-renderer-backgrounding", - "--disable-background-networking", "--disable-dev-shm-usage", ] @@ -207,9 +208,6 @@ def __call__(self): "OptimizationHintsFetching,InterestFeedContentSuggestions," "DisableLoadExtensionCommandLineSwitch" ] - if self.proxy: - args += ["--test-type"] - args += ["--disable-session-crashed-bubble"] if self.expert: args += [ "--disable-web-security",