diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index d414567b5f5..5ca94b84cb3 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -323,19 +323,22 @@ sb.cdp.get_active_element_css() sb.cdp.click(selector) sb.cdp.click_active_element() sb.cdp.click_if_visible(selector) +sb.cdp.click_visible_elements(selector) sb.cdp.mouse_click(selector) sb.cdp.nested_click(parent_selector, selector) sb.cdp.get_nested_element(parent_selector, selector) -sb.cdp.flash(selector) +sb.cdp.select_option_by_text(dropdown_selector, option) +sb.cdp.flash(selector, duration=1, color="44CC88", pause=0) +sb.cdp.highlight(selector) sb.cdp.focus(selector) sb.cdp.highlight_overlay(selector) sb.cdp.remove_element(selector) sb.cdp.remove_from_dom(selector) sb.cdp.remove_elements(selector) -sb.cdp.scroll_into_view(selector) sb.cdp.send_keys(selector, text) sb.cdp.press_keys(selector, text) sb.cdp.type(selector, text) +sb.cdp.set_value(selector, text) sb.cdp.evaluate(expression) sb.cdp.js_dumps(obj_name) sb.cdp.maximize() @@ -373,6 +376,11 @@ sb.cdp.gui_press_keys(keys) sb.cdp.gui_write(text) sb.cdp.gui_click_x_y(x, y) sb.cdp.gui_click_element(selector) +sb.cdp.gui_drag_drop_points(x1, y1, x2, y2) +sb.cdp.gui_drag_and_drop(drag_selector, drop_selector) +sb.cdp.gui_hover_x_y(x, y) +sb.cdp.gui_hover_element(selector) +sb.cdp.gui_hover_and_click(hover_selector, click_selector) sb.cdp.internalize_links() sb.cdp.is_checked(selector) sb.cdp.is_selected(selector) @@ -382,12 +390,20 @@ sb.cdp.uncheck_if_checked(selector) sb.cdp.unselect_if_selected(selector) sb.cdp.is_element_present(selector) sb.cdp.is_element_visible(selector) -sb.cdp.assert_element(selector) sb.cdp.assert_element_present(selector) +sb.cdp.assert_element_absent(selector) +sb.cdp.assert_element(selector) +sb.cdp.assert_element_visible(selector) +sb.cdp.assert_element_not_visible(selector) +sb.cdp.assert_title(title) sb.cdp.assert_text(text, selector="html") sb.cdp.assert_exact_text(text, selector="html") -sb.cdp.scroll_down(amount=25) +sb.cdp.scroll_into_view(selector) +sb.cdp.scroll_to_y(y) +sb.cdp.scroll_to_top() +sb.cdp.scroll_to_bottom() sb.cdp.scroll_up(amount=25) +sb.cdp.scroll_down(amount=25) sb.cdp.save_screenshot(name, folder=None, selector=None) ``` diff --git a/examples/cdp_mode/raw_albertsons.py b/examples/cdp_mode/raw_albertsons.py index 68bd05be6d4..b4837512d19 100644 --- a/examples/cdp_mode/raw_albertsons.py +++ b/examples/cdp_mode/raw_albertsons.py @@ -28,7 +28,7 @@ item.scroll_into_view() sb.sleep(0.025) if required_text in item.text: - item.flash() + item.flash(color="44CC88") sb.sleep(0.025) if item.text not in unique_item_text: unique_item_text.append(item.text) diff --git a/examples/cdp_mode/raw_browserscan.py b/examples/cdp_mode/raw_browserscan.py new file mode 100644 index 00000000000..2f60149955d --- /dev/null +++ b/examples/cdp_mode/raw_browserscan.py @@ -0,0 +1,10 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + url = "https://www.browserscan.net/bot-detection" + sb.activate_cdp_mode(url) + sb.sleep(1) + sb.cdp.flash("Test Results", duration=4) + sb.sleep(1) + sb.cdp.assert_element('strong:contains("Normal")') + sb.cdp.flash('strong:contains("Normal")', duration=4, pause=4) diff --git a/examples/cdp_mode/raw_cdp_with_sb.py b/examples/cdp_mode/raw_cdp_with_sb.py index 6821bbdd1a0..0a3ee41e9b8 100644 --- a/examples/cdp_mode/raw_cdp_with_sb.py +++ b/examples/cdp_mode/raw_cdp_with_sb.py @@ -6,7 +6,7 @@ with SB(uc=True, test=True, locale_code="en") as sb: url = "https://www.priceline.com/" sb.activate_cdp_mode(url) - sb.sleep(3) + sb.sleep(2.5) sb.internalize_links() # Don't open links in a new tab sb.click("#link_header_nav_experiences") sb.sleep(2.5) diff --git a/examples/cdp_mode/raw_footlocker.py b/examples/cdp_mode/raw_footlocker.py index 93dcf37dac3..adb0613d656 100644 --- a/examples/cdp_mode/raw_footlocker.py +++ b/examples/cdp_mode/raw_footlocker.py @@ -3,7 +3,7 @@ with SB(uc=True, test=True, locale_code="en") as sb: url = "https://www.footlocker.com/" sb.activate_cdp_mode(url) - sb.sleep(3) + sb.sleep(2.5) sb.cdp.click_if_visible('button[id*="Agree"]') sb.sleep(1.5) sb.cdp.mouse_click('input[aria-label="Search"]') diff --git a/examples/cdp_mode/raw_nike.py b/examples/cdp_mode/raw_nike.py index 8521d263040..2c1ff671d07 100644 --- a/examples/cdp_mode/raw_nike.py +++ b/examples/cdp_mode/raw_nike.py @@ -3,7 +3,7 @@ with SB(uc=True, test=True, locale_code="en") as sb: url = "https://www.nike.com/" sb.activate_cdp_mode(url) - sb.sleep(3) + sb.sleep(2.5) sb.cdp.gui_click_element('div[data-testid="user-tools-container"]') sb.sleep(1.5) search = "Nike Air Force 1" diff --git a/examples/cdp_mode/raw_planetmc.py b/examples/cdp_mode/raw_planetmc.py index ec9637de017..79647f779e2 100644 --- a/examples/cdp_mode/raw_planetmc.py +++ b/examples/cdp_mode/raw_planetmc.py @@ -3,6 +3,6 @@ with SB(uc=True, test=True, locale_code="en") as sb: url = "www.planetminecraft.com/account/sign_in/" sb.activate_cdp_mode(url) - sb.sleep(1) - sb.cdp.gui_click_element("#turnstile-widget") + sb.sleep(2) + sb.cdp.gui_click_element("#turnstile-widget div") sb.sleep(2) diff --git a/examples/cdp_mode/raw_pokemon.py b/examples/cdp_mode/raw_pokemon.py index c7d2d9de057..f6d32b21415 100644 --- a/examples/cdp_mode/raw_pokemon.py +++ b/examples/cdp_mode/raw_pokemon.py @@ -3,22 +3,24 @@ with SB(uc=True, test=True, locale_code="en") as sb: url = "https://www.pokemon.com/us" sb.activate_cdp_mode(url) - sb.sleep(3) + sb.sleep(2.5) sb.cdp.click_if_visible("button#onetrust-reject-all-handler") - sb.sleep(1) + sb.sleep(1.2) sb.cdp.click('a[href="https://www.pokemon.com/us/pokedex/"]') - sb.sleep(1) + sb.sleep(1.2) sb.cdp.click('b:contains("Show Advanced Search")') - sb.sleep(1) + sb.sleep(1.2) sb.cdp.click('span[data-type="type"][data-value="electric"]') - sb.sleep(1) + sb.sleep(0.5) + sb.scroll_into_view("a#advSearch") + sb.sleep(0.5) sb.cdp.click("a#advSearch") - sb.sleep(1) + sb.sleep(1.2) sb.cdp.click('img[src*="img/pokedex/detail/025.png"]') sb.cdp.assert_text("Pikachu", 'div[class*="title"]') sb.cdp.assert_element('img[alt="Pikachu"]') sb.cdp.scroll_into_view("div.pokemon-ability-info") - sb.sleep(1) + sb.sleep(1.2) sb.cdp.flash('div[class*="title"]') sb.cdp.flash('img[alt="Pikachu"]') sb.cdp.flash("div.pokemon-ability-info") diff --git a/examples/cdp_mode/raw_priceline.py b/examples/cdp_mode/raw_priceline.py index b79f4de1e46..079bdc17eba 100644 --- a/examples/cdp_mode/raw_priceline.py +++ b/examples/cdp_mode/raw_priceline.py @@ -4,7 +4,7 @@ window_handle = sb.driver.current_window_handle url = "https://www.priceline.com" sb.activate_cdp_mode(url) - sb.sleep(3) + sb.sleep(2.5) sb.cdp.click('input[name="endLocation"]') sb.sleep(1) location = "Portland, OR, USA" @@ -16,7 +16,7 @@ sb.cdp.click(selection) sb.sleep(1.5) sb.cdp.click('button[aria-label="Dismiss calendar"]') - sb.sleep(5) + sb.sleep(4.5) sb.connect() if len(sb.driver.window_handles) > 1: sb.switch_to_window(window_handle) @@ -24,6 +24,9 @@ sb.sleep(0.2) sb.switch_to_newest_window() sb.sleep(0.6) + for y in range(1, 9): + sb.scroll_to_y(y * 400) + sb.sleep(1.25) hotel_names = sb.find_elements('a[data-autobot-element-id*="HOTEL_NAME"]') hotel_prices = sb.find_elements('span[font-size="4,,,5"]') print("Priceline Hotels in %s:" % location) @@ -34,4 +37,5 @@ for i, hotel in enumerate(hotel_names): if hotel_prices[i] and hotel_prices[i].text: count += 1 - print("* %s: %s => %s" % (count, hotel.text, hotel_prices[i].text)) + hotel_price = "$" + hotel_prices[i].text + print("* %s: %s => %s" % (count, hotel.text, hotel_price)) diff --git a/examples/cdp_mode/raw_xhr_sb.py b/examples/cdp_mode/raw_xhr_sb.py new file mode 100644 index 00000000000..71c0d6b2fc5 --- /dev/null +++ b/examples/cdp_mode/raw_xhr_sb.py @@ -0,0 +1,92 @@ +"""CDP.network.ResponseReceived with CDP.network.ResourceType.XHR.""" +import ast +import asyncio +import colorama +import mycdp +import sys +import time +from seleniumbase.undetected import cdp_driver + +xhr_requests = [] +last_xhr_request = None +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 = "" + + +def listenXHR(page): + async def handler(evt): + # Get AJAX requests + if evt.type_ is mycdp.network.ResourceType.XHR: + xhr_requests.append([evt.response.url, evt.request_id]) + global last_xhr_request + last_xhr_request = time.time() + page.add_handler(mycdp.network.ResponseReceived, handler) + + +async def receiveXHR(page, requests): + responses = [] + retries = 0 + max_retries = 5 + # Wait at least 2 seconds after last XHR request for more + while True: + if last_xhr_request is None or retries > max_retries: + break + if time.time() - last_xhr_request <= 2: + retries = retries + 1 + time.sleep(2) + continue + else: + break + await page + # Loop through gathered requests and get response body + for request in requests: + try: + res = await page.send(mycdp.network.get_response_body(request[1])) + if res is None: + continue + responses.append({ + "url": request[0], + "body": res[0], + "is_base64": res[1], + }) + except Exception as e: + print("Error getting response:", e) + return responses + + +async def crawl(): + driver = await cdp_driver.cdp_util.start_async() + tab = await driver.get("about:blank") + listenXHR(tab) + + # Change url to something that makes ajax requests + tab = await driver.get("https://resttesttest.com/") + time.sleep(1) + # Click AJAX button on https://resttesttest.com/ + element = await tab.select("button#submitajax") + await element.click_async() + time.sleep(2) + + xhr_responses = await receiveXHR(tab, xhr_requests) + for response in xhr_responses: + print(c1 + "*** ==> XHR Request URL <== ***" + cr) + print(f'{response["url"]}') + is_base64 = response["is_base64"] + b64_data = "Base64 encoded data" + try: + headers = ast.literal_eval(response["body"])["headers"] + print(c2 + "*** ==> XHR Response Headers <== ***" + cr) + print(headers if not is_base64 else b64_data) + except Exception: + response_body = response["body"] + print(c2 + "*** ==> XHR Response Body <== ***" + cr) + print(response_body if not is_base64 else b64_data) + + +if __name__ == "__main__": + print("================= Starting =================") + loop = asyncio.new_event_loop() + loop.run_until_complete(crawl()) diff --git a/requirements.txt b/requirements.txt index 8848e2ff77b..b9fbdde70e0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ pip>=24.2 packaging>=24.2 setuptools~=70.2;python_version<"3.10" -setuptools>=73.0.1;python_version>="3.10" +setuptools>=75.5.0;python_version>="3.10" wheel>=0.45.0 attrs>=24.2.0 certifi>=2024.8.30 exceptiongroup>=1.2.2 websockets~=13.1;python_version<"3.9" -websockets>=14.0;python_version>="3.9" +websockets>=14.1;python_version>="3.9" filelock>=3.16.1 fasteners>=0.19 -mycdp>=1.0.1 +mycdp>=1.1.0 pynose>=1.5.3 platformdirs>=4.3.6 typing-extensions>=4.12.2 @@ -64,7 +64,7 @@ rich==13.9.4 # ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.) coverage>=7.6.1;python_version<"3.9" -coverage>=7.6.4;python_version>="3.9" +coverage>=7.6.5;python_version>="3.9" pytest-cov>=5.0.0;python_version<"3.9" pytest-cov>=6.0.0;python_version>="3.9" flake8==5.0.4;python_version<"3.9" diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 9502cb9d3c2..9c90b5febfc 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.32.9" +__version__ = "4.32.10" diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 37d67d0fe9e..756d9bd3873 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -544,6 +544,8 @@ def uc_open_with_cdp_mode(driver, url=None): ) loop.run_until_complete(driver.cdp_base.wait(0)) + gui_lock = fasteners.InterProcessLock(constants.MultiBrowser.PYAUTOGUILOCK) + if ( "chrome-extension://" in str(driver.cdp_base.main_tab) and len(driver.cdp_base.tabs) >= 2 @@ -553,7 +555,8 @@ def uc_open_with_cdp_mode(driver, url=None): for tab in driver.cdp_base.tabs[-1::-1]: if "chrome-extension://" not in str(tab): - loop.run_until_complete(tab.activate()) + with gui_lock: + loop.run_until_complete(tab.activate()) break page_tab = None @@ -566,13 +569,20 @@ def uc_open_with_cdp_mode(driver, url=None): break if page_tab: loop.run_until_complete(page_tab.aopen()) - loop.run_until_complete(page_tab.activate()) + with gui_lock: + loop.run_until_complete(page_tab.activate()) loop.run_until_complete(driver.cdp_base.update_targets()) page = loop.run_until_complete(driver.cdp_base.get(url)) - loop.run_until_complete(page.activate()) + with gui_lock: + loop.run_until_complete(page.activate()) + loop.run_until_complete(page.wait()) if not safe_url: time.sleep(constants.UC.CDP_MODE_OPEN_WAIT) + if IS_WINDOWS: + time.sleep(constants.UC.EXTRA_WINDOWS_WAIT) + else: + time.sleep(0.012) cdp = types.SimpleNamespace() CDPM = sb_cdp.CDPMethods(loop, page, driver) cdp.get = CDPM.get @@ -603,14 +613,15 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.click = CDPM.click cdp.click_active_element = CDPM.click_active_element cdp.click_if_visible = CDPM.click_if_visible + cdp.click_visible_elements = CDPM.click_visible_elements cdp.mouse_click = CDPM.mouse_click cdp.remove_element = CDPM.remove_element cdp.remove_from_dom = CDPM.remove_from_dom cdp.remove_elements = CDPM.remove_elements - cdp.scroll_into_view = CDPM.scroll_into_view cdp.send_keys = CDPM.send_keys cdp.press_keys = CDPM.press_keys cdp.type = CDPM.type + cdp.set_value = CDPM.set_value cdp.evaluate = CDPM.evaluate cdp.js_dumps = CDPM.js_dumps cdp.maximize = CDPM.maximize @@ -625,6 +636,11 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.gui_write = CDPM.gui_write cdp.gui_click_x_y = CDPM.gui_click_x_y cdp.gui_click_element = CDPM.gui_click_element + cdp.gui_drag_drop_points = CDPM.gui_drag_drop_points + cdp.gui_drag_and_drop = CDPM.gui_drag_and_drop + cdp.gui_hover_x_y = CDPM.gui_hover_x_y + cdp.gui_hover_element = CDPM.gui_hover_element + cdp.gui_hover_and_click = CDPM.gui_hover_and_click cdp.internalize_links = CDPM.internalize_links cdp.get_window = CDPM.get_window cdp.get_element_attributes = CDPM.get_element_attributes @@ -651,7 +667,9 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.get_window_rect = CDPM.get_window_rect cdp.get_window_size = CDPM.get_window_size cdp.nested_click = CDPM.nested_click + cdp.select_option_by_text = CDPM.select_option_by_text cdp.flash = CDPM.flash + cdp.highlight = CDPM.highlight cdp.focus = CDPM.focus cdp.highlight_overlay = CDPM.highlight_overlay cdp.get_window_position = CDPM.get_window_position @@ -663,12 +681,19 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.is_element_present = CDPM.is_element_present cdp.is_element_visible = CDPM.is_element_visible cdp.assert_element_present = CDPM.assert_element_present + cdp.assert_element_absent = CDPM.assert_element_absent cdp.assert_element = CDPM.assert_element cdp.assert_element_visible = CDPM.assert_element + cdp.assert_element_not_visible = CDPM.assert_element_not_visible + cdp.assert_title = CDPM.assert_title cdp.assert_text = CDPM.assert_text cdp.assert_exact_text = CDPM.assert_exact_text - cdp.scroll_down = CDPM.scroll_down + cdp.scroll_into_view = CDPM.scroll_into_view + cdp.scroll_to_y = CDPM.scroll_to_y + cdp.scroll_to_top = CDPM.scroll_to_top + cdp.scroll_to_bottom = CDPM.scroll_to_bottom cdp.scroll_up = CDPM.scroll_up + cdp.scroll_down = CDPM.scroll_down cdp.save_screenshot = CDPM.save_screenshot cdp.page = page # async world cdp.driver = driver.cdp_base # async world @@ -680,6 +705,7 @@ def uc_open_with_cdp_mode(driver, url=None): core_items.tab = cdp.tab core_items.util = cdp.util cdp.core = core_items + cdp.loop = cdp.get_event_loop() driver.cdp = cdp driver._is_using_cdp = True @@ -944,48 +970,46 @@ def uc_gui_click_x_y(driver, x, y, timeframe=0.25): connected = True width_ratio = 1.0 if IS_WINDOWS: - try: - driver.window_handles - except Exception: - connected = False + connected = driver.is_connected() if ( not connected and ( not hasattr(sb_config, "_saved_width_ratio") or not sb_config._saved_width_ratio ) + and not __is_cdp_swap_needed(driver) ): driver.reconnect(0.1) - connected = True - if IS_WINDOWS and connected: + if IS_WINDOWS and not __is_cdp_swap_needed(driver): window_rect = driver.get_window_rect() width = window_rect["width"] height = window_rect["height"] win_x = window_rect["x"] win_y = window_rect["y"] - if ( - hasattr(sb_config, "_saved_width_ratio") - and sb_config._saved_width_ratio - ): - width_ratio = sb_config._saved_width_ratio - else: - scr_width = pyautogui.size().width - driver.maximize_window() - win_width = driver.get_window_size()["width"] - width_ratio = round(float(scr_width) / float(win_width), 2) - width_ratio += 0.01 - if width_ratio < 0.45 or width_ratio > 2.55: - width_ratio = 1.01 - sb_config._saved_width_ratio = width_ratio + scr_width = pyautogui.size().width + driver.maximize_window() + win_width = driver.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio driver.minimize_window() driver.set_window_rect(win_x, win_y, width, height) - elif ( - IS_WINDOWS - and not connected - and hasattr(sb_config, "_saved_width_ratio") - and sb_config._saved_width_ratio - ): - width_ratio = sb_config._saved_width_ratio + elif IS_WINDOWS and __is_cdp_swap_needed(driver): + window_rect = driver.cdp.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + driver.cdp.maximize() + win_width = driver.cdp.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + driver.cdp.minimize() + driver.cdp.set_window_rect(win_x, win_y, width, height) if IS_WINDOWS: x = x * width_ratio y = y * width_ratio @@ -1090,6 +1114,21 @@ def _uc_gui_click_captcha( sb_config._saved_width_ratio = width_ratio driver.minimize_window() driver.set_window_rect(win_x, win_y, width, height) + elif IS_WINDOWS and __is_cdp_swap_needed(driver): + window_rect = driver.cdp.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + driver.cdp.maximize() + win_width = driver.cdp.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + driver.cdp.minimize() + driver.cdp.set_window_rect(win_x, win_y, width, height) if ctype == "cf_t": if ( driver.is_element_present(".cf-turnstile-wrapper iframe") @@ -1201,10 +1240,10 @@ def _uc_gui_click_captcha( element = driver.wait_for_element_present( selector, timeout=2.5 ) - x = i_x + element.rect["x"] + int(element.rect["width"] / 2) - x += 1 - y = i_y + element.rect["y"] + int(element.rect["height"] / 2) - y += 1 + x = i_x + element.rect["x"] + (element.rect["width"] / 2.0) + x += 0.5 + y = i_y + element.rect["y"] + (element.rect["height"] / 2.0) + y += 0.5 else: x = (i_x + 34) * width_ratio y = (i_y + 34) * width_ratio @@ -1218,12 +1257,18 @@ def _uc_gui_click_captcha( return if x and y: sb_config._saved_cf_x_y = (x, y) - if driver.is_element_present(".footer .clearfix .ray-id"): - driver.uc_open_with_disconnect(driver.get_current_url(), 3.8) - else: - driver.disconnect() + if not __is_cdp_swap_needed(driver): + if driver.is_element_present(".footer .clearfix .ray-id"): + driver.uc_open_with_disconnect( + driver.get_current_url(), 3.8 + ) + else: + driver.disconnect() with suppress(Exception): _uc_gui_click_x_y(driver, x, y, timeframe=0.32) + if __is_cdp_swap_needed(driver): + time.sleep(float(constants.UC.RECONNECT_TIME) / 2.0) + return reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.6 if IS_LINUX: reconnect_time = constants.UC.RECONNECT_TIME + 0.2 diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index ed432cfe82f..d587b3a104d 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -39,7 +39,9 @@ def __add_sync_methods(self, element): return element element.clear_input = lambda: self.__clear_input(element) element.click = lambda: self.__click(element) - element.flash = lambda: self.__flash(element) + element.flash = lambda *args, **kwargs: self.__flash( + element, *args, **kwargs + ) element.focus = lambda: self.__focus(element) element.highlight_overlay = lambda: self.__highlight_overlay(element) element.mouse_click = lambda: self.__mouse_click(element) @@ -87,6 +89,10 @@ def get(self, url): safe_url = False if not safe_url: time.sleep(constants.UC.CDP_MODE_OPEN_WAIT) + if shared_utils.is_windows(): + time.sleep(constants.UC.EXTRA_WINDOWS_WAIT) + else: + time.sleep(0.012) self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) @@ -186,7 +192,10 @@ def find_elements_by_text(self, text, tag_name=None): ) updated_elements = [] for element in elements: - if not tag_name or tag_name.lower() == element.tag_name.lower(): + if ( + not tag_name + or tag_name.lower().strip() in element.tag_name.lower().strip() + ): element = self.__add_sync_methods(element) updated_elements.append(element) self.__slow_mode_pause_if_set() @@ -258,9 +267,11 @@ def __click(self, element): self.loop.run_until_complete(self.page.wait()) return result - def __flash(self, element): + def __flash(self, element, *args, **kwargs): return ( - self.loop.run_until_complete(element.flash_async()) + self.loop.run_until_complete( + element.flash_async(*args, **kwargs) + ) ) def __focus(self, element): @@ -292,11 +303,11 @@ def __mouse_move(self, element): def __query_selector(self, element, selector): selector = self.__convert_to_css_if_xpath(selector) - element = self.loop.run_until_complete( + element2 = self.loop.run_until_complete( element.query_selector_async(selector) ) - element = self.__add_sync_methods(element) - return element + element2 = self.__add_sync_methods(element2) + return element2 def __query_selector_all(self, element, selector): selector = self.__convert_to_css_if_xpath(selector) @@ -431,6 +442,7 @@ def sleep(self, seconds): def bring_active_window_to_front(self): self.loop.run_until_complete(self.page.bring_to_front()) + self.__add_light_pause() def get_active_element(self): return self.loop.run_until_complete( @@ -467,6 +479,19 @@ def click_if_visible(self, selector): self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) + def click_visible_elements(self, selector): + elements = self.select_all(selector) + for element in elements: + try: + position = element.get_position() + if (position.width != 0 or position.height != 0): + element.click() + time.sleep(0.0375) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait()) + except Exception: + pass + def mouse_click(self, selector, timeout=settings.SMALL_TIMEOUT): """(Attempt simulating a mouse click)""" self.__slow_mode_pause_if_set() @@ -491,9 +516,44 @@ def get_nested_element(self, parent_selector, selector): element = self.find_element(parent_selector) return element.query_selector(selector) - def flash(self, selector): + def select_option_by_text(self, dropdown_selector, option): + element = self.find_element(dropdown_selector) + options = element.query_selector_all("option") + for found_option in options: + if found_option.text.strip() == option.strip(): + found_option.select_option() + return + raise Exception( + "Unable to find text option {%s} in dropdown {%s}!" + % (dropdown_selector, option) + ) + + def flash( + self, + selector, # The CSS Selector to flash + duration=1, # (seconds) flash duration + color="44CC88", # RGB hex flash color + pause=0, # (seconds) If 0, the next action starts during flash + ): """Paint a quickly-vanishing dot over an element.""" - self.find_element(selector).flash() + selector = self.__convert_to_css_if_xpath(selector) + element = self.find_element(selector) + element.flash(duration=duration, color=color) + if pause and isinstance(pause, (int, float)): + time.sleep(pause) + + 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") + time.sleep(0.15) + element.flash(0.42, "8844CC") + time.sleep(0.15) + element.flash(0.38, "CC8844") + time.sleep(0.15) + element.flash(0.30, "44CC88") + time.sleep(0.30) def focus(self, selector): self.find_element(selector).focus() @@ -522,12 +582,10 @@ def remove_elements(self, selector): with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(js_code)) - def scroll_into_view(self, selector): - self.find_element(selector).scroll_into_view() - def send_keys(self, selector, text, timeout=settings.SMALL_TIMEOUT): - element = self.select(selector) self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + self.__add_light_pause() if text.endswith("\n") or text.endswith("\r"): text = text[:-1] + "\r\n" element.send_keys(text) @@ -536,8 +594,9 @@ def send_keys(self, selector, text, timeout=settings.SMALL_TIMEOUT): def press_keys(self, selector, text, timeout=settings.SMALL_TIMEOUT): """Similar to send_keys(), but presses keys at human speed.""" - element = self.select(selector) self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + self.__add_light_pause() submit = False if text.endswith("\n") or text.endswith("\r"): submit = True @@ -553,8 +612,9 @@ def press_keys(self, selector, text, timeout=settings.SMALL_TIMEOUT): def type(self, selector, text, timeout=settings.SMALL_TIMEOUT): """Similar to send_keys(), but clears the text field first.""" - element = self.select(selector) self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + self.__add_light_pause() with suppress(Exception): element.clear_input() if text.endswith("\n") or text.endswith("\r"): @@ -563,6 +623,42 @@ def type(self, selector, text, timeout=settings.SMALL_TIMEOUT): self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) + 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() + press_enter = False + if text.endswith("\n"): + text = text[:-1] + press_enter = True + value = js_utils.escape_quotes_if_needed(re.escape(text)) + css_selector = re.escape(selector) + css_selector = js_utils.escape_quotes_if_needed(css_selector) + set_value_script = ( + """m_elm = document.querySelector('%s');""" + """m_elm.value = '%s';""" % (css_selector, value) + ) + self.loop.run_until_complete(self.page.evaluate(set_value_script)) + the_type = self.get_element_attribute(selector, "type") + if the_type == "range": + # Some input sliders need a mouse event to trigger listeners. + with suppress(Exception): + mouse_move_script = ( + """m_elm = document.querySelector('%s');""" + """m_evt = new Event('mousemove');""" + """m_elm.dispatchEvent(m_evt);""" % css_selector + ) + self.loop.run_until_complete( + self.page.evaluate(mouse_move_script) + ) + elif press_enter: + self.__add_light_pause() + self.send_keys(selector, "\n") + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait()) + def evaluate(self, expression): """Run a JavaScript expression and return the result.""" return self.loop.run_until_complete( @@ -576,21 +672,30 @@ def js_dumps(self, obj_name): ) def maximize(self): - return self.loop.run_until_complete( - self.page.maximize() - ) + if self.get_window()[1].window_state.value == "maximized": + return + elif self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete(self.page.maximize()) + time.sleep(0.0375) + return self.loop.run_until_complete(self.page.maximize()) def minimize(self): - return self.loop.run_until_complete( - self.page.minimize() - ) + if self.get_window()[1].window_state.value != "minimized": + return self.loop.run_until_complete(self.page.minimize()) def medimize(self): - return self.loop.run_until_complete( - self.page.medimize() - ) + if self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete(self.page.medimize()) + time.sleep(0.0375) + return self.loop.run_until_complete(self.page.medimize()) def set_window_rect(self, x, y, width, height): + if self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete( + self.page.set_window_size( + left=x, top=y, width=width, height=height) + ) + time.sleep(0.0375) return self.loop.run_until_complete( self.page.set_window_size( left=x, top=y, width=width, height=height) @@ -602,6 +707,7 @@ def reset_window_size(self): width = settings.CHROME_START_WIDTH height = settings.CHROME_START_HEIGHT self.set_window_rect(x, y, width, height) + self.__add_light_pause() def get_window(self): return self.loop.run_until_complete( @@ -737,8 +843,10 @@ def get_window_position(self): coordinates["y"] = y if y else 0 return coordinates - def get_element_rect(self, selector): + def get_element_rect(self, selector, timeout=settings.SMALL_TIMEOUT): selector = self.__convert_to_css_if_xpath(selector) + self.select(selector, timeout=timeout) + self.__add_light_pause() coordinates = self.loop.run_until_complete( self.page.js_dumps( """document.querySelector""" @@ -785,7 +893,7 @@ def get_gui_element_center(self, selector): e_height = element_rect["height"] e_x = element_rect["x"] e_y = element_rect["y"] - return ((e_x + e_width / 2), (e_y + e_height / 2)) + return ((e_x + e_width / 2.0) + 0.5, (e_y + e_height / 2.0) + 0.5) def get_document(self): return self.loop.run_until_complete( @@ -798,6 +906,7 @@ def get_flattened_document(self): ) def get_element_attributes(self, selector): + selector = self.__convert_to_css_if_xpath(selector) return self.loop.run_until_complete( self.page.js_dumps( """document.querySelector('%s')""" @@ -1013,6 +1122,156 @@ def __gui_click_x_y(self, x, y, timeframe=0.25, uc_lock=False): pyautogui.click(x=x, y=y) def gui_click_x_y(self, x, y, timeframe=0.25): + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + width_ratio = 1.0 + if shared_utils.is_windows(): + window_rect = self.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + self.maximize() + self.__add_light_pause() + win_width = self.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + width_ratio += 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + self.minimize() + self.__add_light_pause() + self.set_window_rect(win_x, win_y, width, height) + self.__add_light_pause() + x = x * width_ratio + y = y * width_ratio + self.bring_active_window_to_front() + self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False) + + def gui_click_element(self, selector, timeframe=0.25): + self.__slow_mode_pause_if_set() + x, y = self.get_gui_element_center(selector) + self.__add_light_pause() + self.gui_click_x_y(x, y, timeframe=timeframe) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait()) + + def __gui_drag_drop(self, x1, y1, x2, y2, timeframe=0.25, uc_lock=False): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x1 < 0 or y1 < 0 or x1 > screen_width or y1 > screen_height: + raise Exception( + "PyAutoGUI cannot drag-drop from point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x1, y1, screen_width, screen_height) + ) + if x2 < 0 or y2 < 0 or x2 > screen_width or y2 > screen_height: + raise Exception( + "PyAutoGUI cannot drag-drop to point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x2, y2, screen_width, screen_height) + ) + if uc_lock: + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad) + self.__add_light_pause() + if "--debug" in sys.argv: + print(" pyautogui.moveTo(%s, %s)" % (x1, y1)) + pyautogui.dragTo(x2, y2, button="left", duration=timeframe) + else: + # Called from a method where the gui_lock is already active + pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad) + self.__add_light_pause() + if "--debug" in sys.argv: + print(" pyautogui.dragTo(%s, %s)" % (x2, y2)) + pyautogui.dragTo(x2, y2, button="left", duration=timeframe) + + def gui_drag_drop_points(self, x1, y1, x2, y2, timeframe=0.35): + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + width_ratio = 1.0 + if shared_utils.is_windows(): + window_rect = self.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + self.maximize() + self.__add_light_pause() + win_width = self.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + width_ratio += 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + self.minimize() + self.__add_light_pause() + self.set_window_rect(win_x, win_y, width, height) + self.__add_light_pause() + x1 = x1 * width_ratio + y1 = y1 * width_ratio + x2 = x2 * width_ratio + y2 = y2 * width_ratio + self.bring_active_window_to_front() + self.__gui_drag_drop( + x1, y1, x2, y2, timeframe=timeframe, uc_lock=False + ) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait()) + + def gui_drag_and_drop(self, drag_selector, drop_selector, timeframe=0.35): + self.__slow_mode_pause_if_set() + x1, y1 = self.get_gui_element_center(drag_selector) + self.__add_light_pause() + x2, y2 = self.get_gui_element_center(drop_selector) + self.__add_light_pause() + self.gui_drag_drop_points(x1, y1, x2, y2, timeframe=timeframe) + + def __gui_hover_x_y(self, x, y, timeframe=0.25, uc_lock=False): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x < 0 or y < 0 or x > screen_width or y > screen_height: + raise Exception( + "PyAutoGUI cannot hover on point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x, y, screen_width, screen_height) + ) + if uc_lock: + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: # Prevent issues with multiple processes + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + time.sleep(0.056) + if "--debug" in sys.argv: + print(" pyautogui.moveTo(%s, %s)" % (x, y)) + else: + # Called from a method where the gui_lock is already active + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + time.sleep(0.056) + if "--debug" in sys.argv: + print(" pyautogui.moveTo(%s, %s)" % (x, y)) + + def gui_hover_x_y(self, x, y, timeframe=0.25): gui_lock = fasteners.InterProcessLock( constants.MultiBrowser.PYAUTOGUILOCK ) @@ -1041,6 +1300,7 @@ def gui_click_x_y(self, x, y, timeframe=0.25): else: scr_width = pyautogui.size().width self.maximize() + self.__add_light_pause() win_width = self.get_window_size()["width"] width_ratio = round(float(scr_width) / float(win_width), 2) width_ratio += 0.01 @@ -1048,6 +1308,7 @@ def gui_click_x_y(self, x, y, timeframe=0.25): width_ratio = 1.01 sb_config._saved_width_ratio = width_ratio self.set_window_rect(win_x, win_y, width, height) + self.__add_light_pause() self.bring_active_window_to_front() elif ( shared_utils.is_windows() @@ -1059,19 +1320,29 @@ def gui_click_x_y(self, x, y, timeframe=0.25): if shared_utils.is_windows(): x = x * width_ratio y = y * width_ratio - self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False) + self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False) return self.bring_active_window_to_front() - self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False) + self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False) - def gui_click_element(self, selector, timeframe=0.25): + def gui_hover_element(self, selector, timeframe=0.25): self.__slow_mode_pause_if_set() x, y = self.get_gui_element_center(selector) self.__add_light_pause() - self.gui_click_x_y(x, y, timeframe=timeframe) + self.__gui_hover_x_y(x, y, timeframe=timeframe) self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.wait()) + def gui_hover_and_click(self, hover_selector, click_selector): + gui_lock = fasteners.InterProcessLock( + constants.MultiBrowser.PYAUTOGUILOCK + ) + with gui_lock: + self.gui_hover_element(hover_selector) + time.sleep(0.15) + self.gui_hover_element(click_selector) + self.click(click_selector) + def internalize_links(self): """All `target="_blank"` links become `target="_self"`. This prevents those links from opening in a new tab.""" @@ -1079,24 +1350,30 @@ def internalize_links(self): def is_checked(self, selector): """Return True if checkbox (or radio button) is checked.""" + selector = self.__convert_to_css_if_xpath(selector) self.find_element(selector, timeout=settings.SMALL_TIMEOUT) return self.get_element_attribute(selector, "checked") def is_selected(self, selector): + selector = self.__convert_to_css_if_xpath(selector) return self.is_checked(selector) def check_if_unchecked(self, selector): + selector = self.__convert_to_css_if_xpath(selector) if not self.is_checked(selector): self.click(selector) def select_if_unselected(self, selector): + selector = self.__convert_to_css_if_xpath(selector) self.check_if_unchecked(selector) def uncheck_if_checked(self, selector): + selector = self.__convert_to_css_if_xpath(selector) if self.is_checked(selector): self.click(selector) def unselect_if_selected(self, selector): + selector = self.__convert_to_css_if_xpath(selector) self.uncheck_if_checked(selector) def is_element_present(self, selector): @@ -1105,32 +1382,22 @@ def is_element_present(self, selector): return True except Exception: return False - selector = self.__convert_to_css_if_xpath(selector) - element = self.loop.run_until_complete( - self.page.js_dumps( - """document.querySelector('%s')""" - % js_utils.escape_quotes_if_needed(re.escape(selector)) - ) - ) - return element is not None def is_element_visible(self, selector): selector = self.__convert_to_css_if_xpath(selector) element = None if ":contains(" not in selector: try: - element = self.loop.run_until_complete( - self.page.js_dumps( - """window.getComputedStyle(document.querySelector""" - """('%s'))""" - % js_utils.escape_quotes_if_needed(re.escape(selector)) - ) - ) + element = self.select(selector, timeout=0.01) except Exception: return False if not element: return False - return element.get("display") != "none" + try: + position = element.get_position() + return (position.width != 0 or position.height != 0) + except Exception: + return False else: with suppress(Exception): tag_name = selector.split(":contains(")[0].split(" ")[-1] @@ -1160,9 +1427,66 @@ def assert_element_present(self, selector, timeout=settings.SMALL_TIMEOUT): raise Exception("Element {%s} not found!" % selector) return True + def assert_element_absent(self, selector, timeout=settings.SMALL_TIMEOUT): + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + if not self.is_element_present(selector): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Element {%s} was still present after %s second%s!" + % (selector, timeout, plural) + ) + + def assert_element_not_visible( + self, selector, timeout=settings.SMALL_TIMEOUT + ): + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + if not self.is_element_present(selector): + return True + elif not self.is_element_visible(selector): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Element {%s} was still visible after %s second%s!" + % (selector, timeout, plural) + ) + + def assert_title(self, title): + expected = title.strip() + actual = self.get_title().strip() + error = ( + "Expected page title [%s] does not match the actual title [%s]!" + ) + try: + if expected != actual: + raise Exception(error % (expected, actual)) + except Exception: + time.sleep(2) + expected = title.strip() + actual = self.get_title().strip() + if expected != actual: + raise Exception(error % (expected, actual)) + def assert_text( self, text, selector="html", timeout=settings.SMALL_TIMEOUT ): + text = text.strip() element = None try: element = self.select(selector, timeout=timeout) @@ -1180,6 +1504,7 @@ def assert_text( def assert_exact_text( self, text, selector="html", timeout=settings.SMALL_TIMEOUT ): + text = text.strip() element = None try: element = self.select(selector, timeout=timeout) @@ -1197,15 +1522,36 @@ def assert_exact_text( % (text, element.text_all, selector) ) - def scroll_down(self, amount=25): - self.loop.run_until_complete( - self.page.scroll_down(amount) - ) + def scroll_into_view(self, selector): + self.find_element(selector).scroll_into_view() + self.loop.run_until_complete(self.page.wait()) + + def scroll_to_y(self, y): + y = int(y) + js_code = "window.scrollTo(0, %s);" % y + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + self.loop.run_until_complete(self.page.wait()) + + 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()) + + 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()) def scroll_up(self, amount=25): - self.loop.run_until_complete( - self.page.scroll_up(amount) - ) + self.loop.run_until_complete(self.page.scroll_up(amount)) + self.loop.run_until_complete(self.page.wait()) + + def scroll_down(self, amount=25): + self.loop.run_until_complete(self.page.scroll_down(amount)) + self.loop.run_until_complete(self.page.wait()) def save_screenshot(self, name, folder=None, selector=None): filename = name diff --git a/seleniumbase/core/sb_driver.py b/seleniumbase/core/sb_driver.py index d4baab0b4be..d01de1639b5 100644 --- a/seleniumbase/core/sb_driver.py +++ b/seleniumbase/core/sb_driver.py @@ -198,6 +198,11 @@ def is_connected(self): In CDP Mode, the CDP-Driver controls the web browser. The CDP-Driver can be connected while WebDriver isn't. """ + if shared_utils.is_windows(): + return ( + not hasattr(self.driver, "_is_connected") + or self.driver._is_connected + ) try: self.driver.window_handles return True @@ -238,6 +243,17 @@ def get_user_agent(self, *args, **kwargs): return js_utils.get_user_agent(self.driver, *args, **kwargs) def highlight(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + selector = None + if "selector" in kwargs: + selector = kwargs["selector"] + else: + selector = args[0] + if ":contains(" not in selector: + self.driver.cdp.highlight(selector) + return + else: + self.driver.connect() if "scroll" in kwargs: kwargs.pop("scroll") w_args = kwargs.copy() diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 9685ebb3b90..23ec93062ff 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1592,6 +1592,9 @@ def get_partial_link_text_attribute( def click_link_text(self, link_text, timeout=None): """This method clicks link text on a page.""" self.__check_scope() + if self.__is_cdp_swap_needed(): + self.cdp.find_element(link_text).click() + return self.__skip_if_esc() if not timeout: timeout = settings.SMALL_TIMEOUT @@ -2197,6 +2200,9 @@ def click_visible_elements( if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.click_visible_elements(selector) + return self.wait_for_ready_state_complete() if self.__needs_minimum_wait(): time.sleep(0.12) @@ -2637,6 +2643,9 @@ def hover(self, selector, by="css selector", timeout=None): original_selector = selector original_by = by selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.gui_hover_element(selector) + return self.wait_for_element_visible( original_selector, by=original_by, timeout=timeout ) @@ -2679,6 +2688,9 @@ def hover_and_click( click_selector, click_by = self.__recalculate_selector( click_selector, click_by ) + if self.__is_cdp_swap_needed(): + self.cdp.gui_hover_and_click(hover_selector, click_selector) + return dropdown_element = self.wait_for_element_visible( original_selector, by=original_by, timeout=timeout ) @@ -3105,6 +3117,9 @@ def select_option_by_text( timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) + if self.__is_cdp_swap_needed(): + self.cdp.select_option_by_text(dropdown_selector, option) + return self.__select_option( dropdown_selector, option, @@ -3419,8 +3434,8 @@ def get_gui_element_center(self, selector, by="css selector"): if self.__is_cdp_swap_needed(): return self.cdp.get_gui_element_center(selector) element_rect = self.get_gui_element_rect(selector, by=by) - x = int(element_rect["x"]) + int(element_rect["width"] / 2) + 1 - y = int(element_rect["y"]) + int(element_rect["height"] / 2) + 1 + x = element_rect["x"] + (element_rect["width"] / 2.0) + 0.5 + y = element_rect["y"] + (element_rect["height"] / 2.0) + 0.5 return (x, y) def get_window_rect(self): @@ -5959,6 +5974,9 @@ def highlight( scroll - the option to scroll to the element first (Default: True) timeout - the time to wait for the element to appear """ self.__check_scope() + if self.__is_cdp_swap_needed() and ":contains(" not in selector: + self.cdp.highlight(selector) + return self._check_browser() self.__skip_if_esc() if isinstance(selector, WebElement): @@ -6126,6 +6144,9 @@ def slow_scroll_to(self, selector, by="css selector", timeout=None): original_selector = selector original_by = by selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed() and ":contains(" not in selector: + self.cdp.scroll_into_view(selector) + return element = self.wait_for_element_visible( original_selector, by=original_by, timeout=timeout ) @@ -6172,24 +6193,36 @@ def scroll_into_view(self, selector, by="css selector", timeout=None): def scroll_to_top(self): """Scroll to the top of the page.""" self.__check_scope() + if self.__is_cdp_swap_needed(): + self.cdp.scroll_to_top() + return scroll_script = "window.scrollTo(0, 0);" - try: + with suppress(Exception): self.execute_script(scroll_script) time.sleep(0.012) - return True - except Exception: - return False def scroll_to_bottom(self): """Scroll to the bottom of the page.""" self.__check_scope() + if self.__is_cdp_swap_needed(): + self.cdp.scroll_to_bottom() + return scroll_script = "window.scrollTo(0, 10000);" - try: + with suppress(Exception): + self.execute_script(scroll_script) + time.sleep(0.012) + + def scroll_to_y(self, y): + """Scroll to y position on the page.""" + self.__check_scope() + y = int(y) + if self.__is_cdp_swap_needed(): + self.cdp.scroll_to_y(y) + return + scroll_script = "window.scrollTo(0, %s);" % y + with suppress(Exception): self.execute_script(scroll_script) time.sleep(0.012) - return True - except Exception: - return False def click_xpath(self, xpath): """Technically, self.click() automatically detects xpath selectors, @@ -7676,6 +7709,9 @@ def assert_title(self, title): but then the title switches over to the actual page title. In Recorder Mode, this assertion is skipped because the Recorder changes the page title to the selector of the hovered element.""" + if self.__is_cdp_swap_needed(): + self.cdp.assert_title(title) + return self.wait_for_ready_state_complete() expected = title.strip() actual = self.get_page_title().strip() @@ -8166,7 +8202,7 @@ def set_value( timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by, xp_ok=False) if self.__is_cdp_swap_needed(): - self.cdp.type(selector, text) + self.cdp.set_value(selector, text) return self.wait_for_ready_state_complete() self.wait_for_element_present(selector, by=by, timeout=timeout) @@ -8932,6 +8968,9 @@ def wait_for_element_not_present( timeout = self.__get_new_timeout(timeout) original_selector = selector selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.assert_element_absent(selector) + return True return page_actions.wait_for_element_absent( self.driver, selector, @@ -9966,6 +10005,9 @@ def assert_link_text(self, link_text, timeout=None): timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) + if self.__is_cdp_swap_needed(): + self.cdp.find_element(link_text) + return self.wait_for_link_text_visible(link_text, timeout=timeout) if self.demo_mode: a_t = "ASSERT LINK TEXT" @@ -10065,6 +10107,9 @@ def assert_element_absent(self, selector, by="css selector", timeout=None): timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) + if self.__is_cdp_swap_needed(): + self.cdp.assert_element_absent(selector) + return True self.wait_for_element_absent(selector, by=by, timeout=timeout) return True @@ -10083,6 +10128,9 @@ def wait_for_element_not_visible( timeout = self.__get_new_timeout(timeout) original_selector = selector selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.assert_element_not_visible(selector) + return True return page_actions.wait_for_element_not_visible( self.driver, selector, diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index 145bd5b8cf5..5ce1cde7193 100644 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -376,6 +376,7 @@ class Mobile: class UC: RECONNECT_TIME = 2.4 # Seconds CDP_MODE_OPEN_WAIT = 0.9 # Seconds + EXTRA_WINDOWS_WAIT = 0.2 # Seconds class ValidBrowsers: diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py index 52bf9154eac..bf69abbced6 100644 --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -10,6 +10,7 @@ from seleniumbase.config import settings from seleniumbase.fixtures import constants from seleniumbase.fixtures import css_to_xpath +from seleniumbase.fixtures import shared_utils from seleniumbase.fixtures import xpath_to_css @@ -24,9 +25,6 @@ def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT): (Previously, tests would fail immediately if exceeding the timeout.)""" if hasattr(settings, "SKIP_JS_WAITS") and settings.SKIP_JS_WAITS: return - if sb_config.time_limit and not sb_config.recorder_mode: - from seleniumbase.fixtures import shared_utils - start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): @@ -243,8 +241,6 @@ def escape_quotes_if_needed(string): def is_in_frame(driver): # Returns True if the driver has switched to a frame. # Returns False if the driver was on default content. - from seleniumbase.fixtures import shared_utils - if shared_utils.is_cdp_swap_needed(driver): return False in_basic_frame = driver.execute_script( diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index b3d9a206f37..f754ad2b334 100644 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -1555,8 +1555,6 @@ def _reconnect_if_disconnected(driver): if ( hasattr(driver, "_is_using_uc") and driver._is_using_uc - and hasattr(driver, "_is_connected") - and not driver._is_connected and hasattr(driver, "is_connected") and not driver.is_connected() ): diff --git a/seleniumbase/fixtures/shared_utils.py b/seleniumbase/fixtures/shared_utils.py index dbfd8ac7064..d8368828e0b 100644 --- a/seleniumbase/fixtures/shared_utils.py +++ b/seleniumbase/fixtures/shared_utils.py @@ -84,6 +84,17 @@ def fix_url_as_needed(url): return url +def reconnect_if_disconnected(driver): + if ( + hasattr(driver, "_is_using_uc") + and driver._is_using_uc + and hasattr(driver, "is_connected") + and not driver.is_connected() + ): + with suppress(Exception): + driver.connect() + + def is_cdp_swap_needed(driver): """ When someone is using CDP Mode with a disconnected webdriver, @@ -93,9 +104,9 @@ def is_cdp_swap_needed(driver): For other webdriver methods, SeleniumBase will reconnect first. """ return ( - driver.is_cdp_mode_active() - # and hasattr(driver, "_is_connected") - # and not driver._is_connected + hasattr(driver, "is_cdp_mode_active") + and driver.is_cdp_mode_active() + and hasattr(driver, "is_connected") and not driver.is_connected() ) diff --git a/seleniumbase/undetected/cdp_driver/element.py b/seleniumbase/undetected/cdp_driver/element.py index 80c2081b2ae..c15072f0b11 100644 --- a/seleniumbase/undetected/cdp_driver/element.py +++ b/seleniumbase/undetected/cdp_driver/element.py @@ -5,6 +5,7 @@ import pathlib import secrets import typing +from contextlib import suppress from . import cdp_util as util from ._contradict import ContraDict from .config import PathLike @@ -768,6 +769,11 @@ def text(self): Gets the text contents of this element and child nodes, concatenated. Note: This includes text in the form of script content, (text nodes). """ + with suppress(Exception): + if self.node.node_name.lower() in ["input", "textarea"]: + input_node = self.node.shadow_roots[0].children[0].children[0] + if input_node: + return input_node.node_value text_nodes = util.filter_recurse_all( self.node, lambda n: n.node_type == 3 ) @@ -776,6 +782,11 @@ def text(self): @property def text_all(self): """Same as text(). Kept for backwards compatibility.""" + with suppress(Exception): + if self.node.node_name.lower() in ["input", "textarea"]: + input_node = self.node.shadow_roots[0].children[0].children[0] + if input_node: + return input_node.node_value text_nodes = util.filter_recurse_all( self.node, lambda n: n.node_type == 3 ) @@ -868,7 +879,11 @@ async def save_screenshot_async( path.write_bytes(data_bytes) return str(path) - async def flash_async(self, duration: typing.Union[float, int] = 0.5): + async def flash_async( + self, + duration: typing.Union[float, int] = 0.5, + color: typing.Optional[str] = "EE4488", + ): """ Displays for a short time a red dot on the element. (Only if the element itself is visible) @@ -892,11 +907,12 @@ async def flash_async(self, duration: typing.Union[float, int] = 0.5): style = ( "position:absolute;z-index:99999999;padding:0;margin:0;" "left:{:.1f}px; top: {:.1f}px; opacity:0.7;" - "width:8px;height:8px;border-radius:50%;background:#EE4488;" + "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, + color, duration, ) script = ( diff --git a/setup.py b/setup.py index bf7861f5eed..fed28f5952b 100755 --- a/setup.py +++ b/setup.py @@ -150,16 +150,16 @@ 'pip>=24.2', 'packaging>=24.2', 'setuptools~=70.2;python_version<"3.10"', # Newer ones had issues - 'setuptools>=73.0.1;python_version>="3.10"', + 'setuptools>=75.5.0;python_version>="3.10"', 'wheel>=0.45.0', 'attrs>=24.2.0', "certifi>=2024.8.30", "exceptiongroup>=1.2.2", 'websockets~=13.1;python_version<"3.9"', - 'websockets>=14.0;python_version>="3.9"', + 'websockets>=14.1;python_version>="3.9"', 'filelock>=3.16.1', 'fasteners>=0.19', - "mycdp>=1.0.1", + "mycdp>=1.1.0", "pynose>=1.5.3", 'platformdirs>=4.3.6', 'typing-extensions>=4.12.2', @@ -222,7 +222,7 @@ # Usage: coverage run -m pytest; coverage html; coverage report "coverage": [ 'coverage>=7.6.1;python_version<"3.9"', - 'coverage>=7.6.4;python_version>="3.9"', + 'coverage>=7.6.5;python_version>="3.9"', 'pytest-cov>=5.0.0;python_version<"3.9"', 'pytest-cov>=6.0.0;python_version>="3.9"', ],