From 9d5c3442368b063d9fc03d5142353fe893a3e8ba Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sun, 25 May 2025 10:37:29 +0530 Subject: [PATCH 01/15] add `enable_webextensions` method and exception for chrome webextensions --- py/selenium/webdriver/chrome/options.py | 37 +++++++++++++++++++ .../webdriver/common/bidi/webextension.py | 14 ++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/chrome/options.py b/py/selenium/webdriver/chrome/options.py index c03651075d170..a0fe2cc33e858 100644 --- a/py/selenium/webdriver/chrome/options.py +++ b/py/selenium/webdriver/chrome/options.py @@ -22,10 +22,47 @@ class Options(ChromiumOptions): + def __init__(self) -> None: + super().__init__() + self._enable_webextensions = False + @property def default_capabilities(self) -> dict: return DesiredCapabilities.CHROME.copy() + @property + def enable_webextensions(self) -> bool: + """Returns whether webextension support is enabled for Chrome. + + :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 Chrome. + + When enabled, this automatically adds the required Chrome 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 Chrome 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 enable_mobile( self, android_package: Optional[str] = "com.android.chrome", diff --git a/py/selenium/webdriver/common/bidi/webextension.py b/py/selenium/webdriver/common/bidi/webextension.py index 2e42de34c3ce5..5db0d758bcc7d 100644 --- a/py/selenium/webdriver/common/bidi/webextension.py +++ b/py/selenium/webdriver/common/bidi/webextension.py @@ -18,6 +18,7 @@ from typing import Dict, Union from selenium.webdriver.common.bidi.common import command_builder +from selenium.common.exceptions import WebDriverException class WebExtension: @@ -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, 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. From c88cc84b1b6a54c8530721b0f410321e774d4c2a Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sun, 25 May 2025 10:39:07 +0530 Subject: [PATCH 02/15] add chrome test for webextension from path --- py/conftest.py | 4 +++ .../common/bidi_webextension_tests.py | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 45e5c704f8595..4063ed87b199d 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -237,6 +237,10 @@ def get_options(driver_class, config): options.web_socket_url = True options.unhandled_prompt_behavior = "ignore" + # Enable webextensions for Chrome when BiDi is enabled + if driver_class == "Chrome" and hasattr(options, "enable_webextensions"): + options.enable_webextensions = True + return options diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index b487eb370336f..85620ed39844f 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -60,13 +60,36 @@ def test_webextension_initialized(driver): assert driver.webextension is not None -@pytest.mark.xfail_chrome @pytest.mark.xfail_edge def test_install_extension_path(driver, pages): - """Test installing an extension from a directory path.""" + """Test installing an extension from a directory path. + + Note: For Chrome, webextensions are enabled when BiDi is used from conftest.py for this test. + You can also manually enable them using: + + from selenium.webdriver.chrome.options import Options + options = Options() + options.enable_webextensions = True + driver = webdriver.Chrome(options=options) + + Or directly pass the required flags when creating the driver: + + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + + options = Options() + options.add_argument("--remote-debugging-pipe") + options.add_argument("--enable-unsafe-extension-debugging") + + driver = webdriver.Chrome(options=options) + """ path = os.path.join(extensions, EXTENSION_PATH) - ext_info = install_extension(driver, path=path) + if driver.capabilities["browserName"].lower() == "chrome": + # chrome does not uses extension id from manifest.json so we cannot assert the id + ext_info = driver.webextension.install(path=path) + else: + ext_info = install_extension(driver, path=path) verify_extension_injection(driver, pages) uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) From 9ee46c7af19bb6be9098dbab84c34c5b24020e12 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sun, 25 May 2025 11:27:14 +0530 Subject: [PATCH 03/15] move to `chromium/options.py` to support Edge --- py/conftest.py | 4 +- py/selenium/webdriver/chrome/options.py | 37 ------------------- py/selenium/webdriver/chromium/options.py | 34 +++++++++++++++++ .../webdriver/common/bidi/webextension.py | 2 +- .../common/bidi_webextension_tests.py | 7 ++-- 5 files changed, 40 insertions(+), 44 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 4063ed87b199d..db5fe356a0a8d 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -237,8 +237,8 @@ def get_options(driver_class, config): options.web_socket_url = True options.unhandled_prompt_behavior = "ignore" - # Enable webextensions for Chrome when BiDi is enabled - if driver_class == "Chrome" and hasattr(options, "enable_webextensions"): + # Enable webextensions for Chromium-based browsers when BiDi is enabled + if driver_class in ("Chrome", "Edge") and hasattr(options, "enable_webextensions"): options.enable_webextensions = True return options diff --git a/py/selenium/webdriver/chrome/options.py b/py/selenium/webdriver/chrome/options.py index a0fe2cc33e858..c03651075d170 100644 --- a/py/selenium/webdriver/chrome/options.py +++ b/py/selenium/webdriver/chrome/options.py @@ -22,47 +22,10 @@ class Options(ChromiumOptions): - def __init__(self) -> None: - super().__init__() - self._enable_webextensions = False - @property def default_capabilities(self) -> dict: return DesiredCapabilities.CHROME.copy() - @property - def enable_webextensions(self) -> bool: - """Returns whether webextension support is enabled for Chrome. - - :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 Chrome. - - When enabled, this automatically adds the required Chrome 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 Chrome 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 enable_mobile( self, android_package: Optional[str] = "com.android.chrome", diff --git a/py/selenium/webdriver/chromium/options.py b/py/selenium/webdriver/chromium/options.py index a2f55ec1a5534..4b145ac99a053 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 5db0d758bcc7d..b820ff4a0d082 100644 --- a/py/selenium/webdriver/common/bidi/webextension.py +++ b/py/selenium/webdriver/common/bidi/webextension.py @@ -62,7 +62,7 @@ def install(self, path=None, archive_path=None, base64_value=None) -> Dict: except WebDriverException as e: if "Method not available" in str(e): raise WebDriverException( - f"{str(e)}. If you are using Chrome, add '--enable-unsafe-extension-debugging' " + 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 diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index 85620ed39844f..60e1ff2642f72 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -60,11 +60,10 @@ def test_webextension_initialized(driver): assert driver.webextension is not None -@pytest.mark.xfail_edge def test_install_extension_path(driver, pages): """Test installing an extension from a directory path. - Note: For Chrome, webextensions are enabled when BiDi is used from conftest.py for this test. + Note: For Chrome adn Edge, webextensions are enabled when BiDi is used from conftest.py for this test. You can also manually enable them using: from selenium.webdriver.chrome.options import Options @@ -85,8 +84,8 @@ def test_install_extension_path(driver, pages): """ path = os.path.join(extensions, EXTENSION_PATH) - if driver.capabilities["browserName"].lower() == "chrome": - # chrome does not uses extension id from manifest.json so we cannot assert the id + if driver.capabilities["browserName"].lower() in ["chrome", "microsoftedge"]: + # chrome/edge does not uses extension id from manifest.json so we cannot assert the id ext_info = driver.webextension.install(path=path) else: ext_info = install_extension(driver, path=path) From 2e386b923179520b35a99de9ec210e15c47818ae Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sun, 25 May 2025 11:48:35 +0530 Subject: [PATCH 04/15] enable other passing tests for edge and chrome --- .../webdriver/common/bidi_webextension_tests.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index f167769ad4b72..5b3a2a592e14c 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -121,24 +121,27 @@ def test_install_base64_extension_path(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) + if driver.capabilities["browserName"].lower() in ["chrome", "microsoftedge"]: + ext_info = driver.webextension.install(path=path) + else: + 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) + if driver.capabilities["browserName"].lower() in ["chrome", "microsoftedge"]: + ext_info = driver.webextension.install(path=path) + else: + ext_info = install_extension(driver, path=path) + extension_id = ext_info.get("extension") # Uninstall using the extension ID From 0215732caf766198f88b231df3de4ae506c140c2 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sun, 25 May 2025 11:58:38 +0530 Subject: [PATCH 05/15] fix typo --- py/test/selenium/webdriver/common/bidi_webextension_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index 5b3a2a592e14c..c50b898622ed8 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -63,7 +63,7 @@ def test_webextension_initialized(driver): def test_install_extension_path(driver, pages): """Test installing an extension from a directory path. - Note: For Chrome adn Edge, webextensions are enabled when BiDi is used from conftest.py for this test. + Note: For Chrome and Edge, webextensions are enabled when BiDi is used from conftest.py for this test. You can also manually enable them using: from selenium.webdriver.chrome.options import Options @@ -84,7 +84,7 @@ def test_install_extension_path(driver, pages): """ path = os.path.join(extensions, EXTENSION_PATH) - if driver.capabilities["browserName"].lower() in ["chrome", "microsoftedge"]: + if driver.capabilities["browserName"].lower() in ("chrome", "microsoftedge"): # chrome/edge does not uses extension id from manifest.json so we cannot assert the id ext_info = driver.webextension.install(path=path) else: From 03f7be2a538e44c38405cc8ce7252d1d29f065ae Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Mon, 26 May 2025 19:32:10 +0530 Subject: [PATCH 06/15] add and use pytest marker - `webextension` in tests --- py/conftest.py | 32 +++++++++++++------ .../common/bidi_webextension_tests.py | 5 ++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index db5fe356a0a8d..79385bf55b13a 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -85,6 +85,13 @@ def pytest_addoption(parser): ) +def pytest_configure(config): + """Add custom markers for tests.""" + config.addinivalue_line( + "markers", "webextension: mark test as requiring webextension support (for chromium based browsers)" + ) + + def pytest_ignore_collect(collection_path, config): drivers_opt = config.getoption("drivers") _drivers = set(drivers).difference(drivers_opt or drivers) @@ -167,21 +174,21 @@ def fin(): global driver_instance if driver_instance is None: if driver_class == "Firefox": - options = get_options(driver_class, request.config) + options = get_options(driver_class, request.config, request) 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) + options = get_options(driver_class, request.config, request) if driver_class == "Edge": - options = get_options(driver_class, request.config) + options = get_options(driver_class, request.config, request) if driver_class == "WebKitGTK": - options = get_options(driver_class, request.config) + options = get_options(driver_class, request.config, request) if driver_class == "WPEWebKit": - options = get_options(driver_class, request.config) + options = get_options(driver_class, request.config, request) if driver_class == "Remote": - options = get_options("Firefox", request.config) or webdriver.FirefoxOptions() + options = get_options("Firefox", request.config, request) or webdriver.FirefoxOptions() options.set_capability("moz:firefoxOptions", {}) options.enable_downloads = True if driver_path is not None: @@ -210,7 +217,7 @@ def fin(): driver_instance = None -def get_options(driver_class, config): +def get_options(driver_class, config, request=None): browser_path = config.option.binary browser_args = config.option.args headless = config.option.headless @@ -237,8 +244,13 @@ def get_options(driver_class, config): options.web_socket_url = True options.unhandled_prompt_behavior = "ignore" - # Enable webextensions for Chromium-based browsers when BiDi is enabled - if driver_class in ("Chrome", "Edge") and hasattr(options, "enable_webextensions"): + # Only enable webextensions for Chromium-based browsers when the test is marked with @pytest.mark.webextension + if ( + request + and request.node.get_closest_marker("webextension") + and driver_class in ("Chrome", "Edge") + and hasattr(options, "enable_webextensions") + ): options.enable_webextensions = True return options @@ -358,7 +370,7 @@ def clean_service(request): @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) + yield get_options(driver_class, request.config, request) @pytest.fixture diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index c50b898622ed8..89d752c27908f 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -60,10 +60,11 @@ def test_webextension_initialized(driver): assert driver.webextension is not None +@pytest.mark.webextension def test_install_extension_path(driver, pages): """Test installing an extension from a directory path. - Note: For Chrome and Edge, webextensions are enabled when BiDi is used from conftest.py for this test. + Note: For Chrome and Edge, webextensions are enabled when the test is marked with @pytest.mark.webextension. You can also manually enable them using: from selenium.webdriver.chrome.options import Options @@ -121,6 +122,7 @@ def test_install_base64_extension_path(driver, pages): uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) +@pytest.mark.webextension def test_install_unsigned_extension(driver, pages): """Test installing an unsigned extension.""" path = os.path.join(extensions, "webextensions-selenium-example") @@ -133,6 +135,7 @@ def test_install_unsigned_extension(driver, pages): uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) +@pytest.mark.webextension 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) From 26d4dc947ec38cf617d76fa11e53a378d564619d Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Mon, 26 May 2025 20:22:24 +0530 Subject: [PATCH 07/15] move marker to pyproject.toml --- py/conftest.py | 7 ------- py/pyproject.toml | 3 ++- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 79385bf55b13a..fbe30aeeb3fa3 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -85,13 +85,6 @@ def pytest_addoption(parser): ) -def pytest_configure(config): - """Add custom markers for tests.""" - config.addinivalue_line( - "markers", "webextension: mark test as requiring webextension support (for chromium based browsers)" - ) - - def pytest_ignore_collect(collection_path, config): drivers_opt = config.getoption("drivers") _drivers = set(drivers).difference(drivers_opt or drivers) diff --git a/py/pyproject.toml b/py/pyproject.toml index 10582aaefcdac..d1a5732ca7f26 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -83,7 +83,8 @@ markers = [ "xfail_safari: Tests expected to fail in Safari", "xfail_webkitgtk: Tests expected to fail in WebKitGTK", "xfail_wpewebkit: Tests expected to fail in WPEWebKit", - "no_driver_after_test: If there are no drivers after the test it will create a new one." + "no_driver_after_test: If there are no drivers after the test it will create a new one.", + "webextension: Tests that require webextension support (adds special flags for Chrome/Edge)" ] python_files = ["test_*.py", "*_test.py", "*_tests.py"] testpaths = ["test"] From 3c8f861e8745271f38f7539e60f194e60646458f Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 28 May 2025 20:13:36 +0530 Subject: [PATCH 08/15] add separate test class and driver for chrome/edge tests --- py/conftest.py | 25 +-- py/pyproject.toml | 3 +- .../common/bidi_webextension_tests.py | 147 ++++++++++-------- 3 files changed, 90 insertions(+), 85 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index fbe30aeeb3fa3..45e5c704f8595 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -167,21 +167,21 @@ def fin(): global driver_instance if driver_instance is None: if driver_class == "Firefox": - options = get_options(driver_class, request.config, request) + 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, request) + options = get_options(driver_class, request.config) if driver_class == "Edge": - options = get_options(driver_class, request.config, request) + options = get_options(driver_class, request.config) if driver_class == "WebKitGTK": - options = get_options(driver_class, request.config, request) + options = get_options(driver_class, request.config) if driver_class == "WPEWebKit": - options = get_options(driver_class, request.config, request) + options = get_options(driver_class, request.config) if driver_class == "Remote": - options = get_options("Firefox", request.config, request) or webdriver.FirefoxOptions() + options = get_options("Firefox", request.config) or webdriver.FirefoxOptions() options.set_capability("moz:firefoxOptions", {}) options.enable_downloads = True if driver_path is not None: @@ -210,7 +210,7 @@ def fin(): driver_instance = None -def get_options(driver_class, config, request=None): +def get_options(driver_class, config): browser_path = config.option.binary browser_args = config.option.args headless = config.option.headless @@ -237,15 +237,6 @@ def get_options(driver_class, config, request=None): options.web_socket_url = True options.unhandled_prompt_behavior = "ignore" - # Only enable webextensions for Chromium-based browsers when the test is marked with @pytest.mark.webextension - if ( - request - and request.node.get_closest_marker("webextension") - and driver_class in ("Chrome", "Edge") - and hasattr(options, "enable_webextensions") - ): - options.enable_webextensions = True - return options @@ -363,7 +354,7 @@ def clean_service(request): @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, request) + yield get_options(driver_class, request.config) @pytest.fixture diff --git a/py/pyproject.toml b/py/pyproject.toml index d1a5732ca7f26..10582aaefcdac 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -83,8 +83,7 @@ markers = [ "xfail_safari: Tests expected to fail in Safari", "xfail_webkitgtk: Tests expected to fail in WebKitGTK", "xfail_wpewebkit: Tests expected to fail in WPEWebKit", - "no_driver_after_test: If there are no drivers after the test it will create a new one.", - "webextension: Tests that require webextension support (adds special flags for Chrome/Edge)" + "no_driver_after_test: If there are no drivers after the test it will create a new one." ] python_files = ["test_*.py", "*_test.py", "*_tests.py"] testpaths = ["test"] diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index 89d752c27908f..8a23bb261190c 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -20,7 +20,9 @@ import pytest from python.runfiles import Runfiles - +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.edge.options import Options as EdgeOptions from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait @@ -60,92 +62,105 @@ def test_webextension_initialized(driver): assert driver.webextension is not None -@pytest.mark.webextension -def test_install_extension_path(driver, pages): - """Test installing an extension from a directory path. +@pytest.mark.xfail_chrome +@pytest.mark.xfail_edge +class TestFirefoxWebExtension: + """Firefox-specific WebExtension tests.""" - Note: For Chrome and Edge, webextensions are enabled when the test is marked with @pytest.mark.webextension. - You can also manually enable them using: + def test_install_extension_path(self, driver, pages): + """Test installing an extension from a directory path.""" - from selenium.webdriver.chrome.options import Options - options = Options() - options.enable_webextensions = True - driver = webdriver.Chrome(options=options) + 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) - Or directly pass the required flags when creating the driver: + def test_install_archive_extension_path(self, driver, pages): + """Test installing an extension from an archive path.""" - from selenium import webdriver - from selenium.webdriver.chrome.options import Options + 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) - options = Options() - options.add_argument("--remote-debugging-pipe") - options.add_argument("--enable-unsafe-extension-debugging") + def test_install_base64_extension_path(self, driver, pages): + """Test installing an extension from a base64 encoded string.""" - driver = webdriver.Chrome(options=options) - """ - path = os.path.join(extensions, EXTENSION_PATH) + 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) - if driver.capabilities["browserName"].lower() in ("chrome", "microsoftedge"): - # chrome/edge does not uses extension id from manifest.json so we cannot assert the id - ext_info = driver.webextension.install(path=path) - else: + 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) + 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.""" -@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) + 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_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) +@pytest.mark.xfail_firefox +class TestChromiumWebExtension: + """Chrome/Edge-specific WebExtension tests with custom driver.""" - with open(path, "rb") as file: - base64_encoded = base64.b64encode(file.read()).decode("utf-8") + @pytest.fixture + def chromium_driver(self, request): + driver_option = request.config.option.drivers[0].lower() - ext_info = install_extension(driver, base64_value=base64_encoded) + if driver_option == "chrome": + options = ChromeOptions() + browser_class = webdriver.Chrome + elif driver_option == "edge": + options = EdgeOptions() + browser_class = webdriver.Edge + else: + pytest.skip(f"This test requires Chrome or Edge, got {driver_option}") - # TODO: the extension is installed but the script is not injected, check and fix - # verify_extension_injection(driver, pages) + options.enable_bidi = True + options.enable_webextensions = True - uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + driver = browser_class(options=options) + yield driver + driver.quit() -@pytest.mark.webextension -def test_install_unsigned_extension(driver, pages): - """Test installing an unsigned extension.""" - path = os.path.join(extensions, "webextensions-selenium-example") + def test_install_extension_path(self, chromium_driver, pages): + """Test installing an extension from a directory path.""" + path = os.path.join(extensions, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) - if driver.capabilities["browserName"].lower() in ["chrome", "microsoftedge"]: - ext_info = driver.webextension.install(path=path) - else: - ext_info = install_extension(driver, path=path) - verify_extension_injection(driver, pages) - uninstall_extension_and_verify_extension_uninstalled(driver, ext_info) + chromium_driver.get("https://www.webpagetest.org/blank.html") + verify_extension_injection(chromium_driver, pages) + uninstall_extension_and_verify_extension_uninstalled(chromium_driver, ext_info) -@pytest.mark.webextension -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) + def test_install_unsigned_extension(self, chromium_driver, pages): + """Test installing an unsigned extension.""" + path = os.path.join(extensions, "webextensions-selenium-example") + ext_info = chromium_driver.webextension.install(path=path) - if driver.capabilities["browserName"].lower() in ["chrome", "microsoftedge"]: - ext_info = driver.webextension.install(path=path) - else: - ext_info = install_extension(driver, path=path) + chromium_driver.get("https://www.webpagetest.org/blank.html") - extension_id = ext_info.get("extension") + verify_extension_injection(chromium_driver, pages) + uninstall_extension_and_verify_extension_uninstalled(chromium_driver, ext_info) - # Uninstall using the extension ID - uninstall_extension_and_verify_extension_uninstalled(driver, extension_id) + def test_install_with_extension_id_uninstall(self, chromium_driver, pages): + """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) From ea6b3fff325990e23cc89129eb74faab5250b693 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 28 May 2025 22:47:56 +0530 Subject: [PATCH 09/15] use tempdir --- .../selenium/webdriver/common/bidi_webextension_tests.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index 8a23bb261190c..d5cc7998a2112 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -17,13 +17,15 @@ import base64 import os +import tempfile import pytest from python.runfiles import Runfiles + from selenium import webdriver from selenium.webdriver.chrome.options import Options as ChromeOptions -from selenium.webdriver.edge.options import Options as EdgeOptions from selenium.webdriver.common.by import By +from selenium.webdriver.edge.options import Options as EdgeOptions from selenium.webdriver.support.wait import WebDriverWait EXTENSION_ID = "webextensions-selenium-example-v3@example.com" @@ -129,8 +131,11 @@ def chromium_driver(self, request): else: pytest.skip(f"This test requires Chrome or Edge, got {driver_option}") + temp_dir = tempfile.mkdtemp() + options.enable_bidi = True options.enable_webextensions = True + options.add_argument(f"--user-data-dir={temp_dir}") driver = browser_class(options=options) From 19b54683a6a69f4b247d4686dd23043cd98a2b80 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 28 May 2025 22:57:53 +0530 Subject: [PATCH 10/15] use custom pages fixture --- .../common/bidi_webextension_tests.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index d5cc7998a2112..c1c65d4d5e701 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -118,6 +118,17 @@ def test_install_with_extension_id_uninstall(self, driver, pages): class TestChromiumWebExtension: """Chrome/Edge-specific WebExtension tests with custom driver.""" + @pytest.fixture + def pages_chromium(self, chromium_driver, pages): + class ChromiumPages: + def url(self, name, localhost=False): + return pages.url(name, localhost) + + def load(self, name): + chromium_driver.get(self.url(name)) + + return ChromiumPages() + @pytest.fixture def chromium_driver(self, request): driver_option = request.config.option.drivers[0].lower() @@ -142,27 +153,23 @@ def chromium_driver(self, request): yield driver driver.quit() - def test_install_extension_path(self, chromium_driver, pages): + 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) - chromium_driver.get("https://www.webpagetest.org/blank.html") - - verify_extension_injection(chromium_driver, pages) + 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): + 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) - chromium_driver.get("https://www.webpagetest.org/blank.html") - - verify_extension_injection(chromium_driver, pages) + 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, pages): + 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) From 4dd116928d9a69290ec1d82d7a2cf075df4d4be4 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 29 May 2025 00:03:42 +0530 Subject: [PATCH 11/15] simplify `pages_chromium` fixture --- .../webdriver/common/bidi_webextension_tests.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index c1c65d4d5e701..f103de4ab5731 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -119,15 +119,11 @@ class TestChromiumWebExtension: """Chrome/Edge-specific WebExtension tests with custom driver.""" @pytest.fixture - def pages_chromium(self, chromium_driver, pages): - class ChromiumPages: - def url(self, name, localhost=False): - return pages.url(name, localhost) - + def pages_chromium(self, webserver, chromium_driver): + class Pages: def load(self, name): - chromium_driver.get(self.url(name)) - - return ChromiumPages() + chromium_driver.get(webserver.where_is(name, localhost=False)) + return Pages() @pytest.fixture def chromium_driver(self, request): From 4e73b97bb7cab417d4a6a444c38d5fae3400772b Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 29 May 2025 00:28:39 +0530 Subject: [PATCH 12/15] move `chromium_options` to conftest.py --- py/conftest.py | 26 +++++++++++++++++++ .../common/bidi_webextension_tests.py | 22 +++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) 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/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index f103de4ab5731..794dc9cd3b6b7 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -23,9 +23,7 @@ from python.runfiles import Runfiles from selenium import webdriver -from selenium.webdriver.chrome.options import Options as ChromeOptions from selenium.webdriver.common.by import By -from selenium.webdriver.edge.options import Options as EdgeOptions from selenium.webdriver.support.wait import WebDriverWait EXTENSION_ID = "webextensions-selenium-example-v3@example.com" @@ -123,31 +121,29 @@ 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, request): + 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": - options = ChromeOptions() browser_class = webdriver.Chrome elif driver_option == "edge": - options = EdgeOptions() browser_class = webdriver.Edge - else: - pytest.skip(f"This test requires Chrome or Edge, got {driver_option}") temp_dir = tempfile.mkdtemp() - options.enable_bidi = True - options.enable_webextensions = True - options.add_argument(f"--user-data-dir={temp_dir}") + chromium_options.enable_bidi = True + chromium_options.enable_webextensions = True + chromium_options.add_argument(f"--user-data-dir={temp_dir}") - driver = browser_class(options=options) + chromium_driver = browser_class(options=chromium_options) - yield driver - driver.quit() + yield chromium_driver + chromium_driver.quit() def test_install_extension_path(self, chromium_driver, pages_chromium): """Test installing an extension from a directory path.""" From 433bec7273b2797acbea678d49c14fc233aef435 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 29 May 2025 08:18:02 +0530 Subject: [PATCH 13/15] remove user data dir --- py/test/selenium/webdriver/common/bidi_webextension_tests.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index 794dc9cd3b6b7..c05ed9b164255 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -17,7 +17,6 @@ import base64 import os -import tempfile import pytest from python.runfiles import Runfiles @@ -134,11 +133,8 @@ def chromium_driver(self, chromium_options, request): elif driver_option == "edge": browser_class = webdriver.Edge - temp_dir = tempfile.mkdtemp() - chromium_options.enable_bidi = True chromium_options.enable_webextensions = True - chromium_options.add_argument(f"--user-data-dir={temp_dir}") chromium_driver = browser_class(options=chromium_options) From 3d604bc02be4986cbb4043189c15c309a4d5925a Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 29 May 2025 13:36:30 +0530 Subject: [PATCH 14/15] add required flags and delete tempdir --- .../webdriver/common/bidi_webextension_tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index c05ed9b164255..3ea6745730bc9 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -17,6 +17,8 @@ import base64 import os +import shutil +import tempfile import pytest from python.runfiles import Runfiles @@ -133,14 +135,23 @@ def chromium_driver(self, chromium_options, request): 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) From ae9fdedb2ae6a942a23667b7222eed847d77d282 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 29 May 2025 14:08:05 +0530 Subject: [PATCH 15/15] run `format.sh` --- py/selenium/webdriver/common/bidi/webextension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/selenium/webdriver/common/bidi/webextension.py b/py/selenium/webdriver/common/bidi/webextension.py index d4cd6d636cfc7..d91a89cfce2c2 100644 --- a/py/selenium/webdriver/common/bidi/webextension.py +++ b/py/selenium/webdriver/common/bidi/webextension.py @@ -17,8 +17,8 @@ from typing import Union -from selenium.webdriver.common.bidi.common import command_builder from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.bidi.common import command_builder class WebExtension: