Update CDP Mode

This commit is contained in:
Michael Mintz 2024-11-01 12:21:57 -04:00
parent d0625e8a8f
commit f242c8df5c
4 changed files with 337 additions and 2 deletions

View File

@ -231,6 +231,7 @@ sb.cdp.find_all(selector)
sb.cdp.find_elements_by_text(text, tag_name=None)
sb.cdp.select(selector)
sb.cdp.select_all(selector)
sb.cdp.find_elements(selector)
sb.cdp.click_link(link_text)
sb.cdp.tile_windows(windows=None, max_columns=0)
sb.cdp.get_all_cookies(*args, **kwargs)
@ -290,6 +291,8 @@ sb.cdp.get_element_attributes(selector)
sb.cdp.get_element_html(selector)
sb.cdp.set_locale(locale)
sb.cdp.set_attributes(selector, attribute, value)
sb.cdp.gui_click_x_y(x, y)
sb.cdp.gui_click_element(selector)
sb.cdp.internalize_links()
sb.cdp.is_element_present(selector)
sb.cdp.is_element_visible(selector)
@ -297,6 +300,8 @@ sb.cdp.assert_element(selector)
sb.cdp.assert_element_present(selector)
sb.cdp.assert_text(text, selector="html")
sb.cdp.assert_exact_text(text, selector="html")
sb.cdp.scroll_down(amount=25)
sb.cdp.scroll_up(amount=25)
sb.cdp.save_screenshot(name, folder=None, selector=None)
```

View File

@ -588,6 +588,7 @@ def uc_open_with_cdp_mode(driver, url=None):
cdp.find_elements_by_text = CDPM.find_elements_by_text
cdp.select = CDPM.select
cdp.select_all = CDPM.select_all
cdp.find_elements = CDPM.find_elements
cdp.click_link = CDPM.click_link
cdp.tile_windows = CDPM.tile_windows
cdp.get_all_cookies = CDPM.get_all_cookies
@ -619,6 +620,8 @@ def uc_open_with_cdp_mode(driver, url=None):
cdp.reset_window_size = CDPM.reset_window_size
cdp.set_locale = CDPM.set_locale
cdp.set_attributes = CDPM.set_attributes
cdp.gui_click_x_y = CDPM.gui_click_x_y
cdp.gui_click_element = CDPM.gui_click_element
cdp.internalize_links = CDPM.internalize_links
cdp.get_window = CDPM.get_window
cdp.get_element_attributes = CDPM.get_element_attributes
@ -655,6 +658,8 @@ def uc_open_with_cdp_mode(driver, url=None):
cdp.assert_element_visible = CDPM.assert_element
cdp.assert_text = CDPM.assert_text
cdp.assert_exact_text = CDPM.assert_exact_text
cdp.scroll_down = CDPM.scroll_down
cdp.scroll_up = CDPM.scroll_up
cdp.save_screenshot = CDPM.save_screenshot
cdp.page = page # async world
cdp.driver = driver.cdp_base # async world
@ -2218,7 +2223,6 @@ def _set_chrome_options(
)
):
chrome_options.add_argument("--no-pings")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--homepage=chrome://version/")
chrome_options.add_argument("--animation-duration-scale=0")
chrome_options.add_argument("--wm-window-animations-disabled")

View File

@ -1,7 +1,9 @@
"""Add CDP methods to extend the driver"""
import fasteners
import math
import os
import re
import sys
import time
from contextlib import suppress
from seleniumbase import config as sb_config
@ -239,6 +241,9 @@ class CDPMethods():
self.__slow_mode_pause_if_set()
return updated_elements
def find_elements(self, selector, timeout=settings.SMALL_TIMEOUT):
return self.select_all(selector, timeout=timeout)
def click_link(self, link_text):
self.find_elements_by_text(link_text, "a")[0].click()
@ -835,6 +840,194 @@ class CDPMethods():
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
def __verify_pyautogui_has_a_headed_browser(self):
"""PyAutoGUI requires a headed browser so that it can
focus on the correct element when performing actions."""
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
if driver.config.headless:
raise Exception(
"PyAutoGUI can't be used in headless mode!"
)
def __install_pyautogui_if_missing(self):
self.__verify_pyautogui_has_a_headed_browser()
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
pip_find_lock = fasteners.InterProcessLock(
constants.PipInstall.FINDLOCK
)
with pip_find_lock: # Prevent issues with multiple processes
try:
import pyautogui
with suppress(Exception):
use_pyautogui_ver = constants.PyAutoGUI.VER
if pyautogui.__version__ != use_pyautogui_ver:
del pyautogui
shared_utils.pip_install(
"pyautogui", version=use_pyautogui_ver
)
import pyautogui
except Exception:
print("\nPyAutoGUI required! Installing now...")
shared_utils.pip_install(
"pyautogui", version=constants.PyAutoGUI.VER
)
try:
import pyautogui
except Exception:
if (
shared_utils.is_linux()
and (not sb_config.headed or sb_config.xvfb)
and not driver.config.headless
):
from sbvirtualdisplay import Display
xvfb_width = 1366
xvfb_height = 768
if (
hasattr(sb_config, "_xvfb_width")
and sb_config._xvfb_width
and isinstance(sb_config._xvfb_width, int)
and hasattr(sb_config, "_xvfb_height")
and sb_config._xvfb_height
and isinstance(sb_config._xvfb_height, int)
):
xvfb_width = sb_config._xvfb_width
xvfb_height = sb_config._xvfb_height
if xvfb_width < 1024:
xvfb_width = 1024
sb_config._xvfb_width = xvfb_width
if xvfb_height < 768:
xvfb_height = 768
sb_config._xvfb_height = xvfb_height
with suppress(Exception):
xvfb_display = Display(
visible=True,
size=(xvfb_width, xvfb_height),
backend="xvfb",
use_xauth=True,
)
xvfb_display.start()
def __get_configured_pyautogui(self, pyautogui_copy):
if (
shared_utils.is_linux()
and hasattr(pyautogui_copy, "_pyautogui_x11")
and "DISPLAY" in os.environ.keys()
):
if (
hasattr(sb_config, "_pyautogui_x11_display")
and sb_config._pyautogui_x11_display
and hasattr(pyautogui_copy._pyautogui_x11, "_display")
and (
sb_config._pyautogui_x11_display
== pyautogui_copy._pyautogui_x11._display
)
):
pass
else:
import Xlib.display
pyautogui_copy._pyautogui_x11._display = (
Xlib.display.Display(os.environ['DISPLAY'])
)
sb_config._pyautogui_x11_display = (
pyautogui_copy._pyautogui_x11._display
)
return pyautogui_copy
def __gui_click_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 click 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)
if timeframe >= 0.25:
time.sleep(0.056) # Wait if moving at human-speed
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y))
pyautogui.click(x=x, y=y)
else:
# Called from a method where the gui_lock is already active
pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
if timeframe >= 0.25:
time.sleep(0.056) # Wait if moving at human-speed
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y))
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()
and (
not hasattr(sb_config, "_saved_width_ratio")
or not sb_config._saved_width_ratio
)
):
window_rect = self.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
self.maximize()
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.set_window_rect(win_x, win_y, width, height)
self.bring_active_window_to_front()
elif (
shared_utils.is_windows()
and hasattr(sb_config, "_saved_width_ratio")
and sb_config._saved_width_ratio
):
width_ratio = sb_config._saved_width_ratio
self.bring_active_window_to_front()
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)
return
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 internalize_links(self):
"""All `target="_blank"` links become `target="_self"`.
This prevents those links from opening in a new tab."""
@ -938,6 +1131,16 @@ class CDPMethods():
% (text, element.text_all, selector)
)
def scroll_down(self, amount=25):
self.loop.run_until_complete(
self.page.scroll_down(amount)
)
def scroll_up(self, amount=25):
self.loop.run_until_complete(
self.page.scroll_up(amount)
)
def save_screenshot(self, name, folder=None, selector=None):
filename = name
if folder:

View File

@ -1,10 +1,16 @@
"""CDP-Driver is based on NoDriver"""
from __future__ import annotations
import asyncio
import fasteners
import logging
import os
import time
import types
import typing
from contextlib import suppress
from seleniumbase import config as sb_config
from seleniumbase.config import settings
from seleniumbase.fixtures import constants
from seleniumbase.fixtures import shared_utils
from typing import Optional, List, Union, Callable
from .element import Element
@ -15,9 +21,120 @@ from .tab import Tab
import mycdp as cdp
logger = logging.getLogger(__name__)
IS_LINUX = shared_utils.is_linux()
T = typing.TypeVar("T")
def __activate_standard_virtual_display():
from sbvirtualdisplay import Display
width = settings.HEADLESS_START_WIDTH
height = settings.HEADLESS_START_HEIGHT
with suppress(Exception):
_xvfb_display = Display(
visible=0, size=(width, height)
)
_xvfb_display.start()
sb_config._virtual_display = _xvfb_display
sb_config.headless_active = True
def __activate_virtual_display_as_needed(
headless, headed, xvfb, xvfb_metrics
):
"""This is only needed on Linux."""
if IS_LINUX and (not headed or xvfb):
from sbvirtualdisplay import Display
pip_find_lock = fasteners.InterProcessLock(
constants.PipInstall.FINDLOCK
)
with pip_find_lock: # Prevent issues with multiple processes
if not headless:
import Xlib.display
try:
_xvfb_width = None
_xvfb_height = None
if xvfb_metrics:
with suppress(Exception):
metrics_string = xvfb_metrics
metrics_string = metrics_string.replace(" ", "")
metrics_list = metrics_string.split(",")[0:2]
_xvfb_width = int(metrics_list[0])
_xvfb_height = int(metrics_list[1])
# The minimum width,height is: 1024,768
if _xvfb_width < 1024:
_xvfb_width = 1024
sb_config._xvfb_width = _xvfb_width
if _xvfb_height < 768:
_xvfb_height = 768
sb_config._xvfb_height = _xvfb_height
xvfb = True
if not _xvfb_width:
_xvfb_width = 1366
if not _xvfb_height:
_xvfb_height = 768
_xvfb_display = Display(
visible=True,
size=(_xvfb_width, _xvfb_height),
backend="xvfb",
use_xauth=True,
)
_xvfb_display.start()
if "DISPLAY" not in os.environ.keys():
print(
"\nX11 display failed! Will use regular xvfb!"
)
__activate_standard_virtual_display()
except Exception as e:
if hasattr(e, "msg"):
print("\n" + str(e.msg))
else:
print(e)
print("\nX11 display failed! Will use regular xvfb!")
__activate_standard_virtual_display()
return
pyautogui_is_installed = False
try:
import pyautogui
with suppress(Exception):
use_pyautogui_ver = constants.PyAutoGUI.VER
if pyautogui.__version__ != use_pyautogui_ver:
del pyautogui # To get newer ver
shared_utils.pip_install(
"pyautogui", version=use_pyautogui_ver
)
import pyautogui
pyautogui_is_installed = True
except Exception:
message = (
"PyAutoGUI is required for UC Mode on Linux! "
"Installing now..."
)
print("\n" + message)
shared_utils.pip_install(
"pyautogui", version=constants.PyAutoGUI.VER
)
import pyautogui
pyautogui_is_installed = True
if (
pyautogui_is_installed
and hasattr(pyautogui, "_pyautogui_x11")
):
try:
pyautogui._pyautogui_x11._display = (
Xlib.display.Display(os.environ['DISPLAY'])
)
sb_config._pyautogui_x11_display = (
pyautogui._pyautogui_x11._display
)
except Exception as e:
if hasattr(e, "msg"):
print("\n" + str(e.msg))
else:
print(e)
else:
__activate_standard_virtual_display()
async def start(
config: Optional[Config] = None,
*,
@ -27,11 +144,14 @@ async def start(
guest: Optional[bool] = False,
browser_executable_path: Optional[PathLike] = None,
browser_args: Optional[List[str]] = None,
xvfb_metrics: Optional[List[str]] = None, # "Width,Height" for Linux
sandbox: Optional[bool] = True,
lang: Optional[str] = None,
host: Optional[str] = None,
port: Optional[int] = None,
expert: Optional[bool] = None,
xvfb: Optional[int] = None, # Use a special virtual display on Linux
headed: Optional[bool] = None, # Override default Xvfb mode on Linux
expert: Optional[bool] = None, # Open up closed Shadow-root elements
**kwargs: Optional[dict],
) -> Browser:
"""
@ -73,6 +193,9 @@ async def start(
(For example, ensuring shadow-root is always in "open" mode.)
:type expert: bool
"""
if IS_LINUX and not headless and not headed and not xvfb:
xvfb = True # The default setting on Linux
__activate_virtual_display_as_needed(headless, headed, xvfb, xvfb_metrics)
if not config:
config = Config(
user_data_dir,