From 5e161e7c88be829f9a182ab2484d7fcaaff2dbbf Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 5 Jun 2026 18:12:14 -0500 Subject: [PATCH 1/3] [py] drive WebKitGTK/WPE drivers from conftest and deprecate the shipped modules --- py/BUILD.bazel | 184 ------------------ py/conftest.py | 107 ++++++++-- py/selenium/webdriver/webkitgtk/options.py | 7 + py/selenium/webdriver/webkitgtk/service.py | 7 + py/selenium/webdriver/webkitgtk/webdriver.py | 8 + py/selenium/webdriver/wpewebkit/options.py | 8 + py/selenium/webdriver/wpewebkit/service.py | 7 + py/selenium/webdriver/wpewebkit/webdriver.py | 8 + .../webkitgtk/webkitgtk_options_tests.py | 72 ------- .../wpewebkit/wpewebkit_options_tests.py | 60 ------ 10 files changed, 140 insertions(+), 328 deletions(-) delete mode 100644 py/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py delete mode 100644 py/test/unit/selenium/webdriver/wpewebkit/wpewebkit_options_tests.py diff --git a/py/BUILD.bazel b/py/BUILD.bazel index 6835e30127bbf..73d06c67e79de 100644 --- a/py/BUILD.bazel +++ b/py/BUILD.bazel @@ -1219,190 +1219,6 @@ test_suite( ], ) -py_test_suite( - name = "test-webkitgtk-common", - size = "large", - srcs = glob( - [ - "test/selenium/webdriver/common/**/*.py", - "test/selenium/webdriver/support/**/*.py", - ], - exclude = BIDI_TESTS + ACTIONS_TESTS + FEATURE_TESTS, - ), - args = [ - "--instafail", - "--driver=webkitgtk", - "--browser-binary=MiniBrowser", - "--browser-args=--automation", - ], - tags = [ - "exclusive-if-local", - "no-sandbox", - "skip-rbe", - ], - test_suffix = "webkitgtk", - deps = [ - ":common", - ":init-tree", - ":support", - ":webkitgtk", - ":webserver", - ] + TEST_DEPS, -) - -py_test_suite( - name = "test-webkitgtk-actions", - size = "large", - srcs = ACTIONS_TESTS, - args = [ - "--instafail", - "--driver=webkitgtk", - "--browser-binary=MiniBrowser", - "--browser-args=--automation", - ], - tags = [ - "exclusive-if-local", - "no-sandbox", - "skip-rbe", - ], - test_suffix = "webkitgtk-actions", - deps = [ - ":common_actions", - ":init-tree", - ":support", - ":webkitgtk", - ":webserver", - ] + TEST_DEPS, -) - -[ - py_test_suite( - name = "test-webkitgtk-%s" % feature, - size = "large", - srcs = FEATURE_SUITE_DEFS[feature][0], - args = [ - "--instafail", - "--driver=webkitgtk", - "--browser-binary=MiniBrowser", - "--browser-args=--automation", - ], - tags = [ - "exclusive-if-local", - "no-sandbox", - "skip-rbe", - ], - test_suffix = "webkitgtk-%s" % feature, - deps = [ - ":init-tree", - ":support", - ":webkitgtk", - ":webserver", - FEATURE_SUITE_DEFS[feature][1], - ] + TEST_DEPS, - ) - for feature in FEATURE_SUITE_DEFS -] - -test_suite( - name = "test-webkitgtk", - tests = [ - ":test-webkitgtk-actions", - ":test-webkitgtk-common", - ] + [":test-webkitgtk-%s" % f for f in FEATURE_SUITE_DEFS], -) - -py_test_suite( - name = "test-wpewebkit-common", - size = "large", - srcs = glob( - [ - "test/selenium/webdriver/common/**/*.py", - "test/selenium/webdriver/support/**/*.py", - ], - exclude = BIDI_TESTS + ACTIONS_TESTS + FEATURE_TESTS, - ), - args = [ - "--instafail", - "--driver=wpewebkit", - "--browser-binary=MiniBrowser", - "--browser-args=--automation --headless", - ], - tags = [ - "exclusive-if-local", - "no-sandbox", - "skip-rbe", - ], - test_suffix = "wpewebkit", - deps = [ - ":common", - ":init-tree", - ":support", - ":webserver", - ":wpewebkit", - ] + TEST_DEPS, -) - -py_test_suite( - name = "test-wpewebkit-actions", - size = "large", - srcs = ACTIONS_TESTS, - args = [ - "--instafail", - "--driver=wpewebkit", - "--browser-binary=MiniBrowser", - "--browser-args=--automation --headless", - ], - tags = [ - "exclusive-if-local", - "no-sandbox", - "skip-rbe", - ], - test_suffix = "wpewebkit-actions", - deps = [ - ":common_actions", - ":init-tree", - ":support", - ":webserver", - ":wpewebkit", - ] + TEST_DEPS, -) - -[ - py_test_suite( - name = "test-wpewebkit-%s" % feature, - size = "large", - srcs = FEATURE_SUITE_DEFS[feature][0], - args = [ - "--instafail", - "--driver=wpewebkit", - "--browser-binary=MiniBrowser", - "--browser-args=--automation --headless", - ], - tags = [ - "exclusive-if-local", - "no-sandbox", - "skip-rbe", - ], - test_suffix = "wpewebkit-%s" % feature, - deps = [ - ":init-tree", - ":support", - ":webserver", - ":wpewebkit", - FEATURE_SUITE_DEFS[feature][1], - ] + TEST_DEPS, - ) - for feature in FEATURE_SUITE_DEFS -] - -test_suite( - name = "test-wpewebkit", - tests = [ - ":test-wpewebkit-actions", - ":test-wpewebkit-common", - ] + [":test-wpewebkit-%s" % f for f in FEATURE_SUITE_DEFS], -) - py_binary( name = "generate-api-listing", srcs = ["generate_api_module_listing.py"], diff --git a/py/conftest.py b/py/conftest.py index 198fc6d9ada48..4b82553e5115e 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -39,7 +39,10 @@ from selenium import webdriver from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.options import ArgOptions +from selenium.webdriver.common.service import Service from selenium.webdriver.common.utils import free_port +from selenium.webdriver.common.webdriver import LocalWebDriver from selenium.webdriver.remote.server import Server from test.selenium.webdriver.common.network import get_lan_ip from test.selenium.webdriver.common.webserver import SimpleWebServer @@ -278,8 +281,6 @@ class SupportedDrivers(ContainerProtocol): safari: str = "Safari" edge: str = "Edge" ie: str = "Ie" - webkitgtk: str = "WebKitGTK" - wpewebkit: str = "WPEWebKit" @dataclass @@ -289,8 +290,6 @@ class SupportedOptions(ContainerProtocol): edge: str = "EdgeOptions" safari: str = "SafariOptions" ie: str = "IeOptions" - webkitgtk: str = "WebKitGTKOptions" - wpewebkit: str = "WPEWebKitOptions" @dataclass @@ -419,8 +418,6 @@ def options(self, cls_name): self._options.enable_downloads = True 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: @@ -448,8 +445,6 @@ def is_platform_valid(self): 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 @property @@ -470,7 +465,7 @@ def _start_local_driver(self, kwargs): if self.driver_path is not None: kwargs["service"] = self.service try: - return getattr(webdriver, self.driver_class)(**kwargs) + return self._build_local_driver(kwargs) except (WebDriverException, urllib3.exceptions.HTTPError, OSError) as e: if attempt == self.DRIVER_START_RETRIES: raise @@ -489,6 +484,9 @@ def stop_driver(self): if driver_to_stop is not None: driver_to_stop.quit() + def _build_local_driver(self, kwargs): + return getattr(webdriver, self.driver_class)(**kwargs) + @pytest.fixture def driver(request, server): @@ -496,7 +494,7 @@ def driver(request, server): driver_class = getattr(request, "param", "Chrome").lower() if selenium_driver is None: - selenium_driver = Driver(driver_class, request) + selenium_driver = make_driver(driver_class, request) if server: selenium_driver._server = server @@ -700,14 +698,14 @@ def clean_driver(request): @pytest.fixture def clean_service(request): driver_class = request.config.option.drivers[0].lower() - selenium_driver = Driver(driver_class, request) + selenium_driver = make_driver(driver_class, request) return selenium_driver.service @pytest.fixture def clean_options(request): driver_class = request.config.option.drivers[0].lower() - return Driver.clean_options(driver_class, request) + return make_driver(driver_class, request).options @pytest.fixture @@ -778,3 +776,88 @@ def do_GET(self): for server in servers: server.shutdown() server.server_close() + + +# WebKitGTK and WPE WebKit are W3C-compliant drivers maintained by the WebKit +# project and are not run in our CI. Kept isolated here so the base Driver +# carries no WebKit branches and they avoid the deprecated binding modules. +WEBKIT_DRIVERS = { + "webkitgtk": {"options_key": "webkitgtk:browserOptions", "overlay_scrollbars": True}, + "wpewebkit": {"options_key": "wpe:browserOptions", "overlay_scrollbars": False}, +} + + +@dataclass +class SupportedWebKitDrivers(ContainerProtocol): + webkitgtk: str = "WebKitGTK" + wpewebkit: str = "WPEWebKit" + + +class _WebKitService(Service): + def command_line_args(self) -> list[str]: + return ["-p", f"{self.port}"] + + +class _WebKitLocalDriver(LocalWebDriver): + def __init__(self, options=None, service=None): + self.service = service + self.service.start() + try: + super().__init__(command_executor=self.service.service_url, options=options) + except Exception: + self.quit() + raise + + +def _webkit_options(name, *, binary=None, args=()): + config = WEBKIT_DRIVERS[name] + args = list(args) + + options = ArgOptions() + options.set_capability("browserName", "MiniBrowser") + + browser_options = {} + if binary: + browser_options["binary"] = binary + if args: + browser_options["args"] = args + if config["overlay_scrollbars"]: + browser_options["useOverlayScrollbars"] = not (binary or args) + options.set_capability(config["options_key"], browser_options) + return options + + +class WebKitDriver(Driver): + @property + def supported_drivers(self): + return SupportedWebKitDrivers() + + @property + def is_platform_valid(self): + return self.exe_platform != "Windows" + + @Driver.options.setter + def options(self, cls_name): + self._options = _webkit_options( + cls_name.lower(), + binary=self.browser_path.strip("'") if self.browser_path else None, + args=self.browser_args.split() if self.browser_args else (), + ) + if self.is_remote: + self._options.enable_downloads = True + + @property + def service(self): + executable = self.driver_path + if executable: + self._service = _WebKitService(executable_path=executable) + return self._service + return None + + def _build_local_driver(self, kwargs): + return _WebKitLocalDriver(**kwargs) + + +def make_driver(driver_class, request): + cls = WebKitDriver if driver_class.lower() in WEBKIT_DRIVERS else Driver + return cls(driver_class, request) diff --git a/py/selenium/webdriver/webkitgtk/options.py b/py/selenium/webdriver/webkitgtk/options.py index 0eeb251aedd2b..84d05dfbccea0 100644 --- a/py/selenium/webdriver/webkitgtk/options.py +++ b/py/selenium/webdriver/webkitgtk/options.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. +import warnings from typing import Any from selenium.webdriver.common.desired_capabilities import DesiredCapabilities @@ -25,6 +26,12 @@ class Options(ArgOptions): KEY = "webkitgtk:browserOptions" def __init__(self) -> None: + warnings.warn( + "WebKitGTKOptions is deprecated and will be removed in a future release; " + "subclass ArgOptions in your own project if you still need it.", + DeprecationWarning, + stacklevel=2, + ) super().__init__() self._binary_location = "" self._overlay_scrollbars_enabled = True diff --git a/py/selenium/webdriver/webkitgtk/service.py b/py/selenium/webdriver/webkitgtk/service.py index eed4907c58e64..d443af9cde671 100644 --- a/py/selenium/webdriver/webkitgtk/service.py +++ b/py/selenium/webdriver/webkitgtk/service.py @@ -16,6 +16,7 @@ # under the License. import shutil +import warnings from collections.abc import Mapping, Sequence from typing import IO, Any @@ -45,6 +46,12 @@ def __init__( env: Mapping[str, str] | None = None, **kwargs, ) -> None: + warnings.warn( + "WebKitGTKService is deprecated and will be removed in a future release; " + "subclass Service in your own project if you still need it.", + DeprecationWarning, + stacklevel=2, + ) self._service_args = list(service_args or []) super().__init__( diff --git a/py/selenium/webdriver/webkitgtk/webdriver.py b/py/selenium/webdriver/webkitgtk/webdriver.py index e68afa1ca5313..06f92a30b6858 100644 --- a/py/selenium/webdriver/webkitgtk/webdriver.py +++ b/py/selenium/webdriver/webkitgtk/webdriver.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +import warnings + from selenium.webdriver.common.driver_finder import DriverFinder from selenium.webdriver.common.webdriver import LocalWebDriver from selenium.webdriver.webkitgtk.options import Options @@ -37,6 +39,12 @@ def __init__( options: Instance of Options. service: Service object for handling the browser driver if you need to pass extra details. """ + warnings.warn( + "WebKitGTK is deprecated and will be removed in a future release; " + "subclass LocalWebDriver in your own project if you still need it.", + DeprecationWarning, + stacklevel=2, + ) self.options = options if options else Options() self.service = service if service else Service() self.service.path = DriverFinder(self.service, self.options).get_driver_path() diff --git a/py/selenium/webdriver/wpewebkit/options.py b/py/selenium/webdriver/wpewebkit/options.py index a67bab7fbf87a..83b8840b72d2e 100644 --- a/py/selenium/webdriver/wpewebkit/options.py +++ b/py/selenium/webdriver/wpewebkit/options.py @@ -16,6 +16,8 @@ # under the License. +import warnings + from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.options import ArgOptions @@ -24,6 +26,12 @@ class Options(ArgOptions): KEY = "wpe:browserOptions" def __init__(self) -> None: + warnings.warn( + "WPEWebKitOptions is deprecated and will be removed in a future release; " + "subclass ArgOptions in your own project if you still need it.", + DeprecationWarning, + stacklevel=2, + ) super().__init__() self._binary_location = "" diff --git a/py/selenium/webdriver/wpewebkit/service.py b/py/selenium/webdriver/wpewebkit/service.py index 6fca68a3f46dd..7d9f0d663862a 100644 --- a/py/selenium/webdriver/wpewebkit/service.py +++ b/py/selenium/webdriver/wpewebkit/service.py @@ -16,6 +16,7 @@ # under the License. import shutil +import warnings from collections.abc import Mapping, Sequence from typing import IO, Any @@ -45,6 +46,12 @@ def __init__( env: Mapping[str, str] | None = None, **kwargs, ): + warnings.warn( + "WPEWebKitService is deprecated and will be removed in a future release; " + "subclass Service in your own project if you still need it.", + DeprecationWarning, + stacklevel=2, + ) self._service_args = list(service_args or []) super().__init__( diff --git a/py/selenium/webdriver/wpewebkit/webdriver.py b/py/selenium/webdriver/wpewebkit/webdriver.py index a98406d314220..2b5715427366b 100644 --- a/py/selenium/webdriver/wpewebkit/webdriver.py +++ b/py/selenium/webdriver/wpewebkit/webdriver.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +import warnings + from selenium.webdriver.common.driver_finder import DriverFinder from selenium.webdriver.common.webdriver import LocalWebDriver from selenium.webdriver.wpewebkit.options import Options @@ -37,6 +39,12 @@ def __init__( options: Instance of Options. service: Service object for handling the browser driver if you need to pass extra details. """ + warnings.warn( + "WPEWebKit is deprecated and will be removed in a future release; " + "subclass LocalWebDriver in your own project if you still need it.", + DeprecationWarning, + stacklevel=2, + ) self.options = options if options else Options() self.service = service if service else Service() self.service.path = DriverFinder(self.service, self.options).get_driver_path() diff --git a/py/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py b/py/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py deleted file mode 100644 index b6921e37465cf..0000000000000 --- a/py/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py +++ /dev/null @@ -1,72 +0,0 @@ -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -import pytest - -from selenium.webdriver.common.options import PageLoadStrategy -from selenium.webdriver.webkitgtk.options import Options - - -@pytest.fixture -def options(): - return Options() - - -def test_set_binary_location(options): - options.binary_location = "/foo/bar" - assert options._binary_location == "/foo/bar" - - -def test_get_binary_location(options): - options._binary_location = "/foo/bar" - assert options.binary_location == "/foo/bar" - - -def test_set_overlay_scrollbars_enabled(options): - options.overlay_scrollbars_enabled = False - assert options._overlay_scrollbars_enabled is False - - -def test_get_overlay_scrollbars_enabled(options): - options._overlay_scrollbars_enabled = True - assert options.overlay_scrollbars_enabled is True - - -def test_creates_capabilities(options): - options._arguments = ["foo"] - options._binary_location = "/bar" - options._overlay_scrollbars_enabled = True - caps = options.to_capabilities() - opts = caps.get(Options.KEY) - assert opts - assert "foo" in opts["args"] - assert opts["binary"] == "/bar" - assert opts["useOverlayScrollbars"] is True - - -def test_starts_with_default_capabilities(options): - from selenium.webdriver import DesiredCapabilities - - caps = DesiredCapabilities.WEBKITGTK.copy() - caps.update({"pageLoadStrategy": PageLoadStrategy.normal}) - assert options._caps == caps - - -def test_is_a_baseoptions(options): - from selenium.webdriver.common.options import BaseOptions - - assert isinstance(options, BaseOptions) diff --git a/py/test/unit/selenium/webdriver/wpewebkit/wpewebkit_options_tests.py b/py/test/unit/selenium/webdriver/wpewebkit/wpewebkit_options_tests.py deleted file mode 100644 index af00b484ac821..0000000000000 --- a/py/test/unit/selenium/webdriver/wpewebkit/wpewebkit_options_tests.py +++ /dev/null @@ -1,60 +0,0 @@ -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -import pytest - -from selenium.webdriver.common.options import PageLoadStrategy -from selenium.webdriver.wpewebkit.options import Options - - -@pytest.fixture -def options(): - return Options() - - -def test_starts_with_default_capabilities(options): - from selenium.webdriver import DesiredCapabilities - - caps = DesiredCapabilities.WPEWEBKIT.copy() - caps.update({"pageLoadStrategy": PageLoadStrategy.normal}) - assert options._caps == caps - - -def test_set_binary_location(options): - options.binary_location = "/foo/bar" - assert options._binary_location == "/foo/bar" - - -def test_get_binary_location(options): - options._binary_location = "/foo/bar" - assert options.binary_location == "/foo/bar" - - -def test_creates_capabilities(options): - options._arguments = ["foo"] - options._binary_location = "/bar" - caps = options.to_capabilities() - opts = caps.get(Options.KEY) - assert opts - assert "foo" in opts["args"] - assert opts["binary"] == "/bar" - - -def test_is_a_baseoptions(options): - from selenium.webdriver.common.options import BaseOptions - - assert isinstance(options, BaseOptions) From c94e7aa6d1595a26d65acce162d1be2f9a6db3a8 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sat, 6 Jun 2026 08:15:56 -0500 Subject: [PATCH 2/3] [py] webkit harness: stop service on failed start --- py/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/conftest.py b/py/conftest.py index 4b82553e5115e..b56bb946b4f4f 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -801,8 +801,8 @@ def command_line_args(self) -> list[str]: class _WebKitLocalDriver(LocalWebDriver): def __init__(self, options=None, service=None): self.service = service - self.service.start() try: + self.service.start() super().__init__(command_executor=self.service.service_url, options=options) except Exception: self.quit() From 6e765214a41149b2912bac7d632459abff2fe493 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sat, 6 Jun 2026 10:10:37 -0500 Subject: [PATCH 3/3] [py] webkit harness: only tear down the service when it started --- py/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/py/conftest.py b/py/conftest.py index b56bb946b4f4f..d76cd314cb428 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -805,7 +805,10 @@ def __init__(self, options=None, service=None): self.service.start() super().__init__(command_executor=self.service.service_url, options=options) except Exception: - self.quit() + # Only tear down if a process was actually started; otherwise + # Service.stop() raises AttributeError and masks the real failure. + if getattr(self.service, "process", None) is not None: + self.quit() raise