diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md
index bf6ca1ea82b..bfba01be45f 100644
--- a/examples/cdp_mode/ReadMe.md
+++ b/examples/cdp_mode/ReadMe.md
@@ -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)
```
diff --git a/examples/cdp_mode/raw_easyjet.py b/examples/cdp_mode/raw_easyjet.py
new file mode 100644
index 00000000000..35ef468d171
--- /dev/null
+++ b/examples/cdp_mode/raw_easyjet.py
@@ -0,0 +1,42 @@
+from seleniumbase import SB
+
+with SB(uc=True, test=True, locale_code="en") as sb:
+ url = "https://www.easyjet.com/en/"
+ sb.activate_cdp_mode(url)
+ sb.sleep(2.5)
+ sb.cdp.click_if_visible('button#ensRejectAll')
+ sb.sleep(1.2)
+ sb.cdp.click('input[name="from"]')
+ sb.sleep(1.2)
+ sb.cdp.type('input[name="from"]', "London")
+ sb.sleep(1.2)
+ sb.cdp.click('span[data-testid="airport-name"]')
+ sb.sleep(1.2)
+ sb.cdp.type('input[name="to"]', "Venice")
+ sb.sleep(1.2)
+ sb.cdp.click('span[data-testid="airport-name"]')
+ sb.sleep(1.2)
+ sb.cdp.click('input[name="when"]')
+ sb.sleep(1.2)
+ sb.cdp.click('[data-testid="month"] button[aria-disabled="false"]')
+ sb.sleep(1.2)
+ sb.cdp.click('[data-testid="month"]:last-of-type [aria-disabled="false"]')
+ sb.sleep(1.2)
+ sb.cdp.click('button[data-testid="submit"]')
+ sb.sleep(3.5)
+ sb.connect()
+ sb.sleep(0.5)
+ if "easyjet.com" not in sb.get_current_url():
+ sb.driver.close()
+ sb.switch_to_newest_window()
+ days = sb.find_elements("div.flight-grid-day")
+ for day in days:
+ print("**** " + " ".join(day.text.split("\n")[0:2]) + " ****")
+ fares = day.find_elements("css selector", "button.selectable")
+ if not fares:
+ print("No flights today!")
+ for fare in fares:
+ info = fare.text
+ info = info.replace("LOWEST FARE\n", "")
+ info = info.replace("\n", " ")
+ print(info)
diff --git a/examples/cdp_mode/raw_req_async.py b/examples/cdp_mode/raw_req_async.py
index 1f5abffacc4..56e331b7ef1 100644
--- a/examples/cdp_mode/raw_req_async.py
+++ b/examples/cdp_mode/raw_req_async.py
@@ -1,4 +1,4 @@
-"""Using CDP.fetch.RequestPaused to filter content in real time."""
+"""Using CDP.fetch.RequestPaused to filter content in real-time."""
import asyncio
import mycdp
from seleniumbase import decorators
diff --git a/examples/cdp_mode/raw_req_sb.py b/examples/cdp_mode/raw_req_sb.py
index 0de2ceae450..241b5668683 100644
--- a/examples/cdp_mode/raw_req_sb.py
+++ b/examples/cdp_mode/raw_req_sb.py
@@ -1,4 +1,4 @@
-"""Using CDP.fetch.RequestPaused to filter content in real time."""
+"""Using CDP.fetch.RequestPaused to filter content in real-time."""
import mycdp
from seleniumbase import SB
diff --git a/examples/cdp_mode/raw_res_sb.py b/examples/cdp_mode/raw_res_sb.py
new file mode 100644
index 00000000000..e2583f320bd
--- /dev/null
+++ b/examples/cdp_mode/raw_res_sb.py
@@ -0,0 +1,34 @@
+"""Using CDP.network.ResponseReceived and CDP.network.RequestWillBeSent."""
+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") as sb:
+ sb.activate_cdp_mode("about:blank")
+ sb.cdp.add_handler(mycdp.network.RequestWillBeSent, send_handler)
+ sb.cdp.add_handler(mycdp.network.ResponseReceived, receive_handler)
+ url = "https://seleniumbase.io/apps/calculator"
+ sb.cdp.open(url)
+ sb.sleep(1)
diff --git a/examples/cdp_mode/raw_walmart.py b/examples/cdp_mode/raw_walmart.py
new file mode 100644
index 00000000000..31d704b09ca
--- /dev/null
+++ b/examples/cdp_mode/raw_walmart.py
@@ -0,0 +1,31 @@
+from seleniumbase import SB
+
+with SB(uc=True, test=True, locale_code="en") as sb:
+ url = "https://www.walmart.com/"
+ sb.activate_cdp_mode(url)
+ sb.sleep(2.5)
+ sb.cdp.mouse_click('input[aria-label="Search"]')
+ sb.sleep(1.2)
+ search = "Settlers of Catan Board Game"
+ required_text = "Catan"
+ sb.cdp.press_keys('input[aria-label="Search"]', search + "\n")
+ sb.sleep(3.8)
+ items = sb.cdp.find_elements('div[data-testid="list-view"]')
+ print('*** Walmart Search for "%s":' % search)
+ print(' (Results must contain "%s".)' % required_text)
+ for item in items:
+ if required_text in item.text:
+ description = item.querySelector(
+ '[data-automation-id="product-price"] + span'
+ )
+ if description:
+ print("* " + description.text)
+ price = item.querySelector(
+ '[data-automation-id="product-price"]'
+ )
+ if price:
+ price_text = price.text
+ price_text = price_text.split("current price Now ")[-1]
+ price_text = price_text.split("current price ")[-1]
+ price_text = price_text.split(" ")[0]
+ print(" (" + price_text + ")")
diff --git a/examples/raw_pixelscan.py b/examples/raw_pixelscan.py
index 03a140019f6..6c4559c4016 100644
--- a/examples/raw_pixelscan.py
+++ b/examples/raw_pixelscan.py
@@ -2,6 +2,7 @@
with SB(uc=True, incognito=True, test=True) as sb:
sb.driver.uc_open_with_reconnect("https://pixelscan.net/", 10)
+ sb.remove_elements("div.banner") # Remove the banner
sb.remove_elements("jdiv") # Remove chat widgets
no_automation_detected = "No automation framework detected"
sb.assert_text(no_automation_detected, "pxlscn-bot-detection")
diff --git a/help_docs/customizing_test_runs.md b/help_docs/customizing_test_runs.md
index 4eb991d641a..9045f9cc3d5 100644
--- a/help_docs/customizing_test_runs.md
+++ b/help_docs/customizing_test_runs.md
@@ -238,7 +238,7 @@ pytest test_suite.py
Demo Mode:
-🔵 If any test is moving too fast for your eyes to see what's going on, you can run it in **Demo Mode** by adding ``--demo`` on the command line, which pauses the browser briefly between actions, highlights page elements being acted on, and lets you know what test assertions are happening in real time:
+🔵 If any test is moving too fast for your eyes to see what's going on, you can run it in **Demo Mode** by adding ``--demo`` on the command line, which pauses the browser briefly between actions, highlights page elements being acted on, and lets you know what test assertions are happening in real-time:
```bash
pytest my_first_test.py --demo
@@ -335,7 +335,7 @@ class Test:
pytest --headless -n8 --dashboard --html=report.html -v --rs --crumbs
```
-The above not only runs tests in parallel processes, but it also tells tests in the same process to share the same browser session, runs the tests in headless mode, displays the full name of each test on a separate line, creates a realtime dashboard of the test results, and creates a full report after all tests complete.
+The above not only runs tests in parallel processes, but it also tells tests in the same process to share the same browser session, runs the tests in headless mode, displays the full name of each test on a separate line, creates a real-time dashboard of the test results, and creates a full report after all tests complete.
--------
diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt
index 3260df8b31b..771a9a67756 100644
--- a/mkdocs_build/requirements.txt
+++ b/mkdocs_build/requirements.txt
@@ -1,8 +1,8 @@
# mkdocs dependencies for generating the seleniumbase.io website
-# Minimum Python version: 3.8 (for generating docs only)
+# Minimum Python version: 3.9 (for generating docs only)
regex>=2024.9.11
-pymdown-extensions>=10.11.2
+pymdown-extensions>=10.12
pipdeptree>=2.23.4
python-dateutil>=2.8.2
Markdown==3.7
@@ -11,7 +11,7 @@ MarkupSafe==3.0.2
Jinja2==3.1.4
click==8.1.7
ghp-import==2.1.0
-watchdog==5.0.3
+watchdog==6.0.0
cairocffi==1.7.1
pathspec==0.12.1
Babel==2.16.0
@@ -20,7 +20,7 @@ lxml==5.3.0
pyquery==2.0.1
readtime==3.0.0
mkdocs==1.6.1
-mkdocs-material==9.5.42
+mkdocs-material==9.5.43
mkdocs-exclude-search==0.6.6
mkdocs-simple-hooks==0.1.5
mkdocs-material-extensions==1.3.1
diff --git a/requirements.txt b/requirements.txt
index ffc8921fa55..bc3ffee5f27 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -36,7 +36,7 @@ trio==0.27.0
trio-websocket==0.11.1
wsproto==1.2.0
websocket-client==1.8.0
-selenium==4.25.0
+selenium==4.26.1
cssselect==1.2.0
sortedcontainers==2.4.0
execnet==2.1.1
@@ -64,7 +64,8 @@ rich==13.9.3
coverage>=7.6.1;python_version<"3.9"
coverage>=7.6.4;python_version>="3.9"
-pytest-cov>=5.0.0
+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"
flake8==7.1.1;python_version>="3.9"
mccabe==0.7.0
diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py
index 059aca1510b..884e2a6ef40 100755
--- a/seleniumbase/__version__.py
+++ b/seleniumbase/__version__.py
@@ -1,2 +1,2 @@
# seleniumbase package
-__version__ = "4.32.5"
+__version__ = "4.32.6"
diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py
index 8acd0499e0d..1060016c7a0 100644
--- a/seleniumbase/core/browser_launcher.py
+++ b/seleniumbase/core/browser_launcher.py
@@ -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")
diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py
index 7686fd04d3f..f920764d9f7 100644
--- a/seleniumbase/core/sb_cdp.py
+++ b/seleniumbase/core/sb_cdp.py
@@ -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 @@ def select_all(self, selector, timeout=settings.SMALL_TIMEOUT):
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 @@ def set_attributes(self, selector, attribute, value):
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(" 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(" 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 @@ 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_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:
diff --git a/seleniumbase/undetected/cdp_driver/cdp_util.py b/seleniumbase/undetected/cdp_driver/cdp_util.py
index afa65b6b528..5eaa63de93f 100644
--- a/seleniumbase/undetected/cdp_driver/cdp_util.py
+++ b/seleniumbase/undetected/cdp_driver/cdp_util.py
@@ -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 @@
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,
diff --git a/setup.py b/setup.py
index 165e32079a9..b26122db586 100755
--- a/setup.py
+++ b/setup.py
@@ -185,7 +185,7 @@
'trio-websocket==0.11.1',
'wsproto==1.2.0',
'websocket-client==1.8.0',
- 'selenium==4.25.0',
+ 'selenium==4.26.1',
'cssselect==1.2.0',
"sortedcontainers==2.4.0",
'execnet==2.1.1',
@@ -222,7 +222,8 @@
"coverage": [
'coverage>=7.6.1;python_version<"3.9"',
'coverage>=7.6.4;python_version>="3.9"',
- 'pytest-cov>=5.0.0',
+ 'pytest-cov>=5.0.0;python_version<"3.9"',
+ 'pytest-cov>=6.0.0;python_version>="3.9"',
],
# pip install -e .[flake8]
# Usage: flake8