diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 1c70e039ae6..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) @@ -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/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/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" diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index b3d7f189309..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 @@ -679,6 +680,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 +2147,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 +4006,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..ce028bbb071 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,10 +1330,16 @@ 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): + """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""" diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 9e209cb6522..7f9e159e650 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): @@ -4883,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 @@ -4898,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(): diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 0e54039db97..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, Set, Tuple, Union +from typing import List, Optional, 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: 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): diff --git a/setup.py b/setup.py index 427726d072e..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", @@ -233,9 +234,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