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)
[docs] def quit(self) -> None: """Clean up and close the driver.""" self.driver_impl.quit()
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 is_virtual_display_available(): return Display is not None
[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", )