Skip to content

Commit b7f31ce

Browse files
authored
[py][bidi]: add enable_webextensions option for chromium-based browsers (#15794)
* add `enable_webextensions` method and exception for chrome webextensions * add chrome test for webextension from path * add separate test class and driver for chrome/edge tests
1 parent 3788f49 commit b7f31ce

File tree

4 files changed

+183
-59
lines changed

4 files changed

+183
-59
lines changed

py/conftest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,29 @@ def firefox_options(request):
370370
if request.config.option.headless:
371371
options.add_argument("-headless")
372372
return options
373+
374+
375+
@pytest.fixture
376+
def chromium_options(request):
377+
try:
378+
driver_option = request.config.option.drivers[0].lower()
379+
except (AttributeError, TypeError):
380+
raise Exception("This test requires a --driver to be specified")
381+
382+
# Skip if not Chrome or Edge
383+
if driver_option not in ("chrome", "edge"):
384+
pytest.skip(f"This test requires Chrome or Edge, got {driver_option}")
385+
386+
# skip tests in the 'remote' directory if run with a local driver
387+
if request.node.path.parts[-2] == "remote" and get_driver_class(driver_option) != "Remote":
388+
pytest.skip(f"Remote tests can't be run with driver '{driver_option}'")
389+
390+
if driver_option == "chrome":
391+
options = webdriver.ChromeOptions()
392+
elif driver_option == "edge":
393+
options = webdriver.EdgeOptions()
394+
395+
if request.config.option.headless:
396+
options.add_argument("--headless=new")
397+
398+
return options

py/selenium/webdriver/chromium/options.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(self) -> None:
3333
self._extensions: list[str] = []
3434
self._experimental_options: dict[str, Union[str, int, dict, list[str]]] = {}
3535
self._debugger_address: Optional[str] = None
36+
self._enable_webextensions: bool = False
3637

3738
@property
3839
def binary_location(self) -> str:
@@ -126,6 +127,39 @@ def add_experimental_option(self, name: str, value: Union[str, int, dict, list[s
126127
"""
127128
self._experimental_options[name] = value
128129

130+
@property
131+
def enable_webextensions(self) -> bool:
132+
"""Returns whether webextension support is enabled for Chromium-based browsers.
133+
134+
:Returns: True if webextension support is enabled, False otherwise.
135+
"""
136+
return self._enable_webextensions
137+
138+
@enable_webextensions.setter
139+
def enable_webextensions(self, value: bool) -> None:
140+
"""Enables or disables webextension support for Chromium-based browsers.
141+
142+
When enabled, this automatically adds the required Chromium flags:
143+
- --enable-unsafe-extension-debugging
144+
- --remote-debugging-pipe
145+
146+
:Args:
147+
- value: True to enable webextension support, False to disable.
148+
"""
149+
self._enable_webextensions = value
150+
if value:
151+
# Add required flags for Chromium webextension support
152+
required_flags = ["--enable-unsafe-extension-debugging", "--remote-debugging-pipe"]
153+
for flag in required_flags:
154+
if flag not in self._arguments:
155+
self.add_argument(flag)
156+
else:
157+
# Remove webextension flags if disabling
158+
flags_to_remove = ["--enable-unsafe-extension-debugging", "--remote-debugging-pipe"]
159+
for flag in flags_to_remove:
160+
if flag in self._arguments:
161+
self._arguments.remove(flag)
162+
129163
def to_capabilities(self) -> dict:
130164
"""Creates a capabilities with all the options that have been set
131165
:Returns: A dictionary with everything."""

py/selenium/webdriver/common/bidi/webextension.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from typing import Union
1919

20+
from selenium.common.exceptions import WebDriverException
2021
from selenium.webdriver.common.bidi.common import command_builder
2122

2223

@@ -54,8 +55,17 @@ def install(self, path=None, archive_path=None, base64_value=None) -> dict:
5455
extension_data = {"type": "base64", "value": base64_value}
5556

5657
params = {"extensionData": extension_data}
57-
result = self.conn.execute(command_builder("webExtension.install", params))
58-
return result
58+
59+
try:
60+
result = self.conn.execute(command_builder("webExtension.install", params))
61+
return result
62+
except WebDriverException as e:
63+
if "Method not available" in str(e):
64+
raise WebDriverException(
65+
f"{str(e)}. If you are using Chrome or Edge, add '--enable-unsafe-extension-debugging' "
66+
"and '--remote-debugging-pipe' arguments or set options.enable_webextensions = True"
67+
) from e
68+
raise
5969

6070
def uninstall(self, extension_id_or_result: Union[str, dict]) -> None:
6171
"""Uninstalls a web extension from the remote end.

py/test/selenium/webdriver/common/bidi_webextension_tests.py

Lines changed: 111 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717

1818
import base64
1919
import os
20+
import shutil
21+
import tempfile
2022

2123
import pytest
2224
from python.runfiles import Runfiles
2325

26+
from selenium import webdriver
2427
from selenium.webdriver.common.by import By
2528
from selenium.webdriver.support.wait import WebDriverWait
2629

@@ -62,62 +65,113 @@ def test_webextension_initialized(driver):
6265

6366
@pytest.mark.xfail_chrome
6467
@pytest.mark.xfail_edge
65-
def test_install_extension_path(driver, pages):
66-
"""Test installing an extension from a directory path."""
67-
path = os.path.join(extensions, EXTENSION_PATH)
68+
class TestFirefoxWebExtension:
69+
"""Firefox-specific WebExtension tests."""
6870

69-
ext_info = install_extension(driver, path=path)
70-
verify_extension_injection(driver, pages)
71-
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
71+
def test_install_extension_path(self, driver, pages):
72+
"""Test installing an extension from a directory path."""
7273

73-
74-
@pytest.mark.xfail_chrome
75-
@pytest.mark.xfail_edge
76-
def test_install_archive_extension_path(driver, pages):
77-
"""Test installing an extension from an archive path."""
78-
path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH)
79-
80-
ext_info = install_extension(driver, archive_path=path)
81-
verify_extension_injection(driver, pages)
82-
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
83-
84-
85-
@pytest.mark.xfail_chrome
86-
@pytest.mark.xfail_edge
87-
def test_install_base64_extension_path(driver, pages):
88-
"""Test installing an extension from a base64 encoded string."""
89-
path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH)
90-
91-
with open(path, "rb") as file:
92-
base64_encoded = base64.b64encode(file.read()).decode("utf-8")
93-
94-
ext_info = install_extension(driver, base64_value=base64_encoded)
95-
96-
# TODO: the extension is installed but the script is not injected, check and fix
97-
# verify_extension_injection(driver, pages)
98-
99-
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
100-
101-
102-
@pytest.mark.xfail_chrome
103-
@pytest.mark.xfail_edge
104-
def test_install_unsigned_extension(driver, pages):
105-
"""Test installing an unsigned extension."""
106-
path = os.path.join(extensions, "webextensions-selenium-example")
107-
108-
ext_info = install_extension(driver, path=path)
109-
verify_extension_injection(driver, pages)
110-
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
111-
112-
113-
@pytest.mark.xfail_chrome
114-
@pytest.mark.xfail_edge
115-
def test_install_with_extension_id_uninstall(driver, pages):
116-
"""Test uninstalling an extension using just the extension ID."""
117-
path = os.path.join(extensions, EXTENSION_PATH)
118-
119-
ext_info = install_extension(driver, path=path)
120-
extension_id = ext_info.get("extension")
121-
122-
# Uninstall using the extension ID
123-
uninstall_extension_and_verify_extension_uninstalled(driver, extension_id)
74+
path = os.path.join(extensions, EXTENSION_PATH)
75+
ext_info = install_extension(driver, path=path)
76+
verify_extension_injection(driver, pages)
77+
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
78+
79+
def test_install_archive_extension_path(self, driver, pages):
80+
"""Test installing an extension from an archive path."""
81+
82+
path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH)
83+
ext_info = install_extension(driver, archive_path=path)
84+
verify_extension_injection(driver, pages)
85+
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
86+
87+
def test_install_base64_extension_path(self, driver, pages):
88+
"""Test installing an extension from a base64 encoded string."""
89+
90+
path = os.path.join(extensions, EXTENSION_ARCHIVE_PATH)
91+
with open(path, "rb") as file:
92+
base64_encoded = base64.b64encode(file.read()).decode("utf-8")
93+
ext_info = install_extension(driver, base64_value=base64_encoded)
94+
# TODO: the extension is installed but the script is not injected, check and fix
95+
# verify_extension_injection(driver, pages)
96+
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
97+
98+
def test_install_unsigned_extension(self, driver, pages):
99+
"""Test installing an unsigned extension."""
100+
101+
path = os.path.join(extensions, "webextensions-selenium-example")
102+
ext_info = install_extension(driver, path=path)
103+
verify_extension_injection(driver, pages)
104+
uninstall_extension_and_verify_extension_uninstalled(driver, ext_info)
105+
106+
def test_install_with_extension_id_uninstall(self, driver, pages):
107+
"""Test uninstalling an extension using just the extension ID."""
108+
109+
path = os.path.join(extensions, EXTENSION_PATH)
110+
ext_info = install_extension(driver, path=path)
111+
extension_id = ext_info.get("extension")
112+
# Uninstall using the extension ID
113+
uninstall_extension_and_verify_extension_uninstalled(driver, extension_id)
114+
115+
116+
@pytest.mark.xfail_firefox
117+
class TestChromiumWebExtension:
118+
"""Chrome/Edge-specific WebExtension tests with custom driver."""
119+
120+
@pytest.fixture
121+
def pages_chromium(self, webserver, chromium_driver):
122+
class Pages:
123+
def load(self, name):
124+
chromium_driver.get(webserver.where_is(name, localhost=False))
125+
126+
return Pages()
127+
128+
@pytest.fixture
129+
def chromium_driver(self, chromium_options, request):
130+
"""Create a Chrome/Edge driver with webextension support enabled."""
131+
driver_option = request.config.option.drivers[0].lower()
132+
133+
if driver_option == "chrome":
134+
browser_class = webdriver.Chrome
135+
elif driver_option == "edge":
136+
browser_class = webdriver.Edge
137+
138+
temp_dir = tempfile.mkdtemp(prefix="chrome-profile-")
139+
140+
chromium_options.enable_bidi = True
141+
chromium_options.enable_webextensions = True
142+
chromium_options.add_argument(f"--user-data-dir={temp_dir}")
143+
chromium_options.add_argument("--no-sandbox")
144+
chromium_options.add_argument("--disable-dev-shm-usage")
145+
146+
chromium_driver = browser_class(options=chromium_options)
147+
148+
yield chromium_driver
149+
chromium_driver.quit()
150+
151+
# delete the temp directory
152+
if os.path.exists(temp_dir):
153+
shutil.rmtree(temp_dir)
154+
155+
def test_install_extension_path(self, chromium_driver, pages_chromium):
156+
"""Test installing an extension from a directory path."""
157+
path = os.path.join(extensions, EXTENSION_PATH)
158+
ext_info = chromium_driver.webextension.install(path=path)
159+
160+
verify_extension_injection(chromium_driver, pages_chromium)
161+
uninstall_extension_and_verify_extension_uninstalled(chromium_driver, ext_info)
162+
163+
def test_install_unsigned_extension(self, chromium_driver, pages_chromium):
164+
"""Test installing an unsigned extension."""
165+
path = os.path.join(extensions, "webextensions-selenium-example")
166+
ext_info = chromium_driver.webextension.install(path=path)
167+
168+
verify_extension_injection(chromium_driver, pages_chromium)
169+
uninstall_extension_and_verify_extension_uninstalled(chromium_driver, ext_info)
170+
171+
def test_install_with_extension_id_uninstall(self, chromium_driver):
172+
"""Test uninstalling an extension using just the extension ID."""
173+
path = os.path.join(extensions, EXTENSION_PATH)
174+
ext_info = chromium_driver.webextension.install(path=path)
175+
extension_id = ext_info.get("extension")
176+
# Uninstall using the extension ID
177+
uninstall_extension_and_verify_extension_uninstalled(chromium_driver, extension_id)

0 commit comments

Comments
 (0)