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.find_elements_by_text(text, tag_name=None)
sb.cdp.select(selector) sb.cdp.select(selector)
sb.cdp.select_all(selector) sb.cdp.select_all(selector)
sb.cdp.find_elements(selector)
sb.cdp.click_link(link_text) sb.cdp.click_link(link_text)
sb.cdp.tile_windows(windows=None, max_columns=0) sb.cdp.tile_windows(windows=None, max_columns=0)
sb.cdp.get_all_cookies(*args, **kwargs) 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.get_element_html(selector)
sb.cdp.set_locale(locale) sb.cdp.set_locale(locale)
sb.cdp.set_attributes(selector, attribute, value) 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.internalize_links()
sb.cdp.is_element_present(selector) sb.cdp.is_element_present(selector)
sb.cdp.is_element_visible(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_element_present(selector)
sb.cdp.assert_text(text, selector="html") sb.cdp.assert_text(text, selector="html")
sb.cdp.assert_exact_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) 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.find_elements_by_text = CDPM.find_elements_by_text
cdp.select = CDPM.select cdp.select = CDPM.select
cdp.select_all = CDPM.select_all cdp.select_all = CDPM.select_all
cdp.find_elements = CDPM.find_elements
cdp.click_link = CDPM.click_link cdp.click_link = CDPM.click_link
cdp.tile_windows = CDPM.tile_windows cdp.tile_windows = CDPM.tile_windows
cdp.get_all_cookies = CDPM.get_all_cookies 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.reset_window_size = CDPM.reset_window_size
cdp.set_locale = CDPM.set_locale cdp.set_locale = CDPM.set_locale
cdp.set_attributes = CDPM.set_attributes 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.internalize_links = CDPM.internalize_links
cdp.get_window = CDPM.get_window cdp.get_window = CDPM.get_window
cdp.get_element_attributes = CDPM.get_element_attributes 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_element_visible = CDPM.assert_element
cdp.assert_text = CDPM.assert_text cdp.assert_text = CDPM.assert_text
cdp.assert_exact_text = CDPM.assert_exact_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.save_screenshot = CDPM.save_screenshot
cdp.page = page # async world cdp.page = page # async world
cdp.driver = driver.cdp_base # 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("--no-pings")
chrome_options.add_argument("--disable-popup-blocking")
chrome_options.add_argument("--homepage=chrome://version/") chrome_options.add_argument("--homepage=chrome://version/")
chrome_options.add_argument("--animation-duration-scale=0") chrome_options.add_argument("--animation-duration-scale=0")
chrome_options.add_argument("--wm-window-animations-disabled") chrome_options.add_argument("--wm-window-animations-disabled")

View File

@ -1,7 +1,9 @@
"""Add CDP methods to extend the driver""" """Add CDP methods to extend the driver"""
import fasteners
import math import math
import os import os
import re import re
import sys
import time import time
from contextlib import suppress from contextlib import suppress
from seleniumbase import config as sb_config from seleniumbase import config as sb_config
@ -239,6 +241,9 @@ class CDPMethods():
self.__slow_mode_pause_if_set() self.__slow_mode_pause_if_set()
return updated_elements 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): def click_link(self, link_text):
self.find_elements_by_text(link_text, "a")[0].click() self.find_elements_by_text(link_text, "a")[0].click()
@ -835,6 +840,194 @@ class CDPMethods():
with suppress(Exception): with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code)) 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): def internalize_links(self):
"""All `target="_blank"` links become `target="_self"`. """All `target="_blank"` links become `target="_self"`.
This prevents those links from opening in a new tab.""" This prevents those links from opening in a new tab."""
@ -938,6 +1131,16 @@ class CDPMethods():
% (text, element.text_all, selector) % (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): def save_screenshot(self, name, folder=None, selector=None):
filename = name filename = name
if folder: if folder:

View File

@ -1,10 +1,16 @@
"""CDP-Driver is based on NoDriver""" """CDP-Driver is based on NoDriver"""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import fasteners
import logging import logging
import os
import time import time
import types import types
import typing 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 seleniumbase.fixtures import shared_utils
from typing import Optional, List, Union, Callable from typing import Optional, List, Union, Callable
from .element import Element from .element import Element
@ -15,9 +21,120 @@ from .tab import Tab
import mycdp as cdp import mycdp as cdp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
IS_LINUX = shared_utils.is_linux()
T = typing.TypeVar("T") 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( async def start(
config: Optional[Config] = None, config: Optional[Config] = None,
*, *,
@ -27,11 +144,14 @@ async def start(
guest: Optional[bool] = False, guest: Optional[bool] = False,
browser_executable_path: Optional[PathLike] = None, browser_executable_path: Optional[PathLike] = None,
browser_args: Optional[List[str]] = None, browser_args: Optional[List[str]] = None,
xvfb_metrics: Optional[List[str]] = None, # "Width,Height" for Linux
sandbox: Optional[bool] = True, sandbox: Optional[bool] = True,
lang: Optional[str] = None, lang: Optional[str] = None,
host: Optional[str] = None, host: Optional[str] = None,
port: Optional[int] = 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], **kwargs: Optional[dict],
) -> Browser: ) -> Browser:
""" """
@ -73,6 +193,9 @@ async def start(
(For example, ensuring shadow-root is always in "open" mode.) (For example, ensuring shadow-root is always in "open" mode.)
:type expert: bool :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: if not config:
config = Config( config = Config(
user_data_dir, user_data_dir,