Warning
This document is for an old release of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.
Source code for galaxy.selenium.driver_factory
import logging
import os
from typing import (
Any,
cast,
Literal,
Union,
)
# Playwright browser type names (matches BrowserType.name property)
PlaywrightBrowserName = Literal["chromium", "firefox", "webkit"]
try:
from pyvirtualdisplay import Display
except ImportError:
Display = None
try:
from playwright.sync_api import sync_playwright
except ImportError:
sync_playwright = None # type: ignore[assignment]
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.safari.options import Options as SafariOptions
from .has_driver import HasDriver
from .has_driver_protocol import (
HasDriverProtocol,
TimeoutCallback,
)
from .has_playwright_driver import (
HasPlaywrightDriver,
PlaywrightResources,
)
logger = logging.getLogger("selenium.webdriver.remote.remote_connection")
logger.setLevel(logging.WARNING)
DEFAULT_BROWSER = "auto"
DEFAULT_DOWNLOAD_PATH = "/tmp/"
LOGGING_PREFS = {
"browser": "ALL",
}
DEFAULT_SELENIUM_BROWSER = "auto"
DEFAULT_SELENIUM_REMOTE = False
DEFAULT_SELENIUM_REMOTE_PORT = "4444"
DEFAULT_SELENIUM_REMOTE_HOST = "127.0.0.1"
DEFAULT_BACKEND_TYPE: Literal["selenium", "playwright"] = "selenium"
DEFAULT_WINDOW_WIDTH = 1280
DEFAULT_WINDOW_HEIGHT = 1000
VALID_LOCAL_BROWSERS = ["CHROME", "FIREFOX"]
PLAYWRIGHT_UNAVAILABLE_MESSAGE = "playwright must be installed to use the playwright backend, install with 'pip install playwright' and run 'playwright install chromium'"
PYVIRTUALDISPLAY_UNAVAILABLE_MESSAGE = "pyvirtualdisplay must be installed to run this test configuration and is not, install with 'pip install pyvirtualdisplay'"
PLAYWRIGHT_REMOTE_UNSUPPORTED_MESSAGE = "Playwright backend does not support remote drivers (Selenium Grid). Use backend_type='selenium' for remote testing."
class _SeleniumDriverImpl(HasDriver[Any]):
"""Internal wrapper implementing HasDriver for ConfiguredDriver."""
def __init__(self, driver: WebDriver, timeout_handler: TimeoutCallback):
self.driver = driver
self._timeout_handler = timeout_handler
@property
def timeout_handler(self) -> TimeoutCallback:
"""Return timeout value, using kwds override or default."""
return self._timeout_handler
class _PlaywrightDriverImpl(HasPlaywrightDriver[Any]):
"""Internal wrapper implementing HasPlaywrightDriver for ConfiguredDriver."""
def __init__(self, resources: PlaywrightResources, timeout_handler: TimeoutCallback):
self._playwright_resources = resources
self._timeout_handler = timeout_handler
self._current_frame = None
@property
def timeout_handler(self) -> TimeoutCallback:
"""Return timeout value, using kwds override or default."""
return self._timeout_handler
[docs]
class ConfiguredDriver:
"""
Configured driver factory supporting both Selenium and Playwright backends.
This class creates and manages either a Selenium WebDriver or Playwright driver
based on the backend_type parameter. It provides a unified interface via
HasDriverProtocol for both implementations.
Attributes:
driver_impl: The HasDriverProtocol implementation (HasDriver or HasPlaywrightDriver)
config: Configuration dictionary for serialization
backend_type: Which backend is being used ("selenium" or "playwright")
"""
driver_impl: HasDriverProtocol[Any]
config: dict[str, Any]
backend_type: Literal["selenium", "playwright"]
[docs]
def __init__(
self,
timeout_handler: TimeoutCallback,
browser: str = DEFAULT_SELENIUM_BROWSER,
remote: bool = DEFAULT_SELENIUM_REMOTE,
remote_host: str = DEFAULT_SELENIUM_REMOTE_HOST,
remote_port: str = DEFAULT_SELENIUM_REMOTE_PORT,
headless: bool = False,
backend_type: Literal["selenium", "playwright"] = DEFAULT_BACKEND_TYPE,
):
"""
Initialize a configured driver with the specified backend.
Args:
timeout_handler: Callback function that returns timeout value for wait operations
browser: Browser name ("auto", "CHROME", "FIREFOX", etc.)
remote: Whether to use remote Selenium Grid (Selenium only)
remote_host: Remote Selenium Grid host
remote_port: Remote Selenium Grid port
headless: Whether to run browser in headless mode
backend_type: Which backend to use ("selenium" or "playwright")
Raises:
Exception: If Playwright backend is requested with remote=True
Exception: If Playwright is not installed when backend_type="playwright"
"""
self.backend_type = backend_type
self.config = dict(
browser=browser,
remote=remote,
remote_host=remote_host,
remote_port=remote_port,
headless=headless,
backend_type=backend_type,
)
# Validate Playwright limitations
if backend_type == "playwright" and remote:
raise Exception(PLAYWRIGHT_REMOTE_UNSUPPORTED_MESSAGE)
if backend_type == "selenium":
# Create Selenium driver
if remote:
driver = get_remote_driver(
host=remote_host,
port=remote_port,
browser=browser,
)
else:
driver = get_local_driver(
browser=browser,
headless=headless,
)
# Need larger than default window size for workflow index page, paired list creator.
driver.set_window_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
self.driver_impl = cast(HasDriverProtocol, _SeleniumDriverImpl(driver, timeout_handler))
elif backend_type == "playwright":
# Create Playwright driver
resources = get_playwright_driver(
browser=browser,
headless=headless,
)
self.driver_impl = cast(HasDriverProtocol, _PlaywrightDriverImpl(resources, timeout_handler))
else:
raise ValueError(f"Invalid backend_type: {backend_type}. Must be 'selenium' or 'playwright'")
[docs]
def to_dict(self) -> dict[str, Any]:
"""Serialize configuration to dictionary."""
return self.config.copy()
[docs]
@staticmethod
def from_dict(timeout_handler: TimeoutCallback, as_dict: dict[str, Any]) -> "ConfiguredDriver":
"""Deserialize configuration from dictionary."""
return ConfiguredDriver(timeout_handler, **as_dict)
def get_local_browser(browser):
if browser == "auto":
if _which("chromedriver"):
browser = "CHROME"
elif _which("geckodriver"):
browser = "FIREFOX"
else:
raise Exception("Selenium browser is 'auto' but neither geckodriver or chromedriver are found on PATH.")
return browser
[docs]
def get_local_driver(browser=DEFAULT_BROWSER, headless=False) -> WebDriver:
browser = get_local_browser(browser)
if browser not in VALID_LOCAL_BROWSERS:
raise AssertionError(f"{browser} not in VALID_LOCAL_BROWSERS ({VALID_LOCAL_BROWSERS})")
driver_to_class = {
"CHROME": webdriver.Chrome,
"FIREFOX": webdriver.Firefox,
}
driver_class = driver_to_class[browser]
if browser == "CHROME":
chrome_options = ChromeOptions()
if headless:
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
prefs = {"download.default_directory": DEFAULT_DOWNLOAD_PATH}
chrome_options.add_experimental_option("prefs", prefs)
chrome_options.set_capability("goog:loggingPrefs", LOGGING_PREFS)
return driver_class(options=chrome_options)
else:
firefox_options = webdriver.FirefoxOptions()
firefox_options.set_preference("network.proxy.type", 2)
firefox_options.set_preference("network.proxy.autoconfig_url", "http://127.0.0.1:9675")
firefox_options.set_preference("browser.download.folderList", 2)
firefox_options.set_preference("browser.download.dir", DEFAULT_DOWNLOAD_PATH)
firefox_options.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream")
return driver_class(options=firefox_options)
[docs]
def get_playwright_browser_type(browser: str = DEFAULT_BROWSER) -> PlaywrightBrowserName:
"""
Map Selenium browser names to Playwright browser types.
Args:
browser: Browser name (CHROME, FIREFOX, auto, etc.)
Returns:
Playwright browser type: 'chromium', 'firefox', or 'webkit'
This matches the BrowserType.name property values.
Raises:
NotImplementedError: If browser type has no direct Playwright mapping
"""
if browser == "auto" or browser == "CHROME":
return "chromium"
elif browser == "FIREFOX":
return "firefox"
elif browser == "EDGE":
raise NotImplementedError(
f"Browser '{browser}' does not have a direct Playwright mapping. "
"EDGE uses Chromium under the hood - use 'CHROME' or 'auto' instead."
)
elif browser == "SAFARI":
raise NotImplementedError(
f"Browser '{browser}' does not have a direct Playwright mapping. "
"Safari uses WebKit - however, Playwright's 'webkit' is not identical to Safari. "
"If you need Safari-specific testing, use the Selenium backend instead."
)
else:
# Unknown browser type
raise NotImplementedError(
f"Browser '{browser}' is not supported with Playwright backend. "
f"Supported browsers: CHROME, FIREFOX, auto. Use backend_type='selenium' for other browsers."
)
def get_playwright_driver(browser: str = DEFAULT_BROWSER, headless: bool = False) -> PlaywrightResources:
"""
Create Playwright browser resources.
Args:
browser: Browser name to launch (CHROME, FIREFOX, auto, etc.)
headless: Whether to run in headless mode
Returns:
PlaywrightResources containing playwright, browser, and page instances
Raises:
Exception: If playwright is not installed
"""
if sync_playwright is None:
raise Exception(PLAYWRIGHT_UNAVAILABLE_MESSAGE)
browser_type_name = get_playwright_browser_type(browser)
playwright = sync_playwright().start()
# Get the appropriate browser type
if browser_type_name == "firefox":
browser_type = playwright.firefox
elif browser_type_name == "webkit":
browser_type = playwright.webkit
else:
browser_type = playwright.chromium
# Launch browser with headless setting
browser_instance = browser_type.launch(headless=headless)
# Create page with viewport size matching Selenium's window size
page = browser_instance.new_page(viewport={"width": DEFAULT_WINDOW_WIDTH, "height": DEFAULT_WINDOW_HEIGHT})
return PlaywrightResources(
playwright=playwright,
browser=browser_instance,
page=page,
)
[docs]
def get_remote_driver(host, port, browser=DEFAULT_BROWSER) -> WebDriver:
# docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.0.1-aluminum
options: Union[webdriver.ChromeOptions, webdriver.FirefoxOptions, webdriver.EdgeOptions, SafariOptions]
if browser == "auto" or browser == "CHROME":
options = webdriver.ChromeOptions()
options.set_capability("goog:loggingPrefs", LOGGING_PREFS)
elif browser == "FIREFOX":
options = webdriver.FirefoxOptions()
elif browser == "EDGE":
options = webdriver.EdgeOptions()
elif browser == "SAFARI":
options = SafariOptions()
else:
raise Exception(f"Browser '{browser}' not supported.")
executor = f"http://{host}:{port}/wd/hub"
driver = webdriver.Remote(
command_executor=executor,
options=options,
)
return driver
[docs]
def virtual_display_if_enabled(enabled):
if enabled:
if Display is None:
raise Exception(PYVIRTUALDISPLAY_UNAVAILABLE_MESSAGE)
display = Display(visible=0, size=(800, 600))
display.start()
return display
else:
return NoopDisplay()
class NoopDisplay:
def stop(self):
"""No-op stop for consistent use with pyvirtualdisplay Display class."""
# Purposely copied from galaxy.util - just use galaxy.util if we decide
# galaxy_selenium should definitely depend on Galaxy/galaxy-lib.
def _which(file):
# http://stackoverflow.com/questions/5226958/which-equivalent-function-in-python
for path in os.environ["PATH"].split(":"):
if os.path.exists(f"{path}/{file}"):
return f"{path}/{file}"
return None
__all__ = (
"ConfiguredDriver",
"get_local_driver",
"get_remote_driver",
"get_playwright_browser_type",
"is_virtual_display_available",
"virtual_display_if_enabled",
"PlaywrightBrowserName",
)