diff --git a/py/conftest.py b/py/conftest.py index 45e5c704f8595..e508d44469ce7 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -370,3 +370,29 @@ def firefox_options(request): if request.config.option.headless: options.add_argument("-headless") return options + + +@pytest.fixture +def chromium_options(request): + try: + driver_option = 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}") + + # 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.config.option.headless: + options.add_argument("--headless=new") + + return options diff --git a/py/selenium/webdriver/chromium/options.py b/py/selenium/webdriver/chromium/options.py index 1a83cd42b4c77..3203f9b5fb6c9 100644 --- a/py/selenium/webdriver/chromium/options.py +++ b/py/selenium/webdriver/chromium/options.py @@ -33,6 +33,7 @@ def __init__(self) -> None: self._extensions: list[str] = [] self._experimental_options: dict[str, Union[str, int, dict, list[str]]] = {} self._debugger_address: Optional[str] = None + self._enable_webextensions: bool = False @property def binary_location(self) -> str: @@ -126,6 +127,39 @@ def add_experimental_option(self, name: str, value: Union[str, int, dict, list[s """ self._experimental_options[name] = value + @property + def enable_webextensions(self) -> bool: + """Returns whether webextension support is enabled for Chromium-based browsers. + + :Returns: True if webextension support is enabled, False otherwise. + """ + return self._enable_webextensions + + @enable_webextensions.setter + def enable_webextensions(self, value: bool) -> None: + """Enables or disables webextension support for Chromium-based browsers. + + When enabled, this automatically adds the required Chromium flags: + - --enable-unsafe-extension-debugging + - --remote-debugging-pipe + + :Args: + - value: True to enable webextension support, False to disable. + """ + self._enable_webextensions = value + if value: + # Add required flags for Chromium webextension support + required_flags = ["--enable-unsafe-extension-debugging", "--remote-debugging-pipe"] + for flag in required_flags: + if flag not in self._arguments: + self.add_argument(flag) + else: + # Remove webextension flags if disabling + flags_to_remove = ["--enable-unsafe-extension-debugging", "--remote-debugging-pipe"] + for flag in flags_to_remove: + if flag in self._arguments: + self._arguments.remove(flag) + def to_capabilities(self) -> dict: """Creates a capabilities with all the options that have been set :Returns: A dictionary with everything.""" diff --git a/py/selenium/webdriver/common/bidi/webextension.py b/py/selenium/webdriver/common/bidi/webextension.py index 2b43a273f21c1..d91a89cfce2c2 100644 --- a/py/selenium/webdriver/common/bidi/webextension.py +++ b/py/selenium/webdriver/common/bidi/webextension.py @@ -17,6 +17,7 @@ from typing import Union +from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.bidi.common import command_builder @@ -54,8 +55,17 @@ def install(self, path=None, archive_path=None, base64_value=None) -> dict: extension_data = {"type": "base64", "value": base64_value} params = {"extensionData": extension_data} - result = self.conn.execute(command_builder("webExtension.install", params)) - return result + + try: + result = self.conn.execute(command_builder("webExtension.install", params)) + return result + except WebDriverException as e: + if "Method not available" in str(e): + raise WebDriverException( + f"{str(e)}. If you are using Chrome or Edge, add '--enable-unsafe-extension-debugging' " + "and '--remote-debugging-pipe' arguments or set options.enable_webextensions = True" + ) from e + raise def uninstall(self, extension_id_or_result: Union[str, dict]) -> None: """Uninstalls a web extension from the remote end. diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index ac186cf89e980..3ea6745730bc9 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -17,10 +17,13 @@ import base64 import os +import shutil +import tempfile import pytest from python.runfiles import Runfiles +from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait @@ -62,62 +65,113 @@ def test_webextension_initialized(driver): @pytest.mark.xfail_chrome @pytest.mark.xfail_edge -def test_install_extension_path(driver, pages): - """Test installing an extension from a directory path.""" - path = os.path.join(extensions, EXTENSION_PATH) +class TestFirefoxWebExtension: + """Firefox-specific WebExtension tests.""" - ext_info = install_extension(driver, path=path) - verify_extension_injection(driver, pages) - uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + def test_install_extension_path(self, driver, pages): + """Test installing an extension from a directory path.""" - -@pytest.mark.xfail_chrome -@pytest.mark.xfail_edge -def test_install_archive_extension_path(driver, pages): - """Test installing an extension from an archive path.""" - path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH) - - ext_info = install_extension(driver, archive_path=path) - verify_extension_injection(driver, pages) - uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) - - -@pytest.mark.xfail_chrome -@pytest.mark.xfail_edge -def test_install_base64_extension_path(driver, pages): - """Test installing an extension from a base64 encoded string.""" - path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH) - - with open(path, "rb") as file: - base64_encoded = base64.b64encode(file.read()).decode("utf-8") - - ext_info = install_extension(driver, base64_value=base64_encoded) - - # TODO: the extension is installed but the script is not injected, check and fix - # verify_extension_injection(driver, pages) - - uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) - - -@pytest.mark.xfail_chrome -@pytest.mark.xfail_edge -def test_install_unsigned_extension(driver, pages): - """Test installing an unsigned extension.""" - path = os.path.join(extensions, "webextensions-selenium-example") - - ext_info = install_extension(driver, path=path) - verify_extension_injection(driver, pages) - uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) - - -@pytest.mark.xfail_chrome -@pytest.mark.xfail_edge -def test_install_with_extension_id_uninstall(driver, pages): - """Test uninstalling an extension using just the extension ID.""" - path = os.path.join(extensions, EXTENSION_PATH) - - ext_info = install_extension(driver, path=path) - extension_id = ext_info.get("extension") - - # Uninstall using the extension ID - uninstall_extension_and_verify_extension_uninstalled(driver, extension_id) + path = os.path.join(extensions, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + verify_extension_injection(driver, pages) + uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + + def test_install_archive_extension_path(self, driver, pages): + """Test installing an extension from an archive path.""" + + path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH) + ext_info = install_extension(driver, archive_path=path) + verify_extension_injection(driver, pages) + uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + + def test_install_base64_extension_path(self, driver, pages): + """Test installing an extension from a base64 encoded string.""" + + path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH) + with open(path, "rb") as file: + base64_encoded = base64.b64encode(file.read()).decode("utf-8") + ext_info = install_extension(driver, base64_value=base64_encoded) + # TODO: the extension is installed but the script is not injected, check and fix + # verify_extension_injection(driver, pages) + uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + + def test_install_unsigned_extension(self, driver, pages): + """Test installing an unsigned extension.""" + + path = os.path.join(extensions, "webextensions-selenium-example") + ext_info = install_extension(driver, path=path) + verify_extension_injection(driver, pages) + uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + + def test_install_with_extension_id_uninstall(self, driver, pages): + """Test uninstalling an extension using just the extension ID.""" + + path = os.path.join(extensions, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + extension_id = ext_info.get("extension") + # Uninstall using the extension ID + uninstall_extension_and_verify_extension_uninstalled(driver, extension_id) + + +@pytest.mark.xfail_firefox +class TestChromiumWebExtension: + """Chrome/Edge-specific WebExtension tests with custom driver.""" + + @pytest.fixture + def pages_chromium(self, webserver, chromium_driver): + class Pages: + def load(self, name): + chromium_driver.get(webserver.where_is(name, localhost=False)) + + return Pages() + + @pytest.fixture + def chromium_driver(self, chromium_options, request): + """Create a Chrome/Edge driver with webextension support enabled.""" + driver_option = request.config.option.drivers[0].lower() + + if driver_option == "chrome": + browser_class = webdriver.Chrome + elif driver_option == "edge": + browser_class = webdriver.Edge + + temp_dir = tempfile.mkdtemp(prefix="chrome-profile-") + + chromium_options.enable_bidi = True + chromium_options.enable_webextensions = True + chromium_options.add_argument(f"--user-data-dir={temp_dir}") + chromium_options.add_argument("--no-sandbox") + chromium_options.add_argument("--disable-dev-shm-usage") + + chromium_driver = browser_class(options=chromium_options) + + yield chromium_driver + chromium_driver.quit() + + # delete the temp directory + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + def test_install_extension_path(self, chromium_driver, pages_chromium): + """Test installing an extension from a directory path.""" + path = os.path.join(extensions, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + + verify_extension_injection(chromium_driver, pages_chromium) + uninstall_extension_and_verify_extension_uninstalled(chromium_driver, ext_info) + + def test_install_unsigned_extension(self, chromium_driver, pages_chromium): + """Test installing an unsigned extension.""" + path = os.path.join(extensions, "webextensions-selenium-example") + ext_info = chromium_driver.webextension.install(path=path) + + verify_extension_injection(chromium_driver, pages_chromium) + uninstall_extension_and_verify_extension_uninstalled(chromium_driver, ext_info) + + def test_install_with_extension_id_uninstall(self, chromium_driver): + """Test uninstalling an extension using just the extension ID.""" + path = os.path.join(extensions, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + extension_id = ext_info.get("extension") + # Uninstall using the extension ID + uninstall_extension_and_verify_extension_uninstalled(chromium_driver, extension_id)