From a6e951c73c3e2b2ce1082671813692cba2d61a44 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 21 Jul 2026 22:06:43 -0400 Subject: [PATCH 1/4] Make CDP Mode super stealthy --- seleniumbase/core/sb_cdp.py | 61 ++++------ seleniumbase/undetected/cdp_driver/element.py | 105 +++++++++++++++++- 2 files changed, 121 insertions(+), 45 deletions(-) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 077fddac9c9..94c41cee093 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -500,7 +500,10 @@ def click_nth_element(self, selector, number, scroll=True): element = elements[number] if scroll: element.scroll_into_view() - element.click() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) def click_nth_visible_element(self, selector, number, scroll=True): """Finds all matching page elements and clicks the nth visible one. @@ -518,7 +521,10 @@ def click_nth_visible_element(self, selector, number, scroll=True): element = elements[number] if scroll: element.scroll_into_view() - element.click() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) def click_link(self, link_text): self.find_elements_by_text(link_text, "a")[0].click() @@ -875,41 +881,12 @@ def click(self, selector, timeout=None, scroll=True): timeout = settings.SMALL_TIMEOUT self.__slow_mode_pause_if_set() element = self.find_element(selector, timeout=timeout) - tag_name = element.tag_name - if tag_name: - tag_name = tag_name.lower().strip() - if ( - tag_name in [ - "a", - "button", - "canvas", - "div", - "input", - "label", - "li", - "path", - "span", - "svg", - ] - and "contains(" not in selector - ): - if scroll: - element.scroll_into_view() - try: - element.mouse_click() # Simulated click (NOT PyAutoGUI) - except Exception: - element.click() # Standard CDP click - else: - if scroll: - if "contains(" in selector: - element.scroll_into_view() - else: - try: - element.scroll_into_view() - except Exception: - with suppress(Exception): - self.js_scroll_into_view(selector) - element.click() # Standard CDP click + if scroll: + element.scroll_into_view() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait(0.2)) @@ -957,7 +934,10 @@ def click_visible_elements(self, selector, limit=0, scroll=True): if (width != 0 or height != 0): if scroll: element.scroll_into_view() - element.click() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) click_count += 1 time.sleep(0.044) self.__slow_mode_pause_if_set() @@ -998,7 +978,10 @@ def select_option_by_text(self, dropdown_selector, option): options = element.query_selector_all("option") for found_option in options: if found_option.text.strip() == option.strip(): - found_option.select_option() + try: + self.type(dropdown_selector, option) + except Exception: + found_option.select_option() return raise Exception( "Unable to find text option {%s} in dropdown {%s}!" diff --git a/seleniumbase/undetected/cdp_driver/element.py b/seleniumbase/undetected/cdp_driver/element.py index 3bd1b9b052c..2c0b8a2bc19 100644 --- a/seleniumbase/undetected/cdp_driver/element.py +++ b/seleniumbase/undetected/cdp_driver/element.py @@ -786,19 +786,112 @@ async def clear_input_async(self): async def send_keys_async(self, text: str): """ Send text to an input field, or any other html element. - Hint: If you ever get stuck where using py:meth:`~click` - does not work, sending the keystroke \\n or \\r\\n + Hint: If you ever get stuck where using `~click()` + does not work, sending the keystroke \n or \r\n or a spacebar works wonders! :param text: text to send :return: None """ + if self.tag_name.lower() == "textarea" and text.endswith("\r\n"): + text = text[0:-1] await self.apply("(elem) => elem.focus()") - [ + # Map non-alphanumeric symbols to the correct CDP virtual key code. + # Prevents collisions with control keys. Eg. ord('.') = 46 = VK_DELETE. + SYMBOL_MAP = { + ".": ("Period", 190), + ",": ("Comma", 188), + "-": ("Minus", 189), + "=": ("Equal", 187), + "/": ("Slash", 191), + "\\": ("Backslash", 220), + ";": ("Semicolon", 186), + "'": ("Quote", 222), + "`": ("Backquote", 192), + "[": ("BracketLeft", 219), + "]": ("BracketRight", 221), + "!": ("Digit1", 49), + "@": ("Digit2", 50), + "#": ("Digit3", 51), + "$": ("Digit4", 52), + "%": ("Digit5", 53), + "^": ("Digit6", 54), + "&": ("Digit7", 55), + "*": ("Digit8", 56), + "(": ("Digit9", 57), + ")": ("Digit0", 48), + "_": ("Minus", 189), + "+": ("Equal", 187), + "{": ("BracketLeft", 219), + "}": ("BracketRight", 221), + "|": ("Backslash", 220), + ":": ("Semicolon", 186), + '"': ("Quote", 222), + "<": ("Comma", 188), + ">": ("Period", 190), + "?": ("Slash", 191), + "~": ("Backquote", 192), + } + for char in text: + # 1. Map character to its DOM key attributes + if char in ("\r", "\n"): + key = "Enter" + code = "Enter" + vk = 13 + text_val = "\r" + elif char == "\t": + key = "Tab" + code = "Tab" + vk = 9 + text_val = "" + elif char == " ": + key = " " + code = "Space" + vk = 32 + text_val = " " + else: + key = char + text_val = char + if char.isalpha(): + code = f"Key{char.upper()}" + vk = ord(char.upper()) + elif char.isdigit(): + code = f"Digit{char}" + vk = ord(char) + elif char in SYMBOL_MAP: + code, vk = SYMBOL_MAP[char] + else: + code = "" + vk = 0 + # 2. Trigger keydown DOM event WITHOUT inserting text await self._tab.send( - cdp.input_.dispatch_key_event("char", text=char) + cdp.input_.dispatch_key_event( + type_="rawKeyDown", + key=key, + code=code, + windows_virtual_key_code=vk, + ) + ) + # 3. Trigger keypress DOM event AND insert text + if text_val: + await self._tab.send( + cdp.input_.dispatch_key_event( + type_="char", + key=key, + code=code, + text=text_val, + unmodified_text=text_val, + windows_virtual_key_code=vk, + ) + ) + # 4. Trigger keyup DOM event + await self._tab.send( + cdp.input_.dispatch_key_event( + type_="keyUp", + key=key, + code=code, + windows_virtual_key_code=vk, + ) ) - for char in list(text) - ] async def send_file_async(self, *file_paths: PathLike): """ From 1feb0db4187160e290595dfc6f05653c0f0fcc1e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 21 Jul 2026 22:11:10 -0400 Subject: [PATCH 2/4] Update CDP Mode examples --- examples/boilerplates/samples/google_test.py | 2 +- examples/cdp_mode/raw_cdp_clearcote.py | 5 +++++ examples/cdp_mode/raw_cdp_etsy.py | 1 + examples/cdp_mode/raw_chatgpt.py | 9 ++++++--- examples/cdp_mode/raw_easyjet.py | 4 ++-- examples/cdp_mode/raw_glassdoor.py | 10 ++++++---- examples/cdp_mode/raw_priceline.py | 7 ++----- examples/cdp_mode/raw_southwest.py | 4 ++-- examples/raw_google.py | 3 +-- 9 files changed, 26 insertions(+), 19 deletions(-) diff --git a/examples/boilerplates/samples/google_test.py b/examples/boilerplates/samples/google_test.py index 06c569433cd..7683d035b58 100644 --- a/examples/boilerplates/samples/google_test.py +++ b/examples/boilerplates/samples/google_test.py @@ -20,7 +20,7 @@ def test_google_dot_com(self): self.assert_title_contains("Google") self.sleep(0.05) self.save_screenshot_to_logs() # ("./latest_logs" folder) - self.type(HomePage.search_box, "GitHub") + self.press_keys(HomePage.search_box, "GitHub") self.assert_element(HomePage.search_button) self.assert_element(HomePage.feeling_lucky_button) self.click(HomePage.search_button) diff --git a/examples/cdp_mode/raw_cdp_clearcote.py b/examples/cdp_mode/raw_cdp_clearcote.py index 9ab1868edd4..26fa26ca44a 100644 --- a/examples/cdp_mode/raw_cdp_clearcote.py +++ b/examples/cdp_mode/raw_cdp_clearcote.py @@ -2,6 +2,11 @@ sb = sb_cdp.Chrome() sb.goto("https://www.clearcotelabs.com/audit") +sb.click('button[aria-label="Marker 1"]') +sb.click('button[aria-label="Marker 2"]') +sb.click('button[aria-label="Marker 3"]') +sb.press_keys("input", "audit") +sb.select_option_by_text("select", "Mouse") sb.click('button:contains("Run the audit")') sb.sleep(6) sb.assert_element("div.text-emerald-700", timeout=8) diff --git a/examples/cdp_mode/raw_cdp_etsy.py b/examples/cdp_mode/raw_cdp_etsy.py index 5369c00ab31..23302863d03 100644 --- a/examples/cdp_mode/raw_cdp_etsy.py +++ b/examples/cdp_mode/raw_cdp_etsy.py @@ -10,6 +10,7 @@ sb.click('button[aria-label="Search"]') sb.sleep(2) sb.click_if_visible('button[aria-label="Ok"]') +sb.sleep(1) soup = sb.get_beautiful_soup() items = soup.select("div.v2-listing-card__info") num = 0 diff --git a/examples/cdp_mode/raw_chatgpt.py b/examples/cdp_mode/raw_chatgpt.py index c36669294ea..e0db06c98e6 100644 --- a/examples/cdp_mode/raw_chatgpt.py +++ b/examples/cdp_mode/raw_chatgpt.py @@ -9,8 +9,8 @@ close_2 = 'button[data-testid="close-button"]' sb.click_if_visible("%s, %s" % (close_1, close_2)) query = "Compare Playwright to SeleniumBase in under 178 words" - sb.press_keys("#prompt-textarea", query) - sb.click('button[data-testid="send-button"]') + sb.press_keys("#prompt-textarea, #mobile-composer-prompt", query) + sb.click('button[data-testid="send-button"], button[aria-label*="Send"]') print('*** Input for ChatGPT: ***\n"%s"' % query) sb.sleep(3) with suppress(Exception): @@ -18,7 +18,10 @@ sb.wait_for_element_not_visible( 'button[data-testid="stop-button"]', timeout=20 ) - chat = sb.find_element('[data-message-author-role="assistant"] .markdown') + chat = sb.find_element( + '[data-message-author-role="assistant"] .markdown, ' + '[data-assistant-markdown]' + ) soup = sb.get_beautiful_soup(chat.get_html()).text.strip() soup = soup.replace("\n\n\n", "\n\n") print("*** Response from ChatGPT: ***\n%s" % soup) diff --git a/examples/cdp_mode/raw_easyjet.py b/examples/cdp_mode/raw_easyjet.py index d747dfa37cd..848d6dccac0 100644 --- a/examples/cdp_mode/raw_easyjet.py +++ b/examples/cdp_mode/raw_easyjet.py @@ -8,11 +8,11 @@ sb.sleep(1) sb.click('input[name="from"]') sb.sleep(1) - sb.type('input[name="from"]', "London Gatwick") + sb.press_keys('input[name="from"]', "London Gatwick") sb.sleep(1) sb.click('span[data-testid="airport-name"]') sb.sleep(1) - sb.type('input[name="to"]', "Paris") + sb.press_keys('input[name="to"]', "Paris") sb.sleep(1) sb.click('span[data-testid="airport-name"]') sb.sleep(1) diff --git a/examples/cdp_mode/raw_glassdoor.py b/examples/cdp_mode/raw_glassdoor.py index 5dcfe23dbde..f7d7b63c9f7 100644 --- a/examples/cdp_mode/raw_glassdoor.py +++ b/examples/cdp_mode/raw_glassdoor.py @@ -12,11 +12,13 @@ sb.highlight('[data-test="sign-in-button"]') sb.highlight('[data-test="company-search-autocomplete"]') sb.press_keys("#employer-autocomplete", "NASA Goddard\n") - sb.sleep(0.5) - sb.click('button[data-role-variant="primary"] span:contains("Search")') - sb.sleep(2) + sb.sleep(1) + sb.click_if_visible( + '[data-role-variant="primary"] span:contains("Search")' + ) + sb.sleep(1) sb.click_if_visible('[aria-label*="NASA"] img') - sb.sleep(2) + sb.sleep(1) print(sb.get_page_title()) sb.save_as_pdf_to_logs() sb.save_page_source_to_logs() diff --git a/examples/cdp_mode/raw_priceline.py b/examples/cdp_mode/raw_priceline.py index f4489be22b5..cbb4818268d 100644 --- a/examples/cdp_mode/raw_priceline.py +++ b/examples/cdp_mode/raw_priceline.py @@ -1,5 +1,4 @@ -"""An example of bypassing PerimeterX detection on Priceline. -(PyAutoGUI is installed at runtime if it's not installed.)""" +"""An example of bypassing PerimeterX detection on Priceline.""" from seleniumbase import SB with SB(uc=True, test=True, locale="en", guest=True, pls="none") as sb: @@ -7,11 +6,9 @@ sb.goto("https://www.priceline.com") sb.sleep(3) input_selector = "div.location-input input" - sb.gui_hover_element(input_selector) - sb.mouse_click(input_selector) + sb.click(input_selector) location = "Portland, OR" selection = "Oregon, United States" # (Dropdown option) - sb.gui_hover_element(input_selector) sb.press_keys(input_selector, location) sb.sleep(0.5) sb.click(selection) diff --git a/examples/cdp_mode/raw_southwest.py b/examples/cdp_mode/raw_southwest.py index bf571949f47..df64c01bd38 100644 --- a/examples/cdp_mode/raw_southwest.py +++ b/examples/cdp_mode/raw_southwest.py @@ -6,8 +6,8 @@ sb.activate_cdp_mode() sb.goto("https://www.southwest.com/air/booking/") sb.sleep(2.8) - origin = "BOS" - destination = "MDW" + origin = "DEN" + destination = "LAX" sb.click_if_visible("button#onetrust-accept-btn-handler") sb.sleep(0.5) sb.gui_click_element("input#originationAirportCode") diff --git a/examples/raw_google.py b/examples/raw_google.py index 105772755a2..69c4d0f5db9 100644 --- a/examples/raw_google.py +++ b/examples/raw_google.py @@ -4,8 +4,7 @@ sb.activate_cdp_mode() sb.goto("https://google.com/ncr") sb.click_if_visible('button:contains("Accept all")') - sb.type('[name="q"]', "SeleniumBase GitHub page") - sb.click('[value="Google Search"]') + sb.press_keys('[name="q"]', "SeleniumBase GitHub page\n") sb.sleep(4) # The "AI Overview" sometimes loads print(sb.get_page_title()) sb.save_as_pdf_to_logs() From e08ed3a3baeba129ad2ecc155b5c9c26df8a1ea1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 21 Jul 2026 22:11:29 -0400 Subject: [PATCH 3/4] Refresh Python dependencies --- mkdocs_build/requirements.txt | 2 +- requirements.txt | 6 +++--- setup.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 63063704d43..d6679e08474 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -3,7 +3,7 @@ regex>=2026.7.19 pymdown-extensions>=10.21.3 -pipdeptree>=4.0.0 +pipdeptree>=4.1.0 python-dateutil>=2.8.2 click>=8.4.2 Markdown==3.10.2 diff --git a/requirements.txt b/requirements.txt index 9ee92aa1526..2f3472c7856 100755 --- a/requirements.txt +++ b/requirements.txt @@ -10,12 +10,12 @@ exceptiongroup>=1.3.1 websockets~=15.0.1;python_version<"3.10" websockets>=16.1.1;python_version>="3.10" filelock~=3.19.1;python_version<"3.10" -filelock>=3.31.1;python_version>="3.10" +filelock>=3.32.0;python_version>="3.10" fasteners>=0.20 mycdp>=1.4.0 pynose>=1.5.5 platformdirs~=4.4.0;python_version<"3.10" -platformdirs>=4.10.1;python_version>="3.10" +platformdirs>=4.11.0;python_version>="3.10" typing-extensions>=4.16.0 sbvirtualdisplay>=1.4.0 MarkupSafe>=3.0.3 @@ -67,7 +67,7 @@ pytest-xdist==3.8.0 parameterized==0.9.0 behave==1.2.6 soupsieve~=2.8.4;python_version<"3.10" -soupsieve~=2.9.0;python_version>="3.10" +soupsieve~=2.9.1;python_version>="3.10" beautifulsoup4~=4.15.0 pyotp~=2.10.0 python-xlib==0.33;platform_system=="Linux" diff --git a/setup.py b/setup.py index bbd962cd41e..40b697d209f 100755 --- a/setup.py +++ b/setup.py @@ -174,12 +174,12 @@ 'websockets~=15.0.1;python_version<"3.10"', 'websockets>=16.1.1;python_version>="3.10"', 'filelock~=3.19.1;python_version<"3.10"', - 'filelock>=3.31.1;python_version>="3.10"', + 'filelock>=3.32.0;python_version>="3.10"', 'fasteners>=0.20', 'mycdp>=1.4.0', 'pynose>=1.5.5', 'platformdirs~=4.4.0;python_version<"3.10"', - 'platformdirs>=4.10.1;python_version>="3.10"', + 'platformdirs>=4.11.0;python_version>="3.10"', 'typing-extensions>=4.16.0', 'sbvirtualdisplay>=1.4.0', 'MarkupSafe>=3.0.3', @@ -231,7 +231,7 @@ 'parameterized==0.9.0', 'behave==1.2.6', # Newer ones had issues 'soupsieve~=2.8.4;python_version<"3.10"', - 'soupsieve~=2.9.0;python_version>="3.10"', + 'soupsieve~=2.9.1;python_version>="3.10"', 'beautifulsoup4~=4.15.0', 'pyotp~=2.10.0', 'python-xlib==0.33;platform_system=="Linux"', From 2897f0a992e0cffa6d0af45ac8d2b85416cac26b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 21 Jul 2026 22:11:47 -0400 Subject: [PATCH 4/4] Version 4.51.5 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 3f2547aee94..522bac39614 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.51.4" +__version__ = "4.51.5"