From fbf214343eab2ecf3640bf7de116b51ef6a1d555 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 12:55:33 -0400 Subject: [PATCH 1/9] Update CDP Mode --- examples/cdp_mode/ReadMe.md | 2 + seleniumbase/core/browser_launcher.py | 4 ++ seleniumbase/core/sb_cdp.py | 38 ++++++++++----- seleniumbase/fixtures/base_case.py | 5 +- seleniumbase/undetected/cdp_driver/browser.py | 46 ++++++++++++++++--- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 1c70e039ae6..d0f2593fbb4 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -384,6 +384,8 @@ sb.cdp.go_back() sb.cdp.go_forward() sb.cdp.get_navigation_history() sb.cdp.tile_windows(windows=None, max_columns=0) +sb.cdp.grant_permissions(permissions, origin=None) +sb.cdp.grant_all_permissions() sb.cdp.get_all_cookies(*args, **kwargs) sb.cdp.set_all_cookies(*args, **kwargs) sb.cdp.save_cookies(*args, **kwargs) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index b3d7f189309..d80a9e5d704 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -679,6 +679,8 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.go_forward = CDPM.go_forward cdp.get_navigation_history = CDPM.get_navigation_history cdp.tile_windows = CDPM.tile_windows + cdp.grant_permissions = CDPM.grant_permissions + cdp.grant_all_permissions = CDPM.grant_all_permissions cdp.get_all_cookies = CDPM.get_all_cookies cdp.set_all_cookies = CDPM.set_all_cookies cdp.save_cookies = CDPM.save_cookies @@ -2144,6 +2146,7 @@ def _set_chrome_options( prefs["download.prompt_for_download"] = False prefs["download_bubble.partial_view_enabled"] = False prefs["credentials_enable_service"] = False + prefs["autofill.credit_card_enabled"] = False prefs["local_discovery.notifications_enabled"] = False prefs["safebrowsing.enabled"] = False # Prevent PW "data breach" pop-ups prefs["safebrowsing.disable_download_protection"] = True @@ -4002,6 +4005,7 @@ def get_local_driver( "download.directory_upgrade": True, "download.prompt_for_download": False, "credentials_enable_service": False, + "autofill.credit_card_enabled": False, "local_discovery.notifications_enabled": False, "safebrowsing.disable_download_protection": True, "safebrowsing.enabled": False, # Prevent PW "data breach" pop-ups diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index e8578ead566..ed4a58dd9fb 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -645,6 +645,23 @@ def tile_windows(self, windows=None, max_columns=0): driver.tile_windows(windows, max_columns) ) + def grant_permissions(self, permissions, origin=None): + """Grant specific permissions to the current window. + Applies to all origins if no origin is specified.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.grant_permissions(permissions, origin) + ) + + def grant_all_permissions(self): + """Grant all permissions to the current window for all origins.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete(driver.grant_all_permissions()) + def get_all_cookies(self, *args, **kwargs): driver = self.driver if hasattr(driver, "cdp_base"): @@ -681,9 +698,7 @@ def clear_cookies(self): driver = self.driver if hasattr(driver, "cdp_base"): driver = driver.cdp_base - return self.loop.run_until_complete( - driver.cookies.clear() - ) + return self.loop.run_until_complete(driver.cookies.clear()) def sleep(self, seconds): time.sleep(seconds) @@ -702,9 +717,7 @@ def get_active_element_css(self): js_code = active_css_js.get_active_element_css js_code = js_code.replace("return getBestSelector", "getBestSelector") - return self.loop.run_until_complete( - self.page.evaluate(js_code) - ) + return self.loop.run_until_complete(self.page.evaluate(js_code)) def click(self, selector, timeout=None): if not timeout: @@ -978,17 +991,13 @@ def evaluate(self, expression): "\n".join(exp_list[0:-1]) + "\n" + exp_list[-1].strip()[len("return "):] ).strip() - return self.loop.run_until_complete( - self.page.evaluate(expression) - ) + return self.loop.run_until_complete(self.page.evaluate(expression)) def js_dumps(self, obj_name): """Similar to evaluate(), but for dictionary results.""" if obj_name.startswith("return "): obj_name = obj_name[len("return "):] - return self.loop.run_until_complete( - self.page.js_dumps(obj_name) - ) + return self.loop.run_until_complete(self.page.js_dumps(obj_name)) def maximize(self): if self.get_window()[1].window_state.value == "maximized": @@ -1309,6 +1318,8 @@ def get_element_attributes(self, selector): ) def get_element_attribute(self, selector, attribute): + """Find an element and return the value of an attribute. + Raises an exception if there's no such element or attribute.""" attributes = self.get_element_attributes(selector) with suppress(Exception): return attributes[attribute] @@ -1319,6 +1330,9 @@ def get_element_attribute(self, selector, attribute): return value def get_attribute(self, selector, attribute): + """Find an element and return the value of an attribute. + If the element doesn't exist: Raises an exception. + If the attribute doesn't exist: Returns None.""" return self.find_element(selector).get_attribute(attribute) def get_element_html(self, selector): diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 9e209cb6522..e3e99f87ef0 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1910,7 +1910,10 @@ def get_attribute( timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) if self.__is_cdp_swap_needed(): - return self.cdp.get_element_attribute(selector, attribute) + if hard_fail: + return self.cdp.get_element_attribute(selector, attribute) + else: + return self.cdp.get_attribute(selector, attribute) self.wait_for_ready_state_complete() time.sleep(0.01) if self.__is_shadow_selector(selector): diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 0e54039db97..73c9143026a 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -16,7 +16,7 @@ import warnings from collections import defaultdict from seleniumbase import config as sb_config -from typing import List, Set, Tuple, Union +from typing import List, Optional, Required, Set, Tuple, Union import mycdp as cdp from . import cdp_util as util from . import tab @@ -504,10 +504,22 @@ async def start(self=None) -> Browser: # self.connection.handlers[cdp.inspector.Detached] = [self.stop] # return self + async def grant_permissions( + self, + permissions: Required[List[str] | str], + origin: Optional[str] = None, + ): + """Grant specific permissions to the current window. + Applies to all origins if no origin is specified.""" + if isinstance(permissions, str): + permissions = [permissions] + await self.connection.send( + cdp.browser.grant_permissions(permissions, origin) + ) + async def grant_all_permissions(self): """ Grant permissions for: - accessibilityEvents audioCapture backgroundSync backgroundFetch @@ -524,19 +536,39 @@ async def grant_all_permissions(self): notifications paymentHandler periodicBackgroundSync - protectedMediaIdentifier sensors storageAccess topLevelStorageAccess videoCapture - videoCapturePanTiltZoom wakeLockScreen wakeLockSystem windowManagement """ - permissions = list(cdp.browser.PermissionType) - permissions.remove(cdp.browser.PermissionType.FLASH) - permissions.remove(cdp.browser.PermissionType.CAPTURED_SURFACE_CONTROL) + permissions = [ + "audioCapture", + "backgroundSync", + "backgroundFetch", + "clipboardReadWrite", + "clipboardSanitizedWrite", + "displayCapture", + "durableStorage", + "geolocation", + "idleDetection", + "localFonts", + "midi", + "midiSysex", + "nfc", + "notifications", + "paymentHandler", + "periodicBackgroundSync", + "sensors", + "storageAccess", + "topLevelStorageAccess", + "videoCapture", + "wakeLockScreen", + "wakeLockSystem", + "windowManagement", + ] await self.connection.send(cdp.browser.grant_permissions(permissions)) async def tile_windows(self, windows=None, max_columns: int = 0): From c795a850036cfcfd346ffcf989997212ed13455b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 12:56:29 -0400 Subject: [PATCH 2/9] Refresh optional Python dependencies --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index c621f0736e3..23d80e7e755 100755 --- a/requirements.txt +++ b/requirements.txt @@ -77,9 +77,9 @@ rich>=14.0.0,<15 # ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.) coverage>=7.6.1;python_version<"3.9" -coverage>=7.8.2;python_version>="3.9" +coverage>=7.9.0;python_version>="3.9" pytest-cov>=5.0.0;python_version<"3.9" -pytest-cov>=6.1.1;python_version>="3.9" +pytest-cov>=6.2.1;python_version>="3.9" flake8==5.0.4;python_version<"3.9" flake8==7.2.0;python_version>="3.9" mccabe==0.7.0 diff --git a/setup.py b/setup.py index 427726d072e..cb244290073 100755 --- a/setup.py +++ b/setup.py @@ -233,9 +233,9 @@ # Usage: coverage run -m pytest; coverage html; coverage report "coverage": [ 'coverage>=7.6.1;python_version<"3.9"', - 'coverage>=7.8.2;python_version>="3.9"', + 'coverage>=7.9.0;python_version>="3.9"', 'pytest-cov>=5.0.0;python_version<"3.9"', - 'pytest-cov>=6.1.1;python_version>="3.9"', + 'pytest-cov>=6.2.1;python_version>="3.9"', ], # pip install -e .[flake8] # Usage: flake8 From ad8754ad1c63762c3d1f54e9de0173dc50f6fa67 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 12:57:24 -0400 Subject: [PATCH 3/9] Version 4.39.4 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 9316bd83daa..68bbaad9ea7 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.39.3" +__version__ = "4.39.4" From 07360c4c8930b3cbd357c4ed1b642502f7aabbc2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 13:55:11 -0400 Subject: [PATCH 4/9] Version 4.39.4b --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 68bbaad9ea7..d59e4ca585f 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.39.4" +__version__ = "4.39.4b" From 2b9519b950916f28ba7622162a4026b43f49210d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 14:05:07 -0400 Subject: [PATCH 5/9] Update typing for backwards compatibility --- seleniumbase/undetected/cdp_driver/browser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 73c9143026a..56c941f2909 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -16,7 +16,7 @@ import warnings from collections import defaultdict from seleniumbase import config as sb_config -from typing import List, Optional, Required, Set, Tuple, Union +from typing import List, Optional, Set, Tuple, Union import mycdp as cdp from . import cdp_util as util from . import tab @@ -506,7 +506,7 @@ async def start(self=None) -> Browser: async def grant_permissions( self, - permissions: Required[List[str] | str], + permissions: List[str] | str, origin: Optional[str] = None, ): """Grant specific permissions to the current window. From e77ce7635aa5e99c790572ce239c806844fe54ee Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 14:06:18 -0400 Subject: [PATCH 6/9] Update get_element_html(selector) --- seleniumbase/core/sb_cdp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index ed4a58dd9fb..ce028bbb071 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -1336,7 +1336,10 @@ def get_attribute(self, selector, attribute): return self.find_element(selector).get_attribute(attribute) def get_element_html(self, selector): + """Find an element and return the outerHTML.""" selector = self.__convert_to_css_if_xpath(selector) + self.find_element(selector) + self.__add_light_pause() return self.loop.run_until_complete( self.page.evaluate( """document.querySelector('%s').outerHTML""" From 7d9809084878cd9fa6530717fa08309ffed3d9ca Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 14:07:50 -0400 Subject: [PATCH 7/9] Update docs / comments --- examples/cdp_mode/ReadMe.md | 2 +- seleniumbase/core/browser_launcher.py | 1 + seleniumbase/fixtures/base_case.py | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index d0f2593fbb4..4af84e09a6d 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -364,7 +364,7 @@ with SB(uc=True, test=True, locale="en", pls="none") as sb: sb.cdp.get(url, **kwargs) sb.cdp.open(url, **kwargs) sb.cdp.reload(ignore_cache=True, script_to_evaluate_on_load=None) -sb.cdp.refresh() +sb.cdp.refresh(*args, **kwargs) sb.cdp.get_event_loop() sb.cdp.add_handler(event, handler) sb.cdp.find_element(selector, best_match=False, timeout=None) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index d80a9e5d704..7944341963a 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -532,6 +532,7 @@ def uc_open_with_reconnect(driver, url, reconnect_time=None): def uc_open_with_cdp_mode(driver, url=None, **kwargs): + """Activate CDP Mode with the URL and kwargs.""" import asyncio from seleniumbase.undetected.cdp_driver import cdp_util diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index e3e99f87ef0..7f9e159e650 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -4886,6 +4886,7 @@ def deactivate_design_mode(self, url=None): self.execute_script(script) def activate_cdp_mode(self, url=None, **kwargs): + """Activate CDP Mode with the URL and kwargs.""" if hasattr(self.driver, "_is_using_uc") and self.driver._is_using_uc: if self.__is_cdp_swap_needed(): return # CDP Mode is already active @@ -4901,6 +4902,8 @@ def activate_cdp_mode(self, url=None, **kwargs): self.cdp = self.driver.cdp def activate_recorder(self): + """Activate Recorder Mode on the current tab/window. + For persistent Recorder Mode, use the extension instead.""" from seleniumbase.js_code.recorder_js import recorder_js if not self.is_chromium(): From 248b0ec11771b19943c06f35d0b2f027211b0365 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 14:08:09 -0400 Subject: [PATCH 8/9] Update classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index cb244290073..3e2c14187a4 100755 --- a/setup.py +++ b/setup.py @@ -126,6 +126,7 @@ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP :: Browsers", "Topic :: Scientific/Engineering", From 01ac27d4a146adc7d8dbf7b2540427d9b2898b91 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 12 Jun 2025 14:08:50 -0400 Subject: [PATCH 9/9] Version 4.39.4 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index d59e4ca585f..68bbaad9ea7 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.39.4b" +__version__ = "4.39.4"