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