diff --git a/py/conftest.py b/py/conftest.py index c18e8fcf5c0e5..fd1b28d6bebc9 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -17,6 +17,7 @@ import os import platform +from dataclasses import dataclass from pathlib import Path import pytest @@ -100,49 +101,231 @@ def pytest_generate_tests(metafunc): metafunc.parametrize("driver", metafunc.config.option.drivers, indirect=True) -def get_driver_class(driver_option): - """Generate the driver class name from the lowercase driver option.""" - if driver_option == "webkitgtk": - driver_class = "WebKitGTK" - elif driver_option == "wpewebkit": - driver_class = "WPEWebKit" - else: - driver_class = driver_option.capitalize() - return driver_class +driver_instance = None +selenium_driver = None + + +class ContainerProtocol: + def __contains__(self, name): + if name.lower() in self.__dict__: + return True + return False + + +@dataclass +class SupportedDrivers(ContainerProtocol): + chrome: str = "Chrome" + firefox: str = "Firefox" + safari: str = "Safari" + edge: str = "Edge" + ie: str = "Ie" + webkitgtk: str = "WebKitGTK" + wpewebkit: str = "WPEWebKit" + remote: str = "Remote" + + +@dataclass +class SupportedOptions(ContainerProtocol): + chrome: str = "ChromeOptions" + firefox: str = "FirefoxOptions" + edge: str = "EdgeOptions" + safari: str = "SafariOptions" + ie: str = "IeOptions" + remote: str = "FirefoxOptions" + webkitgtk: str = "WebKitGTKOptions" + wpewebkit: str = "WPEWebKitOptions" + + +@dataclass +class SupportedBidiDrivers(ContainerProtocol): + chrome: str = "Chrome" + firefox: str = "Firefox" + edge: str = "Edge" + remote: str = "Remote" + + +class Driver: + def __init__(self, driver_class, request): + self.driver_class = driver_class + self._request = request + self._driver = None + self._service = None + self.options = driver_class + self.headless = driver_class + self.bidi = driver_class + + @classmethod + def clean_options(cls, driver_class, request): + return cls(driver_class, request).options + + @property + def supported_drivers(self): + return SupportedDrivers() + + @property + def supported_options(self): + return SupportedOptions() + + @property + def supported_bidi_drivers(self): + return SupportedBidiDrivers() + + @property + def driver_class(self): + return self._driver_class + + @driver_class.setter + def driver_class(self, cls_name): + if cls_name.lower() not in self.supported_drivers: + raise AttributeError(f"Invalid driver class {cls_name.lower()}") + self._driver_class = getattr(self.supported_drivers, cls_name.lower()) + + @property + def exe_platform(self): + return platform.system() + + @property + def browser_path(self): + if self._request.config.option.binary: + return self._request.config.option.binary + return None + @property + def browser_args(self): + if self._request.config.option.args: + return self._request.config.option.args + return None -driver_instance = None + @property + def driver_path(self): + if self._request.config.option.executable: + return self._request.config.option.executable + return None + + @property + def headless(self): + return self._headless + + @headless.setter + def headless(self, cls_name): + self._headless = self._request.config.option.headless + if self._headless: + if cls_name.lower() == "chrome" or cls_name.lower() == "edge": + self._options.add_argument("--headless") + if cls_name.lower() == "firefox": + self._options.add_argument("-headless") + + @property + def bidi(self): + return self._bidi + + @bidi.setter + def bidi(self, cls_name): + self._bidi = self._request.config.option.bidi + if self._bidi: + self._options.web_socket_url = True + self._options.unhandled_prompt_behavior = "ignore" + + @property + def options(self): + return self._options + + @options.setter + def options(self, cls_name): + if cls_name.lower() not in self.supported_options: + raise AttributeError(f"Invalid Options class {cls_name.lower()}") + + if self.driver_class == self.supported_drivers.firefox: + self._options = getattr(webdriver, self.supported_options.firefox)() + if self.exe_platform == "Linux": + # There are issues with window size/position when running Firefox + # under Wayland, so we use XWayland instead. + os.environ["MOZ_ENABLE_WAYLAND"] = "0" + elif self.driver_class == self.supported_drivers.remote: + self._options = getattr(webdriver, self.supported_options.firefox)() + self._options.set_capability("moz:firefoxOptions", {}) + self._options.enable_downloads = True + else: + opts_cls = getattr(self.supported_options, cls_name.lower()) + self._options = getattr(webdriver, opts_cls)() + + if self.browser_path or self.browser_args: + if self.driver_class == self.supported_drivers.webkitgtk: + self._options.overlay_scrollbars_enabled = False + if self.browser_path is not None: + self._options.binary_location = self.browser_path.strip("'") + if self.browser_args is not None: + for arg in self.browser_args.split(): + self._options.add_argument(arg) + + @property + def service(self): + executable = self.driver_path + if executable: + module = getattr(webdriver, self.driver_class.lower()) + self._service = module.service.Service(executable_path=executable) + return self._service + return None + + @property + def driver(self): + self._driver = self._initialize_driver() + return self._driver + + @property + def is_platform_valid(self): + if self.driver_class.lower() == "safari" and self.exe_platform != "Darwin": + return False + if self.driver_class.lower() == "ie" and self.exe_platform != "Windows": + return False + if "webkit" in self.driver_class.lower() and self.exe_platform == "Windows": + return False + return True + + def _initialize_driver(self): + kwargs = {} + if self.options is not None: + kwargs["options"] = self.options + if self.driver_path is not None: + kwargs["service"] = self.service + return getattr(webdriver, self.driver_class)(**kwargs) + + @property + def stop_driver(self): + def fin(): + global driver_instance + if self._driver is not None: + self._driver.quit() + self._driver = None + driver_instance = None + + return fin @pytest.fixture(scope="function") def driver(request): - kwargs = {} - driver_option = getattr(request, "param", "Chrome") + global driver_instance + global selenium_driver + driver_class = getattr(request, "param", "Chrome").lower() + + if selenium_driver is None: + selenium_driver = Driver(driver_class, request) - # browser can be changed with `--driver=firefox` as an argument or to addopts in pytest.ini - driver_class = get_driver_class(driver_option) + # skip tests if not available on the platform + if not selenium_driver.is_platform_valid: + pytest.skip(f"{driver_class} tests can only run on {selenium_driver.exe_platform}") # skip tests in the 'remote' directory if run with a local driver - if request.node.path.parts[-2] == "remote" and driver_class != "Remote": - pytest.skip(f"Remote tests can't be run with driver '{driver_option.lower()}'") - - # skip tests that can't run on certain platforms - _platform = platform.system() - if driver_class == "Safari" and _platform != "Darwin": - pytest.skip("Safari tests can only run on an Apple OS") - if (driver_class == "Ie") and _platform != "Windows": - pytest.skip("IE and EdgeHTML Tests can only run on Windows") - if "WebKit" in driver_class and _platform == "Windows": - pytest.skip("WebKit tests cannot be run on Windows") + if request.node.path.parts[-2] == "remote" and selenium_driver.driver_class != "Remote": + pytest.skip(f"Remote tests can't be run with driver '{selenium_driver.driver_class}'") # skip tests for drivers that don't support BiDi when --bidi is enabled - if request.config.option.bidi: - if driver_class in ("Ie", "Safari", "WebKitGTK", "WPEWebKit"): + if selenium_driver.bidi: + if driver_class.lower() not in selenium_driver.supported_bidi_drivers: pytest.skip(f"{driver_class} does not support BiDi") # conditionally mark tests as expected to fail based on driver marker = request.node.get_closest_marker(f"xfail_{driver_class.lower()}") - if marker is not None: if "run" in marker.kwargs: if marker.kwargs["run"] is False: @@ -153,104 +336,23 @@ def driver(request): marker.kwargs.pop("raises") pytest.xfail(**marker.kwargs) - def fin(): - global driver_instance - if driver_instance is not None: - driver_instance.quit() - driver_instance = None - - request.addfinalizer(fin) - - driver_path = request.config.option.executable - options = None + request.addfinalizer(selenium_driver.stop_driver) - global driver_instance if driver_instance is None: - if driver_class == "Firefox": - options = get_options(driver_class, request.config) - if platform.system() == "Linux": - # There are issues with window size/position when running Firefox - # under Wayland, so we use XWayland instead. - os.environ["MOZ_ENABLE_WAYLAND"] = "0" - if driver_class == "Chrome": - options = get_options(driver_class, request.config) - if driver_class == "Edge": - options = get_options(driver_class, request.config) - if driver_class == "WebKitGTK": - options = get_options(driver_class, request.config) - if driver_class == "WPEWebKit": - options = get_options(driver_class, request.config) - if driver_class == "Remote": - options = get_options("Firefox", request.config) or webdriver.FirefoxOptions() - options.set_capability("moz:firefoxOptions", {}) - options.enable_downloads = True - if driver_path is not None: - kwargs["service"] = get_service(driver_class, driver_path) - if options is not None: - kwargs["options"] = options - - driver_instance = getattr(webdriver, driver_class)(**kwargs) + driver_instance = selenium_driver.driver yield driver_instance # Close the browser after BiDi tests. Those make event subscriptions # and doesn't seems to be stable enough, causing the flakiness of the # subsequent tests. # Remove this when BiDi implementation and API is stable. - if request.config.option.bidi: - - def fin(): - global driver_instance - if driver_instance is not None: - driver_instance.quit() - driver_instance = None - - request.addfinalizer(fin) + if selenium_driver.bidi: + request.addfinalizer(selenium_driver.stop_driver) if request.node.get_closest_marker("no_driver_after_test"): driver_instance = None -def get_options(driver_class, config): - browser_path = config.option.binary - browser_args = config.option.args - headless = config.option.headless - bidi = config.option.bidi - - options = getattr(webdriver, f"{driver_class}Options")() - - if browser_path or browser_args: - if driver_class == "WebKitGTK": - options.overlay_scrollbars_enabled = False - if browser_path is not None: - options.binary_location = browser_path.strip("'") - if browser_args is not None: - for arg in browser_args.split(): - options.add_argument(arg) - - if headless: - if driver_class == "Chrome" or driver_class == "Edge": - options.add_argument("--headless") - if driver_class == "Firefox": - options.add_argument("-headless") - - if bidi: - options.web_socket_url = True - options.unhandled_prompt_behavior = "ignore" - - return options - - -def get_service(driver_class, executable): - # Let the default behaviour be used if we don't set the driver executable - if not executable: - return None - - module = getattr(webdriver, driver_class.lower()) - service = module.service.Service(executable_path=executable) - - return service - - @pytest.fixture(scope="session", autouse=True) def stop_driver(request): def fin(): @@ -335,10 +437,11 @@ def driver_executable(request): @pytest.fixture(scope="function") def clean_driver(request): + _supported_drivers = SupportedDrivers() try: - driver_class = get_driver_class(request.config.option.drivers[0]) + driver_class = getattr(_supported_drivers, request.config.option.drivers[0].lower()) except (AttributeError, TypeError): - raise Exception("This test requires a --driver to be specified") + raise Exception("This test requires a --driver to be specified.") driver_reference = getattr(webdriver, driver_class) yield driver_reference if request.node.get_closest_marker("no_driver_after_test"): @@ -347,52 +450,51 @@ def clean_driver(request): @pytest.fixture(scope="function") def clean_service(request): - driver_class = get_driver_class(request.config.option.drivers[0]) - yield get_service(driver_class, request.config.option.executable) + driver_class = request.config.option.drivers[0].lower() + selenium_driver = Driver(driver_class, request) + yield selenium_driver.service @pytest.fixture(scope="function") def clean_options(request): - driver_class = get_driver_class(request.config.option.drivers[0]) - yield get_options(driver_class, request.config) + driver_class = request.config.option.drivers[0].lower() + yield Driver.clean_options(driver_class, request) @pytest.fixture def firefox_options(request): + _supported_drivers = SupportedDrivers() try: - driver_option = request.config.option.drivers[0] + driver_class = request.config.option.drivers[0].lower() except (AttributeError, TypeError): raise Exception("This test requires a --driver to be specified") + # skip tests in the 'remote' directory if run with a local driver - if request.node.path.parts[-2] == "remote" and get_driver_class(driver_option) != "Remote": - pytest.skip(f"Remote tests can't be run with driver '{driver_option}'") - options = webdriver.FirefoxOptions() - if request.config.option.headless: - options.add_argument("-headless") + if request.node.path.parts[-2] == "remote" and getattr(_supported_drivers, driver_class) != "Remote": + pytest.skip(f"Remote tests can't be run with driver '{driver_class}'") + + options = Driver.clean_options("firefox", request) + return options @pytest.fixture def chromium_options(request): + _supported_drivers = SupportedDrivers() try: - driver_option = request.config.option.drivers[0].lower() + driver_class = request.config.option.drivers[0].lower() except (AttributeError, TypeError): raise Exception("This test requires a --driver to be specified") # Skip if not Chrome or Edge - if driver_option not in ("chrome", "edge"): - pytest.skip(f"This test requires Chrome or Edge, got {driver_option}") + if driver_class not in ("chrome", "edge"): + pytest.skip(f"This test requires Chrome or Edge, got {driver_class}") # skip tests in the 'remote' directory if run with a local driver - if request.node.path.parts[-2] == "remote" and get_driver_class(driver_option) != "Remote": - pytest.skip(f"Remote tests can't be run with driver '{driver_option}'") - - if driver_option == "chrome": - options = webdriver.ChromeOptions() - elif driver_option == "edge": - options = webdriver.EdgeOptions() + if request.node.path.parts[-2] == "remote" and getattr(_supported_drivers, driver_class) != "Remote": + pytest.skip(f"Remote tests can't be run with driver '{driver_class}'") - if request.config.option.headless: - options.add_argument("--headless") + if driver_class in ("chrome", "edge"): + options = Driver.clean_options(driver_class, request) return options