diff --git a/requirements.txt b/requirements.txt index 27af72fe3f5..2b656458a02 100755 --- a/requirements.txt +++ b/requirements.txt @@ -65,7 +65,8 @@ iniconfig==2.3.0;python_version>="3.10" pluggy==1.5.0;python_version<"3.9" pluggy==1.6.0;python_version>="3.9" pytest==8.3.5;python_version<"3.9" -pytest==8.4.2;python_version>="3.9" +pytest==8.4.2;python_version>="3.9" and python_version<"3.11" +pytest==9.0.1;python_version>="3.11" pytest-html==4.0.2 pytest-metadata==3.1.1 pytest-ordering==0.6 diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index f3297a6b988..dde5e0c262d 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.44.11" +__version__ = "4.44.12" diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index db7da8c51af..b0445a664c4 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -159,9 +159,21 @@ def extend_driver( # Extend the driver with new methods driver.default_find_element = driver.find_element driver.default_find_elements = driver.find_elements + driver.default_add_cookie = driver.add_cookie + driver.default_get_cookie = driver.get_cookie + driver.default_delete_cookie = driver.delete_cookie + driver.default_back = driver.back + driver.default_forward = driver.forward + driver.default_refresh = driver.refresh DM = sb_driver.DriverMethods(driver) driver.find_element = DM.find_element driver.find_elements = DM.find_elements + driver.add_cookie = DM.add_cookie + driver.get_cookie = DM.get_cookie + driver.delete_cookie = DM.delete_cookie + driver.back = DM.back + driver.forward = DM.forward + driver.refresh = DM.refresh driver.locator = DM.locator page = types.SimpleNamespace() page.open = DM.open_url diff --git a/seleniumbase/core/log_helper.py b/seleniumbase/core/log_helper.py index 17d9f851f78..ed380683c4c 100644 --- a/seleniumbase/core/log_helper.py +++ b/seleniumbase/core/log_helper.py @@ -192,8 +192,8 @@ def log_test_failure_data(test, test_logpath, driver, browser, url=None): and test._outcome.errors ): try: - exc_message = test._outcome.errors[0][1][1] - traceback_address = test._outcome.errors[0][1][2] + exc_message = test._outcome.errors[-1][1][1] + traceback_address = test._outcome.errors[-1][1][2] traceback_list = traceback.format_list( traceback.extract_tb(traceback_address)[1:] ) diff --git a/seleniumbase/core/report_helper.py b/seleniumbase/core/report_helper.py index 2712c8397e9..51ddbf054b8 100644 --- a/seleniumbase/core/report_helper.py +++ b/seleniumbase/core/report_helper.py @@ -111,7 +111,7 @@ def process_failures(test, test_count, duration): and test._outcome.errors ): try: - exc_message = test._outcome.errors[0][1][1] + exc_message = test._outcome.errors[-1][1][1] except Exception: exc_message = "(Unknown Exception)" else: diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index b68b22d5f02..0d04f488b0a 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -713,7 +713,10 @@ def click(self, selector, timeout=None): tag_name in ["a", "button", "canvas", "div", "input", "li", "span"] and "contains(" not in selector ): - element.mouse_click() # Simulated click (NOT PyAutoGUI) + try: + element.mouse_click() # Simulated click (NOT PyAutoGUI) + except Exception: + element.click() # Standard CDP click else: element.click() # Standard CDP click self.__slow_mode_pause_if_set() diff --git a/seleniumbase/core/sb_driver.py b/seleniumbase/core/sb_driver.py index 50a07a46c21..edf34713656 100644 --- a/seleniumbase/core/sb_driver.py +++ b/seleniumbase/core/sb_driver.py @@ -12,6 +12,10 @@ class DriverMethods(WebDriver): def __init__(self, driver): self.driver = driver + if hasattr(driver, "session_id"): + self.session_id = driver.session_id + if hasattr(driver, "command_executor"): + self.command_executor = driver.command_executor def __is_cdp_swap_needed(self): """If the driver is disconnected, use a CDP method when available.""" @@ -37,6 +41,36 @@ def find_elements(self, by=None, value=None): value, by = page_utils.swap_selector_and_by_if_reversed(value, by) return self.driver.default_find_elements(by=by, value=value) + def add_cookie(self, *args, **kwargs): + page_actions._reconnect_if_disconnected(self.driver) + self.driver.default_add_cookie(*args, **kwargs) + + def get_cookie(self, *args, **kwargs): + page_actions._reconnect_if_disconnected(self.driver) + self.driver.default_get_cookie(*args, **kwargs) + + def delete_cookie(self, *args, **kwargs): + page_actions._reconnect_if_disconnected(self.driver) + self.driver.default_delete_cookie(*args, **kwargs) + + def back(self): + if self.__is_cdp_swap_needed(): + self.driver.cdp.go_back() + return + self.driver.default_back() + + def forward(self): + if self.__is_cdp_swap_needed(): + self.driver.cdp.go_forward() + return + self.driver.default_forward() + + def refresh(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + self.driver.cdp.refresh(*args, **kwargs) + return + self.driver.default_refresh() + def locator(self, selector, by=None): if not by: by = "css selector" diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 8645166c463..dd5f33b914b 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -3969,8 +3969,23 @@ def set_content_to_parent_frame(self): def open_new_window(self, switch_to=True): """Opens a new browser tab/window and switches to it by default.""" + url = None + if self.__looks_like_a_page_url(str(switch_to)): + # Different API for CDP Mode: First arg is a `url`. + # (Also, don't break backwards compat for reg mode) + url = switch_to + switch_to = True if self.__is_cdp_swap_needed(): - self.cdp.open_new_tab(switch_to=switch_to) + self.cdp.open_new_tab(url=url, switch_to=switch_to) + return + elif ( + hasattr(self.driver, "_is_using_uc") + and self.driver._is_using_uc + and hasattr(self.driver, "_is_using_cdp") + and self.driver._is_using_cdp + ): + self.disconnect() + self.cdp.open_new_tab(url=url, switch_to=switch_to) return self.wait_for_ready_state_complete() if switch_to: @@ -5040,9 +5055,8 @@ def activate_cdp_mode(self, url=None, **kwargs): if hasattr(self.cdp, "find_element_by_text"): self.find_element_by_text = self.cdp.find_element_by_text if ( - hasattr(sb_config, "_cdp_proxy") - and sb_config._cdp_proxy - and "@" in sb_config._cdp_proxy + hasattr(self.driver, "_is_using_auth") + and self.driver._is_using_auth ): with suppress(Exception): self.cdp.loop.run_until_complete(self.cdp.page.wait(0.25)) @@ -6364,6 +6378,7 @@ def press_up_arrow(self, selector="body", times=1, by="css selector"): By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" self.__check_scope() + self._check_browser() if times < 1: return element = self.wait_for_element_present(selector) @@ -6386,6 +6401,7 @@ def press_down_arrow(self, selector="body", times=1, by="css selector"): By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" self.__check_scope() + self._check_browser() if times < 1: return element = self.wait_for_element_present(selector) @@ -6408,6 +6424,7 @@ def press_left_arrow(self, selector="body", times=1, by="css selector"): By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" self.__check_scope() + self._check_browser() if times < 1: return element = self.wait_for_element_present(selector) @@ -6430,6 +6447,7 @@ def press_right_arrow(self, selector="body", times=1, by="css selector"): By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" self.__check_scope() + self._check_browser() if times < 1: return element = self.wait_for_element_present(selector) @@ -8781,6 +8799,9 @@ def set_text(self, selector, text, by="css selector", timeout=None): if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.set_value(selector, text) + return self.wait_for_ready_state_complete() element = page_actions.wait_for_element_present( self.driver, selector, by, timeout @@ -8801,10 +8822,14 @@ def set_text_content( if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) - self.wait_for_ready_state_complete() - element = page_actions.wait_for_element_present( - self.driver, selector, by, timeout - ) + element = None + if self.__is_cdp_swap_needed(): + element = self.cdp.select(selector, timeout=timeout) + else: + self.wait_for_ready_state_complete() + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout + ) if element.tag_name.lower() in ["input", "textarea"]: self.js_update_text(selector, text, by=by, timeout=timeout) return @@ -15945,7 +15970,7 @@ def __get_exception_info(self): and self._outcome.errors ): try: - exc_message = self._outcome.errors[0][1][1] + exc_message = self._outcome.errors[-1][1][1] except Exception: exc_message = "(Unknown Exception)" else: @@ -16093,8 +16118,16 @@ def __has_exception(self): else: return False elif hasattr(self, "_outcome") and hasattr(self._outcome, "errors"): - if self._outcome.errors: - has_exception = True + if python3_11_or_newer: + if ( + self._outcome.errors + and self._outcome.errors[-1] + and self._outcome.errors[-1][1] + ): + has_exception = True + else: + if self._outcome.errors: + has_exception = True else: has_exception = sys.exc_info()[1] is not None if self.__will_be_skipped and hasattr(self, "_using_sb_fixture"): diff --git a/seleniumbase/masterqa/master_qa.py b/seleniumbase/masterqa/master_qa.py index 8b41cf31916..5240d3a28d9 100644 --- a/seleniumbase/masterqa/master_qa.py +++ b/seleniumbase/masterqa/master_qa.py @@ -11,6 +11,11 @@ from seleniumbase.fixtures import js_utils +python3_11_or_newer = False +if sys.version_info >= (3, 11): + python3_11_or_newer = True + + class MasterQA(BaseCase): def setUp(self): self.check_count = 0 @@ -309,8 +314,17 @@ def __has_exception(self): if hasattr(sys, "last_traceback") and sys.last_traceback is not None: has_exception = True elif hasattr(self, "_outcome"): - if hasattr(self._outcome, "errors") and self._outcome.errors: - has_exception = True + if hasattr(self._outcome, "errors"): + if python3_11_or_newer: + if ( + self._outcome.errors + and self._outcome.errors[-1] + and self._outcome.errors[-1][1] + ): + has_exception = True + else: + if self._outcome.errors: + has_exception = True else: has_exception = sys.exc_info()[1] is not None return has_exception diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index ad613edab0f..4ee2c26e35c 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -430,14 +430,28 @@ async def get( username_and_password = sb_config._cdp_proxy.split("@")[0] proxy_user = username_and_password.split(":")[0] proxy_pass = username_and_password.split(":")[1] - await self.set_auth(proxy_user, proxy_pass, self.tabs[0]) - time.sleep(0.25) + if ( + hasattr(self.main_tab, "_last_auth") + and self.main_tab._last_auth == username_and_password + ): + pass # Auth was already set + else: + self.main_tab._last_auth = username_and_password + await self.set_auth(proxy_user, proxy_pass, self.tabs[0]) + time.sleep(0.25) if "auth" in kwargs and kwargs["auth"] and ":" in kwargs["auth"]: username_and_password = kwargs["auth"] proxy_user = username_and_password.split(":")[0] proxy_pass = username_and_password.split(":")[1] - await self.set_auth(proxy_user, proxy_pass, self.tabs[0]) - time.sleep(0.25) + if ( + hasattr(self.main_tab, "_last_auth") + and self.main_tab._last_auth == username_and_password + ): + pass # Auth was already set + else: + self.main_tab._last_auth = username_and_password + await self.set_auth(proxy_user, proxy_pass, self.tabs[0]) + time.sleep(0.25) await connection.sleep(0.15) frame_id, loader_id, *_ = await connection.send( cdp.page.navigate(url) diff --git a/setup.py b/setup.py index 994e8ee1531..84b163081b5 100755 --- a/setup.py +++ b/setup.py @@ -214,7 +214,8 @@ 'pluggy==1.5.0;python_version<"3.9"', 'pluggy==1.6.0;python_version>="3.9"', 'pytest==8.3.5;python_version<"3.9"', - 'pytest==8.4.2;python_version>="3.9"', + 'pytest==8.4.2;python_version>="3.9" and python_version<"3.11"', + 'pytest==9.0.1;python_version>="3.11"', "pytest-html==4.0.2", # Newer ones had issues 'pytest-metadata==3.1.1', "pytest-ordering==0.6",