From a1a3b659da940dd68c761a09c79741ff5c7c857a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 10 Dec 2024 01:18:45 -0500 Subject: [PATCH 1/5] Improve CDP Mode --- seleniumbase/core/browser_launcher.py | 42 ++++---- seleniumbase/core/sb_cdp.py | 96 ++++++++++++++----- seleniumbase/fixtures/base_case.py | 64 ++++++------- seleniumbase/fixtures/js_utils.py | 2 + seleniumbase/undetected/cdp_driver/element.py | 6 +- 5 files changed, 138 insertions(+), 72 deletions(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 6133be101c5..b9ea7ed3e06 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -1413,7 +1413,7 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): ctype = "cf_t" else: return - if not driver.is_connected(): + if not driver.is_connected() and not __is_cdp_swap_needed(driver): driver.connect() time.sleep(2) install_pyautogui_if_missing(driver) @@ -1425,7 +1425,10 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): ) with gui_lock: # Prevent issues with multiple processes needs_switch = False - is_in_frame = js_utils.is_in_frame(driver) + if not __is_cdp_swap_needed(driver): + is_in_frame = js_utils.is_in_frame(driver) + else: + is_in_frame = False selector = "#challenge-stage" if ctype == "g_rc": selector = "#recaptcha-token" @@ -1433,7 +1436,7 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): driver.switch_to.parent_frame() needs_switch = True is_in_frame = js_utils.is_in_frame(driver) - if not is_in_frame: + if not is_in_frame and not __is_cdp_swap_needed(driver): # Make sure the window is on top page_actions.switch_to_window( driver, driver.current_window_handle, 2, uc_lock=False @@ -1500,17 +1503,18 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): and frame == "iframe" ): frame = 'iframe[title="reCAPTCHA"]' - if not is_in_frame or needs_switch: - # Currently not in frame (or nested frame outside CF one) - try: - if visible_iframe or ctype == "g_rc": - driver.switch_to_frame(frame) - except Exception: - if visible_iframe or ctype == "g_rc": - if driver.is_element_present("iframe"): - driver.switch_to_frame("iframe") - else: - return + if not __is_cdp_swap_needed(driver): + if not is_in_frame or needs_switch: + # Currently not in frame (or nested frame outside CF one) + try: + if visible_iframe or ctype == "g_rc": + driver.switch_to_frame(frame) + except Exception: + if visible_iframe or ctype == "g_rc": + if driver.is_element_present("iframe"): + driver.switch_to_frame("iframe") + else: + return try: selector = "div.cf-turnstile" if ctype == "g_rc": @@ -1526,11 +1530,11 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): tab_count += 1 time.sleep(0.027) active_element_css = js_utils.get_active_element_css(driver) - print(active_element_css) if ( active_element_css.startswith(selector) or active_element_css.endswith(" > div" * 2) or (special_form and active_element_css.endswith(" div")) + or (ctype == "g_rc" and "frame[name" in active_element_css) ): found_checkbox = True sb_config._saved_cf_tab_count = tab_count @@ -1550,6 +1554,7 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): ) and hasattr(sb_config, "_saved_cf_tab_count") and sb_config._saved_cf_tab_count + and not __is_cdp_swap_needed(driver) ): driver.uc_open_with_disconnect(driver.current_url, 3.8) with suppress(Exception): @@ -4845,7 +4850,12 @@ def get_local_driver( ) uc_activated = True except URLError as e: - if cert in e.args[0] and IS_MAC: + if ( + IS_MAC + and hasattr(e, "args") + and isinstance(e.args, (list, tuple)) + and cert in e.args[0] + ): mac_certificate_error = True else: raise diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 18edd34b03d..893d3083632 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -268,6 +268,7 @@ def click_nth_element(self, selector, number): if number < 0: number = 0 element = elements[number] + element.scroll_into_view() element.click() def click_nth_visible_element(self, selector, number): @@ -284,6 +285,7 @@ def click_nth_visible_element(self, selector, number): if number < 0: number = 0 element = elements[number] + element.scroll_into_view() element.click() def click_link(self, link_text): @@ -311,6 +313,13 @@ def __click(self, element): return result def __flash(self, element, *args, **kwargs): + element.scroll_into_view() + if len(args) < 3 and "x_offset" not in kwargs: + x_offset = self.__get_x_scroll_offset() + kwargs["x_offset"] = x_offset + if len(args) < 3 and "y_offset" not in kwargs: + y_offset = self.__get_y_scroll_offset() + kwargs["y_offset"] = y_offset return ( self.loop.run_until_complete( element.flash_async(*args, **kwargs) @@ -382,9 +391,9 @@ def __save_to_dom(self, element): ) def __scroll_into_view(self, element): - return ( - self.loop.run_until_complete(element.scroll_into_view_async()) - ) + self.loop.run_until_complete(element.scroll_into_view_async()) + self.__add_light_pause() + return None def __select_option(self, element): return ( @@ -431,6 +440,18 @@ def __get_js_attributes(self, element): self.loop.run_until_complete(element.get_js_attributes_async()) ) + def __get_x_scroll_offset(self): + x_scroll_offset = self.loop.run_until_complete( + self.page.evaluate("window.pageXOffset") + ) + return x_scroll_offset or 0 + + def __get_y_scroll_offset(self): + y_scroll_offset = self.loop.run_until_complete( + self.page.evaluate("window.pageYOffset") + ) + return y_scroll_offset or 0 + def tile_windows(self, windows=None, max_columns=0): """Tile windows and return the grid of tiled windows.""" driver = self.driver @@ -504,7 +525,7 @@ def get_active_element_css(self): def click(self, selector, timeout=settings.SMALL_TIMEOUT): self.__slow_mode_pause_if_set() element = self.find_element(selector, timeout=timeout) - self.__add_light_pause() + element.scroll_into_view() element.click() self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) @@ -518,7 +539,9 @@ def click_active_element(self): def click_if_visible(self, selector): if self.is_element_visible(selector): - self.find_element(selector).click() + element = self.find_element(selector) + element.scroll_into_view() + element.click() self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) @@ -545,9 +568,10 @@ def click_visible_elements(self, selector, limit=0): except Exception: continue if (width != 0 or height != 0): + element.scroll_into_view() element.click() click_count += 1 - time.sleep(0.044) + time.sleep(0.042) self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) except Exception: @@ -557,7 +581,7 @@ def mouse_click(self, selector, timeout=settings.SMALL_TIMEOUT): """(Attempt simulating a mouse click)""" self.__slow_mode_pause_if_set() element = self.find_element(selector, timeout=timeout) - self.__add_light_pause() + element.scroll_into_view() element.mouse_click() self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) @@ -579,6 +603,7 @@ def get_nested_element(self, parent_selector, selector): def select_option_by_text(self, dropdown_selector, option): element = self.find_element(dropdown_selector) + element.scroll_into_view() options = element.query_selector_all("option") for found_option in options: if found_option.text.strip() == option.strip(): @@ -599,7 +624,10 @@ def flash( """Paint a quickly-vanishing dot over an element.""" selector = self.__convert_to_css_if_xpath(selector) element = self.find_element(selector) - element.flash(duration=duration, color=color) + element.scroll_into_view() + x_offset = self.__get_x_scroll_offset() + y_offset = self.__get_y_scroll_offset() + element.flash(duration, color, x_offset, y_offset) if pause and isinstance(pause, (int, float)): time.sleep(pause) @@ -607,17 +635,22 @@ def highlight(self, selector): """Highlight an element with multi-colors.""" selector = self.__convert_to_css_if_xpath(selector) element = self.find_element(selector) - element.flash(0.46, "44CC88") + element.scroll_into_view() + x_offset = self.__get_x_scroll_offset() + y_offset = self.__get_y_scroll_offset() + element.flash(0.46, "44CC88", x_offset, y_offset) time.sleep(0.15) - element.flash(0.42, "8844CC") + element.flash(0.42, "8844CC", x_offset, y_offset) time.sleep(0.15) - element.flash(0.38, "CC8844") + element.flash(0.38, "CC8844", x_offset, y_offset) time.sleep(0.15) - element.flash(0.30, "44CC88") + element.flash(0.30, "44CC88", x_offset, y_offset) time.sleep(0.30) def focus(self, selector): - self.find_element(selector).focus() + element = self.find_element(selector) + element.scroll_into_view() + element.focus() def highlight_overlay(self, selector): self.find_element(selector).highlight_overlay() @@ -646,7 +679,7 @@ def remove_elements(self, selector): def send_keys(self, selector, text, timeout=settings.SMALL_TIMEOUT): self.__slow_mode_pause_if_set() element = self.select(selector, timeout=timeout) - self.__add_light_pause() + element.scroll_into_view() if text.endswith("\n") or text.endswith("\r"): text = text[:-1] + "\r\n" element.send_keys(text) @@ -657,7 +690,7 @@ def press_keys(self, selector, text, timeout=settings.SMALL_TIMEOUT): """Similar to send_keys(), but presses keys at human speed.""" self.__slow_mode_pause_if_set() element = self.select(selector, timeout=timeout) - self.__add_light_pause() + element.scroll_into_view() submit = False if text.endswith("\n") or text.endswith("\r"): submit = True @@ -675,7 +708,7 @@ def type(self, selector, text, timeout=settings.SMALL_TIMEOUT): """Similar to send_keys(), but clears the text field first.""" self.__slow_mode_pause_if_set() element = self.select(selector, timeout=timeout) - self.__add_light_pause() + element.scroll_into_view() with suppress(Exception): element.clear_input() if text.endswith("\n") or text.endswith("\r"): @@ -688,8 +721,8 @@ def set_value(self, selector, text, timeout=settings.SMALL_TIMEOUT): """Similar to send_keys(), but clears the text field first.""" self.__slow_mode_pause_if_set() selector = self.__convert_to_css_if_xpath(selector) - self.select(selector, timeout=timeout) - self.__add_light_pause() + element = self.select(selector, timeout=timeout) + element.scroll_into_view() press_enter = False if text.endswith("\n"): text = text[:-1] @@ -1655,17 +1688,24 @@ def assert_url_contains(self, substring): raise Exception(error % (expected, actual)) def assert_text( - self, text, selector="html", timeout=settings.SMALL_TIMEOUT + self, text, selector="body", timeout=settings.SMALL_TIMEOUT ): + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) text = text.strip() element = None try: element = self.find_element(selector, timeout=timeout) except Exception: raise Exception("Element {%s} not found!" % selector) - for i in range(30): + for i in range(int(timeout * 10)): + with suppress(Exception): + element = self.find_element(selector, timeout=0.1) if text in element.text_all: return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break time.sleep(0.1) raise Exception( "Text {%s} not found in {%s}! Actual text: {%s}" @@ -1673,20 +1713,27 @@ def assert_text( ) def assert_exact_text( - self, text, selector="html", timeout=settings.SMALL_TIMEOUT + self, text, selector="body", timeout=settings.SMALL_TIMEOUT ): + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) text = text.strip() element = None try: element = self.select(selector, timeout=timeout) except Exception: raise Exception("Element {%s} not found!" % selector) - for i in range(30): + for i in range(int(timeout * 10)): + with suppress(Exception): + element = self.select(selector, timeout=0.1) if ( self.is_element_visible(selector) and text.strip() == element.text_all.strip() ): return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break time.sleep(0.1) raise Exception( "Expected Text {%s}, is not equal to {%s} in {%s}!" @@ -1727,26 +1774,31 @@ def scroll_to_y(self, y): with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(js_code)) self.loop.run_until_complete(self.page.wait()) + self.__add_light_pause() def scroll_to_top(self): js_code = "window.scrollTo(0, 0);" with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(js_code)) self.loop.run_until_complete(self.page.wait()) + self.__add_light_pause() def scroll_to_bottom(self): js_code = "window.scrollTo(0, 10000);" with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(js_code)) self.loop.run_until_complete(self.page.wait()) + self.__add_light_pause() def scroll_up(self, amount=25): self.loop.run_until_complete(self.page.scroll_up(amount)) self.loop.run_until_complete(self.page.wait()) + self.__add_light_pause() def scroll_down(self, amount=25): self.loop.run_until_complete(self.page.scroll_down(amount)) self.loop.run_until_complete(self.page.wait()) + self.__add_light_pause() def save_screenshot(self, name, folder=None, selector=None): filename = name diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index d31abe9cd22..966a21ed693 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1451,7 +1451,7 @@ def is_element_enabled(self, selector, by="css selector"): return self.__is_shadow_element_enabled(selector) return page_actions.is_element_enabled(self.driver, selector, by) - def is_text_visible(self, text, selector="html", by="css selector"): + def is_text_visible(self, text, selector="body", by="css selector"): """Returns whether the text substring is visible in the element.""" self.wait_for_ready_state_complete() time.sleep(0.01) @@ -1460,7 +1460,7 @@ def is_text_visible(self, text, selector="html", by="css selector"): return self.__is_shadow_text_visible(text, selector) return page_actions.is_text_visible(self.driver, text, selector, by) - def is_exact_text_visible(self, text, selector="html", by="css selector"): + def is_exact_text_visible(self, text, selector="body", by="css selector"): """Returns whether the text is exactly equal to the element text. (Leading and trailing whitespace is ignored in the verification.)""" self.wait_for_ready_state_complete() @@ -1472,7 +1472,7 @@ def is_exact_text_visible(self, text, selector="html", by="css selector"): self.driver, text, selector, by ) - def is_non_empty_text_visible(self, selector="html", by="css selector"): + def is_non_empty_text_visible(self, selector="body", by="css selector"): """Returns whether the element has any non-empty text visible. Whitespace-only text is considered empty text.""" self.wait_for_ready_state_complete() @@ -1842,7 +1842,7 @@ def click_partial_link_text(self, partial_link_text, timeout=None): elif self.slow_mode: self.__slow_mode_pause_if_active() - def get_text(self, selector="html", by="css selector", timeout=None): + def get_text(self, selector="body", by="css selector", timeout=None): self.__check_scope() if not timeout: timeout = settings.LARGE_TIMEOUT @@ -2105,7 +2105,7 @@ def get_property( return property_value def get_text_content( - self, selector="html", by="css selector", timeout=None + self, selector="body", by="css selector", timeout=None ): """Returns the text that appears in the HTML for an element. This is different from "self.get_text(selector, by="css selector")" @@ -6093,7 +6093,7 @@ def highlight_elements( if limit > 0 and count >= limit: break - def press_up_arrow(self, selector="html", times=1, by="css selector"): + def press_up_arrow(self, selector="body", times=1, by="css selector"): """Simulates pressing the UP Arrow on the keyboard. By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" @@ -6115,7 +6115,7 @@ def press_up_arrow(self, selector="html", times=1, by="css selector"): if self.slow_mode: time.sleep(0.1) - def press_down_arrow(self, selector="html", times=1, by="css selector"): + def press_down_arrow(self, selector="body", times=1, by="css selector"): """Simulates pressing the DOWN Arrow on the keyboard. By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" @@ -6137,7 +6137,7 @@ def press_down_arrow(self, selector="html", times=1, by="css selector"): if self.slow_mode: time.sleep(0.1) - def press_left_arrow(self, selector="html", times=1, by="css selector"): + def press_left_arrow(self, selector="body", times=1, by="css selector"): """Simulates pressing the LEFT Arrow on the keyboard. By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" @@ -6159,7 +6159,7 @@ def press_left_arrow(self, selector="html", times=1, by="css selector"): if self.slow_mode: time.sleep(0.1) - def press_right_arrow(self, selector="html", times=1, by="css selector"): + def press_right_arrow(self, selector="body", times=1, by="css selector"): """Simulates pressing the RIGHT Arrow on the keyboard. By default, "html" will be used as the CSS Selector target. You can specify how many times in-a-row the action happens.""" @@ -9730,7 +9730,7 @@ def assert_elements_visible(self, *args, **kwargs): ############ def wait_for_text_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): self.__check_scope() if not timeout: @@ -9748,7 +9748,7 @@ def wait_for_text_visible( ) def wait_for_exact_text_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): self.__check_scope() if not timeout: @@ -9765,7 +9765,7 @@ def wait_for_exact_text_visible( ) def wait_for_non_empty_text_visible( - self, selector="html", by="css selector", timeout=None + self, selector="body", by="css selector", timeout=None ): """Searches for any text in the element of the given selector. Returns the element if it has visible text within the timeout. @@ -9786,7 +9786,7 @@ def wait_for_non_empty_text_visible( ) def wait_for_text( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """The shorter version of wait_for_text_visible()""" self.__check_scope() @@ -9799,7 +9799,7 @@ def wait_for_text( ) def wait_for_exact_text( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """The shorter version of wait_for_exact_text_visible()""" self.__check_scope() @@ -9812,7 +9812,7 @@ def wait_for_exact_text( ) def wait_for_non_empty_text( - self, selector="html", by="css selector", timeout=None + self, selector="body", by="css selector", timeout=None ): """The shorter version of wait_for_non_empty_text_visible()""" self.__check_scope() @@ -9825,7 +9825,7 @@ def wait_for_non_empty_text( ) def find_text( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Same as wait_for_text_visible() - returns the element""" self.__check_scope() @@ -9838,7 +9838,7 @@ def find_text( ) def find_exact_text( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Same as wait_for_exact_text_visible() - returns the element""" self.__check_scope() @@ -9851,7 +9851,7 @@ def find_exact_text( ) def find_non_empty_text( - self, selector="html", by="css selector", timeout=None + self, selector="body", by="css selector", timeout=None ): """Same as wait_for_non_empty_text_visible() - returns the element""" self.__check_scope() @@ -9864,7 +9864,7 @@ def find_non_empty_text( ) def assert_text_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Same as assert_text()""" self.__check_scope() @@ -9875,7 +9875,7 @@ def assert_text_visible( return self.assert_text(text, selector, by=by, timeout=timeout) def assert_text( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Similar to wait_for_text_visible() Raises an exception if the element or the text is not found. @@ -9945,7 +9945,7 @@ def assert_text( return True def assert_exact_text( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Similar to assert_text(), but the text must be exact, rather than exist as a subset of the full text. @@ -9992,7 +9992,7 @@ def assert_exact_text( return True def assert_non_empty_text( - self, selector="html", by="css selector", timeout=None + self, selector="body", by="css selector", timeout=None ): """Assert that the element has any non-empty text visible. Raises an exception if the element has no text within the timeout. @@ -10279,7 +10279,7 @@ def assert_element_not_visible( ############ def wait_for_text_not_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): self.__check_scope() if not timeout: @@ -10292,7 +10292,7 @@ def wait_for_text_not_visible( ) def wait_for_exact_text_not_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): self.__check_scope() if not timeout: @@ -10305,7 +10305,7 @@ def wait_for_exact_text_not_visible( ) def assert_text_not_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Similar to wait_for_text_not_visible() Raises an exception if the text is still visible after timeout. @@ -10326,7 +10326,7 @@ def assert_text_not_visible( return True def assert_exact_text_not_visible( - self, text, selector="html", by="css selector", timeout=None + self, text, selector="body", by="css selector", timeout=None ): """Similar to wait_for_exact_text_not_visible() Raises an exception if the exact text is still visible after timeout. @@ -11083,7 +11083,7 @@ def deferred_assert_element_present( return False def deferred_assert_text( - self, text, selector="html", by="css selector", timeout=None, fs=False + self, text, selector="body", by="css selector", timeout=None, fs=False ): """A non-terminating assertion for text from an element on a page. Failures will be saved until the process_deferred_asserts() @@ -11119,7 +11119,7 @@ def deferred_assert_text( return False def deferred_assert_exact_text( - self, text, selector="html", by="css selector", timeout=None, fs=False + self, text, selector="body", by="css selector", timeout=None, fs=False ): """A non-terminating assertion for exact text from an element. Failures will be saved until the process_deferred_asserts() @@ -11158,7 +11158,7 @@ def deferred_assert_exact_text( def deferred_assert_non_empty_text( self, - selector="html", + selector="body", by="css selector", timeout=None, fs=False, @@ -11278,7 +11278,7 @@ def delayed_assert_element_present( ) def delayed_assert_text( - self, text, selector="html", by="css selector", timeout=None, fs=False + self, text, selector="body", by="css selector", timeout=None, fs=False ): """Same as self.deferred_assert_text()""" return self.deferred_assert_text( @@ -11286,7 +11286,7 @@ def delayed_assert_text( ) def delayed_assert_exact_text( - self, text, selector="html", by="css selector", timeout=None, fs=False + self, text, selector="body", by="css selector", timeout=None, fs=False ): """Same as self.deferred_assert_exact_text()""" return self.deferred_assert_exact_text( @@ -11295,7 +11295,7 @@ def delayed_assert_exact_text( def delayed_assert_non_empty_text( self, - selector="html", + selector="body", by="css selector", timeout=None, fs=False, diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py index 2d1801db656..2e0ef7ef112 100644 --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -1188,6 +1188,8 @@ def highlight_with_jquery_2(driver, message, selector, o_bs, msg_dur): def get_active_element_css(driver): from seleniumbase.js_code import active_css_js + if shared_utils.is_cdp_swap_needed(driver): + return driver.cdp.get_active_element_css() return execute_script(driver, active_css_js.get_active_element_css) diff --git a/seleniumbase/undetected/cdp_driver/element.py b/seleniumbase/undetected/cdp_driver/element.py index c15072f0b11..fe3750c3d4d 100644 --- a/seleniumbase/undetected/cdp_driver/element.py +++ b/seleniumbase/undetected/cdp_driver/element.py @@ -883,6 +883,8 @@ async def flash_async( self, duration: typing.Union[float, int] = 0.5, color: typing.Optional[str] = "EE4488", + x_offset: typing.Union[float, int] = 0, + y_offset: typing.Union[float, int] = 0, ): """ Displays for a short time a red dot on the element. @@ -910,8 +912,8 @@ async def flash_async( "width:8px;height:8px;border-radius:50%;background:#{};" "animation:show-pointer-ani {:.2f}s ease 1;" ).format( - pos.center[0] - 4, # -4 to account for drawn circle itself (w,h) - pos.center[1] - 4, + pos.center[0] + x_offset - 4, # -4 to account for the circle + pos.center[1] + y_offset - 4, # -4 to account for the circle color, duration, ) From eeff476f67f607ca5cbc57cc1974306b1b1ddcaa Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 10 Dec 2024 01:19:46 -0500 Subject: [PATCH 2/5] Improve multi-threading --- seleniumbase/core/browser_launcher.py | 32 ++++++++----- seleniumbase/core/log_helper.py | 14 ++++-- seleniumbase/core/proxy_helper.py | 65 ++++++++++++++------------- seleniumbase/fixtures/shared_utils.py | 11 +++++ seleniumbase/plugins/pytest_plugin.py | 21 ++++++++- 5 files changed, 96 insertions(+), 47 deletions(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index b9ea7ed3e06..0a7e442dc5e 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -1769,17 +1769,27 @@ def _add_chrome_proxy_extension( ): # Single-threaded if zip_it: - proxy_helper.create_proxy_ext( - proxy_string, proxy_user, proxy_pass, bypass_list - ) - proxy_zip = proxy_helper.PROXY_ZIP_PATH - chrome_options.add_extension(proxy_zip) + proxy_zip_lock = fasteners.InterProcessLock(PROXY_ZIP_LOCK) + with proxy_zip_lock: + proxy_helper.create_proxy_ext( + proxy_string, proxy_user, proxy_pass, bypass_list + ) + proxy_zip = proxy_helper.PROXY_ZIP_PATH + chrome_options.add_extension(proxy_zip) else: - proxy_helper.create_proxy_ext( - proxy_string, proxy_user, proxy_pass, bypass_list, zip_it=False - ) - proxy_dir_path = proxy_helper.PROXY_DIR_PATH - chrome_options = add_chrome_ext_dir(chrome_options, proxy_dir_path) + proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK) + with proxy_dir_lock: + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + bypass_list, + zip_it=False, + ) + proxy_dir_path = proxy_helper.PROXY_DIR_PATH + chrome_options = add_chrome_ext_dir( + chrome_options, proxy_dir_path + ) else: # Multi-threaded if zip_it: @@ -1808,7 +1818,7 @@ def _add_chrome_proxy_extension( proxy_user, proxy_pass, bypass_list, - False, + zip_it=False, ) chrome_options = add_chrome_ext_dir( chrome_options, proxy_helper.PROXY_DIR_PATH diff --git a/seleniumbase/core/log_helper.py b/seleniumbase/core/log_helper.py index 6c2b047d3db..ceb9c531a29 100644 --- a/seleniumbase/core/log_helper.py +++ b/seleniumbase/core/log_helper.py @@ -7,6 +7,7 @@ from seleniumbase import config as sb_config from seleniumbase.config import settings from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils python3_11_or_newer = False if sys.version_info >= (3, 11): @@ -33,6 +34,8 @@ def log_screenshot(test_logpath, driver, screenshot=None, get=False): if screenshot != screenshot_warning: with open(screenshot_path, "wb") as file: file.write(screenshot) + with suppress(Exception): + shared_utils.make_writable(screenshot_path) else: print("WARNING: %s" % screenshot_warning) if get: @@ -282,13 +285,14 @@ def log_test_failure_data(test, test_logpath, driver, browser, url=None): sb_config._report_time = the_time sb_config._report_traceback = traceback_message sb_config._report_exception = exc_message - with suppress(Exception): - if not os.path.exists(test_logpath): + if not os.path.exists(test_logpath): + with suppress(Exception): os.makedirs(test_logpath) with suppress(Exception): log_file = codecs.open(basic_file_path, "w+", encoding="utf-8") log_file.writelines("\r\n".join(data_to_save)) log_file.close() + shared_utils.make_writable(basic_file_path) def log_skipped_test_data(test, test_logpath, driver, browser, reason): @@ -343,6 +347,7 @@ def log_skipped_test_data(test, test_logpath, driver, browser, reason): log_file = codecs.open(file_path, "w+", encoding="utf-8") log_file.writelines("\r\n".join(data_to_save)) log_file.close() + shared_utils.make_writable(file_path) def log_page_source(test_logpath, driver, source=None): @@ -365,14 +370,15 @@ def log_page_source(test_logpath, driver, source=None): "unresponsive, or closed prematurely!" ) ) - with suppress(Exception): - if not os.path.exists(test_logpath): + if not os.path.exists(test_logpath): + with suppress(Exception): os.makedirs(test_logpath) html_file_path = os.path.join(test_logpath, html_file_name) with suppress(Exception): html_file = codecs.open(html_file_path, "w+", encoding="utf-8") html_file.write(page_source) html_file.close() + shared_utils.make_writable(html_file_path) def get_test_id(test): diff --git a/seleniumbase/core/proxy_helper.py b/seleniumbase/core/proxy_helper.py index a205f8b6474..c1838b2a885 100644 --- a/seleniumbase/core/proxy_helper.py +++ b/seleniumbase/core/proxy_helper.py @@ -2,10 +2,12 @@ import re import warnings import zipfile +from contextlib import suppress from seleniumbase.config import proxy_list from seleniumbase.config import settings from seleniumbase.fixtures import constants from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils DOWNLOADS_DIR = constants.Files.DOWNLOADS_FOLDER PROXY_ZIP_PATH = os.path.join(DOWNLOADS_DIR, "proxy.zip") @@ -109,31 +111,35 @@ def create_proxy_ext( """"minimum_chrome_version":"22.0.0"\n""" """}""" ) - import threading - - lock = threading.RLock() # Support multi-threaded tests. Eg. "pytest -n=4" - with lock: - abs_path = os.path.abspath(".") - downloads_path = os.path.join(abs_path, DOWNLOADS_DIR) - if not os.path.exists(downloads_path): - os.mkdir(downloads_path) - if zip_it: - zf = zipfile.ZipFile(PROXY_ZIP_PATH, mode="w") - zf.writestr("background.js", background_js) - zf.writestr("manifest.json", manifest_json) - zf.close() - else: - proxy_ext_dir = PROXY_DIR_PATH - if not os.path.exists(proxy_ext_dir): - os.mkdir(proxy_ext_dir) - manifest_file = os.path.join(proxy_ext_dir, "manifest.json") - with open(manifest_file, mode="w") as f: - f.write(manifest_json) - proxy_host = proxy_string.split(":")[0] - proxy_port = proxy_string.split(":")[1] - background_file = os.path.join(proxy_ext_dir, "background.js") - with open(background_file, mode="w") as f: - f.write(background_js) + abs_path = os.path.abspath(".") + downloads_path = os.path.join(abs_path, DOWNLOADS_DIR) + if not os.path.exists(downloads_path): + os.mkdir(downloads_path) + if zip_it: + zf = zipfile.ZipFile(PROXY_ZIP_PATH, mode="w") + zf.writestr("background.js", background_js) + zf.writestr("manifest.json", manifest_json) + zf.close() + with suppress(Exception): + shared_utils.make_writable(PROXY_ZIP_PATH) + else: + proxy_ext_dir = PROXY_DIR_PATH + if not os.path.exists(proxy_ext_dir): + os.mkdir(proxy_ext_dir) + with suppress(Exception): + shared_utils.make_writable(proxy_ext_dir) + manifest_file = os.path.join(proxy_ext_dir, "manifest.json") + with open(manifest_file, mode="w") as f: + f.write(manifest_json) + with suppress(Exception): + shared_utils.make_writable(manifest_json) + proxy_host = proxy_string.split(":")[0] + proxy_port = proxy_string.split(":")[1] + background_file = os.path.join(proxy_ext_dir, "background.js") + with open(background_file, mode="w") as f: + f.write(background_js) + with suppress(Exception): + shared_utils.make_writable(background_js) def remove_proxy_zip_if_present(): @@ -141,13 +147,12 @@ def remove_proxy_zip_if_present(): Used in the implementation of https://stackoverflow.com/a/35293284 for https://stackoverflow.com/questions/12848327/ """ - try: - if os.path.exists(PROXY_ZIP_PATH): + if os.path.exists(PROXY_ZIP_PATH): + with suppress(Exception): os.remove(PROXY_ZIP_PATH) - if os.path.exists(PROXY_ZIP_LOCK): + if os.path.exists(PROXY_ZIP_LOCK): + with suppress(Exception): os.remove(PROXY_ZIP_LOCK) - except Exception: - pass def validate_proxy_string(proxy_string): diff --git a/seleniumbase/fixtures/shared_utils.py b/seleniumbase/fixtures/shared_utils.py index 9ca88a180cd..1ca6b8a1e20 100644 --- a/seleniumbase/fixtures/shared_utils.py +++ b/seleniumbase/fixtures/shared_utils.py @@ -1,6 +1,7 @@ """Shared utility methods""" import colorama import os +import pathlib import platform import sys import time @@ -128,6 +129,16 @@ def is_chrome_130_or_newer(self, binary_location=None): return False +def make_dir_files_writable(dir_path): + # Make all files in the given directory writable. + for file_path in pathlib.Path(dir_path).glob("*"): + if file_path.is_file(): + mode = os.stat(file_path).st_mode + mode |= (mode & 0o444) >> 1 # copy R bits to W + with suppress(Exception): + os.chmod(file_path, mode) + + def make_writable(file_path): # Set permissions to: "If you can read it, you can write it." mode = os.stat(file_path).st_mode diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 3846ee3ca98..a729070c823 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -2143,6 +2143,9 @@ def _perform_pytest_unconfigure_(config): log_helper.archive_logs_if_set( constants.Logs.LATEST + "/", sb_config.archive_logs ) + if os.path.exists("./assets/"): # Used by pytest-html reports + with suppress(Exception): + shared_utils.make_dir_files_writable("./assets/") log_helper.clear_empty_logs() # Dashboard post-processing: Disable time-based refresh and stamp complete if not hasattr(sb_config, "dashboard") or not sb_config.dashboard: @@ -2207,8 +2210,12 @@ def _perform_pytest_unconfigure_(config): ) with open(html_report_path, "w", encoding="utf-8") as f: f.write(the_html_r) # Finalize the HTML report + with suppress(Exception): + shared_utils.make_writable(html_report_path) with open(html_report_path_copy, "w", encoding="utf-8") as f: - f.write(the_html_r) # Finalize the HTML report + f.write(the_html_r) # Finalize the HTML report copy + with suppress(Exception): + shared_utils.make_writable(html_report_path_copy) assets_style = "./assets/style.css" if os.path.exists(assets_style): html_style = None @@ -2223,6 +2230,8 @@ def _perform_pytest_unconfigure_(config): ) with open(assets_style, "w", encoding="utf-8") as f: f.write(html_style) + with suppress(Exception): + shared_utils.make_writable(assets_style) # Done with "pytest_unconfigure" unless using the Dashboard return stamp = "" @@ -2304,6 +2313,8 @@ def _perform_pytest_unconfigure_(config): ) with open(dashboard_path, "w", encoding="utf-8") as f: f.write(the_html_d) # Finalize the dashboard + with suppress(Exception): + shared_utils.make_writable(dashboard_path) assets_style = "./assets/style.css" if os.path.exists(assets_style): html_style = None @@ -2318,6 +2329,8 @@ def _perform_pytest_unconfigure_(config): ) with open(assets_style, "w", encoding="utf-8") as f: f.write(html_style) + with suppress(Exception): + shared_utils.make_writable(assets_style) # Part 2: Appending a pytest html report with dashboard data html_report_path = None if sb_config._html_report_name: @@ -2398,8 +2411,12 @@ def _perform_pytest_unconfigure_(config): ) with open(html_report_path, "w", encoding="utf-8") as f: f.write(the_html_r) # Finalize the HTML report + with suppress(Exception): + shared_utils.make_writable(html_report_path) with open(html_report_path_copy, "w", encoding="utf-8") as f: - f.write(the_html_r) # Finalize the HTML report + f.write(the_html_r) # Finalize the HTML report copy + with suppress(Exception): + shared_utils.make_writable(html_report_path_copy) except KeyboardInterrupt: pass except Exception: From 1021bc60dfbbfc78ba584b12c477642c303f6a15 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 10 Dec 2024 01:21:57 -0500 Subject: [PATCH 3/5] Update examples --- examples/cdp_mode/raw_albertsons.py | 4 +--- examples/cdp_mode/raw_nordstrom.py | 2 +- examples/cdp_mode/raw_tiktok.py | 2 +- examples/raw_pixelscan.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/cdp_mode/raw_albertsons.py b/examples/cdp_mode/raw_albertsons.py index b4837512d19..22e078205b3 100644 --- a/examples/cdp_mode/raw_albertsons.py +++ b/examples/cdp_mode/raw_albertsons.py @@ -24,9 +24,7 @@ info_selector = 'span[data-test-id*="recipe-thumb-title"]' items = sb.cdp.find_elements("%s %s" % (item_selector, info_selector)) for item in items: - sb.sleep(0.03) - item.scroll_into_view() - sb.sleep(0.025) + sb.sleep(0.06) if required_text in item.text: item.flash(color="44CC88") sb.sleep(0.025) diff --git a/examples/cdp_mode/raw_nordstrom.py b/examples/cdp_mode/raw_nordstrom.py index fdb3ab11432..f2f0f3e6854 100644 --- a/examples/cdp_mode/raw_nordstrom.py +++ b/examples/cdp_mode/raw_nordstrom.py @@ -11,7 +11,7 @@ sb.sleep(2.2) for i in range(16): sb.cdp.scroll_down(16) - sb.sleep(0.16) + sb.sleep(0.14) print('*** Nordstrom Search for "%s":' % search) unique_item_text = [] items = sb.cdp.find_elements("article") diff --git a/examples/cdp_mode/raw_tiktok.py b/examples/cdp_mode/raw_tiktok.py index 360a90ad8d8..b685f9171a3 100644 --- a/examples/cdp_mode/raw_tiktok.py +++ b/examples/cdp_mode/raw_tiktok.py @@ -11,5 +11,5 @@ print(sb.cdp.get_text('h2[data-e2e="user-bio"]')) for i in range(55): sb.cdp.scroll_down(12) - sb.sleep(0.06) + sb.sleep(0.05) sb.sleep(1) diff --git a/examples/raw_pixelscan.py b/examples/raw_pixelscan.py index 6c4559c4016..0bddfa40321 100644 --- a/examples/raw_pixelscan.py +++ b/examples/raw_pixelscan.py @@ -1,7 +1,7 @@ from seleniumbase import SB with SB(uc=True, incognito=True, test=True) as sb: - sb.driver.uc_open_with_reconnect("https://pixelscan.net/", 10) + sb.driver.uc_open_with_reconnect("https://pixelscan.net/", 7) sb.remove_elements("div.banner") # Remove the banner sb.remove_elements("jdiv") # Remove chat widgets no_automation_detected = "No automation framework detected" From be1a182e5a5a5ef555ed23273231f1ed3f4763a6 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 10 Dec 2024 01:29:21 -0500 Subject: [PATCH 4/5] Add new CDP Mode examples --- examples/cdp_mode/raw_cf.py | 16 +++++++++++ examples/cdp_mode/raw_pixelscan.py | 19 +++++++++++++ examples/cdp_mode/raw_res_nike.py | 44 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 examples/cdp_mode/raw_cf.py create mode 100644 examples/cdp_mode/raw_pixelscan.py create mode 100644 examples/cdp_mode/raw_res_nike.py diff --git a/examples/cdp_mode/raw_cf.py b/examples/cdp_mode/raw_cf.py new file mode 100644 index 00000000000..744bb55afe0 --- /dev/null +++ b/examples/cdp_mode/raw_cf.py @@ -0,0 +1,16 @@ +"""Using CDP Mode with PyAutoGUI to bypass CAPTCHAs.""" +from seleniumbase import SB + +with SB(uc=True, test=True, locale_code="en") as sb: + url = "https://www.cloudflare.com/login" + sb.activate_cdp_mode(url) + sb.sleep(2) + sb.uc_gui_handle_captcha() # PyAutoGUI press Tab and Spacebar + sb.sleep(2) + +with SB(uc=True, test=True, locale_code="en") as sb: + url = "https://www.cloudflare.com/login" + sb.activate_cdp_mode(url) + sb.sleep(2) + sb.uc_gui_click_captcha() # PyAutoGUI click. (Linux needs it) + sb.sleep(2) diff --git a/examples/cdp_mode/raw_pixelscan.py b/examples/cdp_mode/raw_pixelscan.py new file mode 100644 index 00000000000..155804c059f --- /dev/null +++ b/examples/cdp_mode/raw_pixelscan.py @@ -0,0 +1,19 @@ +from seleniumbase import SB + +with SB(uc=True, incognito=True, test=True) as sb: + sb.activate_cdp_mode("https://pixelscan.net/") + sb.sleep(3) + sb.remove_elements("div.banner") # Remove the banner + sb.remove_elements("jdiv") # Remove chat widgets + sb.cdp.scroll_down(15) + not_masking_text = "You are not masking your fingerprint" + sb.assert_text(not_masking_text, "pxlscn-fingerprint-masking") + no_automation_detected = "No automation framework detected" + sb.assert_text(no_automation_detected, "pxlscn-bot-detection") + sb.highlight("span.text-success", loops=8) + sb.sleep(1) + fingerprint_masking_div = "pxlscn-fingerprint-masking div" + sb.highlight(fingerprint_masking_div, loops=9) + sb.sleep(1) + sb.highlight(".bot-detection-context", loops=10) + sb.sleep(2) diff --git a/examples/cdp_mode/raw_res_nike.py b/examples/cdp_mode/raw_res_nike.py new file mode 100644 index 00000000000..416a001a8d0 --- /dev/null +++ b/examples/cdp_mode/raw_res_nike.py @@ -0,0 +1,44 @@ +"""Using CDP.network.RequestWillBeSent and CDP.network.ResponseReceived.""" +import colorama +import mycdp +import sys +from seleniumbase import SB + +c1 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +async def send_handler(event: mycdp.network.RequestWillBeSent): + r = event.request + s = f"{r.method} {r.url}" + for k, v in r.headers.items(): + s += f"\n\t{k} : {v}" + print(c1 + "*** ==> RequestWillBeSent <== ***" + cr) + print(s) + + +async def receive_handler(event: mycdp.network.ResponseReceived): + print(c2 + "*** ==> ResponseReceived <== ***" + cr) + print(event.response) + + +with SB(uc=True, test=True, locale_code="en", ad_block=True) as sb: + url = "https://www.nike.com/" + sb.activate_cdp_mode(url) + sb.cdp.add_handler(mycdp.network.RequestWillBeSent, send_handler) + sb.cdp.add_handler(mycdp.network.ResponseReceived, receive_handler) + sb.sleep(2.5) + sb.cdp.gui_click_element('div[data-testid="user-tools-container"]') + sb.sleep(1.5) + search = "Nike Air Force 1" + sb.cdp.press_keys('input[type="search"]', search) + sb.sleep(4) + elements = sb.cdp.select_all('ul[data-testid*="products"] figure .details') + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("* " + element.text) + sb.sleep(2) From 0cf36096742d4644d76e7fa9e6b3e4828a058094 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 10 Dec 2024 01:29:36 -0500 Subject: [PATCH 5/5] Version 4.33.7 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 0785261842d..2f8817f109c 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.33.6" +__version__ = "4.33.7"