From 6316b0aa95c947fc4f974662212f3cf77d1f829e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2026 14:08:07 -0400 Subject: [PATCH 1/5] Add a CDP-based `click_and_hold()` method --- seleniumbase/core/browser_launcher.py | 1 + seleniumbase/core/sb_cdp.py | 34 +++++++++++++++++-- seleniumbase/fixtures/base_case.py | 2 ++ seleniumbase/undetected/cdp_driver/element.py | 26 +++++++++++--- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 648c1ff8404..869f068eed1 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -806,6 +806,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.click_if_visible = CDPM.click_if_visible cdp.click_visible_elements = CDPM.click_visible_elements cdp.click_with_offset = CDPM.click_with_offset + cdp.click_and_hold = CDPM.click_and_hold cdp.mouse_click = CDPM.mouse_click cdp.get_parent = CDPM.get_parent cdp.remove_element = CDPM.remove_element diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 34ea18c506d..42b65394391 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -87,6 +87,11 @@ def __add_sync_methods(self, element): element, *args, **kwargs ) ) + element.click_and_hold = ( + lambda *args, **kwargs: ( + self.__mouse_click_and_hold(element, *args, **kwargs) + ) + ) element.mouse_drag = ( lambda destination: self.__mouse_drag(element, destination) ) @@ -612,9 +617,20 @@ def __is_in_viewport(self, element): self.loop.run_until_complete(element.is_in_viewport_async()) ) - def __mouse_click(self, element): + def __mouse_click(self, element, timeframe=0): + result = ( + self.loop.run_until_complete( + element.mouse_click_async(timeframe=timeframe) + ) + ) + self.loop.run_until_complete(self.page.wait(0.2)) + return result + + def __mouse_click_and_hold(self, element, timeframe=2): result = ( - self.loop.run_until_complete(element.mouse_click_async()) + self.loop.run_until_complete( + element.mouse_click_async(timeframe=timeframe) + ) ) self.loop.run_until_complete(self.page.wait(0.2)) return result @@ -945,8 +961,20 @@ def click_visible_elements(self, selector, limit=0, scroll=True): except Exception: break + def click_and_hold(self, selector, timeout=None, scroll=True, timeframe=2): + """(Simulate a mouse click & hold)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.find_element(selector, timeout=timeout) + if scroll: + element.scroll_into_view() + element.click_and_hold(timeframe) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + def mouse_click(self, selector, timeout=None, scroll=True): - """(Attempt simulating a mouse click)""" + """(Simulate a mouse click)""" if not timeout: timeout = settings.SMALL_TIMEOUT self.__slow_mode_pause_if_set() diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index b41b94d1319..b9483c39f96 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -5111,6 +5111,8 @@ def activate_cdp_mode(self, url=None, **kwargs): self.add_handler = self.cdp.add_handler if hasattr(self.cdp, "clear_input"): self.clear_input = self.cdp.clear_input + if hasattr(self.cdp, "click_and_hold"): + self.click_and_hold = self.cdp.click_and_hold if hasattr(self.cdp, "close_active_tab"): self.close_active_tab = self.cdp.close_active_tab if hasattr(self.cdp, "find_element_by_text"): diff --git a/seleniumbase/undetected/cdp_driver/element.py b/seleniumbase/undetected/cdp_driver/element.py index e549b006890..7d30883e5f8 100644 --- a/seleniumbase/undetected/cdp_driver/element.py +++ b/seleniumbase/undetected/cdp_driver/element.py @@ -494,7 +494,7 @@ async def mouse_click_async( button: str = "left", buttons: typing.Optional[int] = 1, modifiers: typing.Optional[int] = 0, - hold: bool = False, + timeframe: float = 0.0, ): """ Native click (on element). @@ -542,13 +542,31 @@ async def mouse_click_async( ) ), ) - await asyncio.sleep(random.uniform(0.011, 0.015)) + if not timeframe or timeframe <= 0: + # If 0 (or less), hold for a small amount of time + await asyncio.sleep(random.uniform(0.0126, 0.0152)) + x += random.uniform(-0.12, 0.12) + y += random.uniform(-0.12, 0.12) + else: + end_time = asyncio.get_running_loop().time() + timeframe + while asyncio.get_running_loop().time() < end_time: + await self._tab.send( + cdp.input_.dispatch_mouse_event( + type_="mouseMoved", + x=x + random.uniform(-0.12, 0.12), + y=y + random.uniform(-0.12, 0.12), + button=cdp.input_.MouseButton(button), + buttons=buttons, + force=0.5, + ) + ) + await asyncio.sleep(random.uniform(0.120, 0.280)) asyncio.create_task( self._tab.send( cdp.input_.dispatch_mouse_event( "mouseReleased", - x=x + random.uniform(-0.12, 0.12), - y=y + random.uniform(-0.12, 0.12), + x=x, + y=y, modifiers=modifiers, button=cdp.input_.MouseButton(button), buttons=buttons, From e667ad0ff5e19c8f85f7b7f42ba9f672a63639e3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2026 14:09:54 -0400 Subject: [PATCH 2/5] Adjust timings and refactor --- seleniumbase/core/sb_cdp.py | 15 ++++++++--- seleniumbase/plugins/sb_manager.py | 4 ++- seleniumbase/undetected/cdp_driver/element.py | 26 +++++++++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 42b65394391..b4eea6066e1 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -662,7 +662,7 @@ def __press_keys(self, element, text): text = text[:-1] for key in text: element.send_keys(key) - time.sleep(float(0.042 + (random.random() / 110.0))) + time.sleep(float(0.0422 + (random.random() / 112.0))) if submit: element.send_keys("\r\n") time.sleep(0.044) @@ -1160,7 +1160,7 @@ def press_keys(self, selector, text, timeout=None): except AttributeError: element = self.select(selector, timeout=0.1) element.send_keys(key) - time.sleep(float(0.042 + (random.random() / 110.0))) + time.sleep(float(0.0422 + (random.random() / 112.0))) if submit: element.send_keys("\r\n") time.sleep(0.044) @@ -2088,7 +2088,7 @@ def gui_press_keys(self, keys): self.__make_sure_pyautogui_lock_is_writable() for key in keys: pyautogui.press(key) - time.sleep(float(0.042 + (random.random() / 110.0))) + time.sleep(float(0.0422 + (random.random() / 112.0))) self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.sleep(0.025)) @@ -2216,6 +2216,15 @@ def quit(self): driver = self.driver if hasattr(driver, "cdp_base"): driver = driver.cdp_base + if ( + self.driver.config.uses_custom_data_dir + and shared_utils.is_windows() + ): + # Let Windows save the session to a custom use_data_dir + self.loop.run_until_complete( + driver.connection.send(mycdp.browser.close()) + ) + time.sleep(0.3) driver.quit() def _on_a_cf_turnstile_page(self, source=None): diff --git a/seleniumbase/plugins/sb_manager.py b/seleniumbase/plugins/sb_manager.py index 15462c00c7c..5efe51d8e93 100644 --- a/seleniumbase/plugins/sb_manager.py +++ b/seleniumbase/plugins/sb_manager.py @@ -1489,7 +1489,9 @@ def SB( and hasattr(driver.cdp, "loop") ): asyncio.set_event_loop(driver.cdp.loop) - tasks = [tab.aclose() for tab in driver.cdp.get_tabs()] + tasks = [] + for tab in driver.cdp.get_tabs(): + tasks.append(tab.aclose()) tasks.append(driver.cdp.driver.connection.aclose()) driver.cdp.loop.run_until_complete(asyncio.gather(*tasks)) driver.cdp.loop.close() diff --git a/seleniumbase/undetected/cdp_driver/element.py b/seleniumbase/undetected/cdp_driver/element.py index 7d30883e5f8..1cf41868976 100644 --- a/seleniumbase/undetected/cdp_driver/element.py +++ b/seleniumbase/undetected/cdp_driver/element.py @@ -15,8 +15,10 @@ import mycdp.overlay import mycdp.page import mycdp.runtime +from seleniumbase.fixtures import shared_utils logger = logging.getLogger(__name__) +IS_LINUX = shared_utils.is_linux() if typing.TYPE_CHECKING: from .tab import Tab @@ -525,9 +527,9 @@ async def mouse_click_async( asyncio.create_task(self.flash_async(0.25))''' with suppress(Exception): await self.mouse_move_async() - await asyncio.sleep(random.uniform(0.003, 0.005)) - x = center[0] + random.uniform(-0.8, 0.8) - y = center[1] + random.uniform(-0.8, 0.8) + await asyncio.sleep(random.uniform(0.0035, 0.0048)) + x = center[0] + random.uniform(-0.85, 0.85) + y = center[1] + random.uniform(-0.85, 0.85) asyncio.create_task( self._tab.send( cdp.input_.dispatch_mouse_event( @@ -609,7 +611,7 @@ async def mouse_click_with_offset_async( logger.debug("Clicking on location: %.2f, %.2f" % (x_pos, y_pos)) with suppress(Exception): await self.mouse_move_async() - await asyncio.sleep(random.uniform(0.003, 0.005)) + await asyncio.sleep(random.uniform(0.0035, 0.0048)) script1 = 'sessionStorage.getItem("pxsid") !== null;' script2 = 'sessionStorage.getItem("PIM-SESSION-ID") !== null;' using_px = True @@ -827,6 +829,20 @@ async def send_keys_async(self, text: str): if self.tag_name.lower() == "textarea" and text.endswith("\r\n"): text = text[0:-1] await self.apply("(elem) => elem.focus()") + if ( + IS_LINUX + and not ( + self.tag_name.lower() == "textarea" + and text.endswith("\r") or text.endswith("\n") + ) + ): + [ + await self._tab.send( + cdp.input_.dispatch_key_event(type_="char", text=char) + ) + for char in list(text) + ] + return # Map non-alphanumeric symbols to the correct CDP virtual key code. # Prevents collisions with control keys. Eg. ord('.') = 46 = VK_DELETE. SYMBOL_MAP = { @@ -916,7 +932,7 @@ async def send_keys_async(self, text: str): ) ) # 4. Trigger keyup DOM event - await asyncio.sleep(random.uniform(0.011, 0.015)) + await asyncio.sleep(random.uniform(0.0126, 0.0152)) await self._tab.send( cdp.input_.dispatch_key_event( type_="keyUp", From f932c2d874c000d0149f2754a32bc955cfd71aa7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2026 14:10:22 -0400 Subject: [PATCH 3/5] Update examples --- examples/cdp_mode/raw_cdp_clearcote.py | 1 + examples/raw_google.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/cdp_mode/raw_cdp_clearcote.py b/examples/cdp_mode/raw_cdp_clearcote.py index e12d1bced31..3324efb0f3a 100644 --- a/examples/cdp_mode/raw_cdp_clearcote.py +++ b/examples/cdp_mode/raw_cdp_clearcote.py @@ -7,6 +7,7 @@ sb.click('button[aria-label="Marker 3"]') sb.press_keys("input", "audit") sb.set_value('input[type="range"]', "100") +sb.click_and_hold("button[data-interaction-hold]") sb.select_option_by_text("select", "Mouse") sb.click('button:contains("Run the audit")') sb.sleep(6) diff --git a/examples/raw_google.py b/examples/raw_google.py index 69c4d0f5db9..869ac0070b3 100644 --- a/examples/raw_google.py +++ b/examples/raw_google.py @@ -4,7 +4,8 @@ sb.activate_cdp_mode() sb.goto("https://google.com/ncr") sb.click_if_visible('button:contains("Accept all")') - sb.press_keys('[name="q"]', "SeleniumBase GitHub page\n") + sb.press_keys('[name="q"]', "SeleniumBase GitHub page") + sb.click('[value="Google Search"]') sb.sleep(4) # The "AI Overview" sometimes loads print(sb.get_page_title()) sb.save_as_pdf_to_logs() From 7775f1db77594bd1642714de013d999e49c36e5c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2026 14:10:34 -0400 Subject: [PATCH 4/5] Update the docs --- examples/cdp_mode/ReadMe.md | 2 ++ help_docs/cdp_mode_methods.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 25bc0bc8780..0b4f62540f0 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -391,6 +391,7 @@ sb.click_visible_elements(selector, limit=0, scroll=True) sb.click_nth_element(selector, number, scroll=True) sb.click_nth_visible_element(selector, number, scroll=True) sb.click_with_offset(selector, x, y, center=False, scroll=True) +sb.click_and_hold(selector, timeout=None, scroll=True, timeframe=2) sb.click_link(link_text) sb.go_back() sb.go_forward() @@ -735,6 +736,7 @@ After finding an element in CDP Mode, you can access `WebElement` methods: element.clear_input() element.click() element.click_with_offset(x, y, center=False) +element.click_and_hold(timeframe=2) element.flash(duration=0.5, color="EE4488") element.focus() element.gui_click(timeframe=0.27) diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index bb1f6be7e5f..7156091b031 100644 --- a/help_docs/cdp_mode_methods.md +++ b/help_docs/cdp_mode_methods.md @@ -37,6 +37,7 @@ sb.click_visible_elements(selector, limit=0, scroll=True) sb.click_nth_element(selector, number, scroll=True) sb.click_nth_visible_element(selector, number, scroll=True) sb.click_with_offset(selector, x, y, center=False, scroll=True) +sb.click_and_hold(selector, timeout=None, scroll=True, timeframe=2) sb.click_link(link_text) sb.go_back() sb.go_forward() @@ -381,6 +382,7 @@ After finding an element in CDP Mode, you can access `WebElement` methods: element.clear_input() element.click() element.click_with_offset(x, y, center=False) +element.click_and_hold(timeframe=2) element.flash(duration=0.5, color="EE4488") element.focus() element.gui_click(timeframe=0.27) From dc07cb87d21ca6991e1e156f6b60db2beb661039 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2026 14:10:50 -0400 Subject: [PATCH 5/5] Version 4.51.7 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index a3a1f6b520a..cb615cae19d 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.51.6" +__version__ = "4.51.7"