diff --git a/py/selenium/webdriver/chrome/webdriver.py b/py/selenium/webdriver/chrome/webdriver.py index 44882bcf4cae0..915bd1c0ee180 100644 --- a/py/selenium/webdriver/chrome/webdriver.py +++ b/py/selenium/webdriver/chrome/webdriver.py @@ -21,6 +21,7 @@ from selenium.webdriver.chrome.service import Service from selenium.webdriver.chromium.webdriver import ChromiumDriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.client_config import ClientConfig class WebDriver(ChromiumDriver): @@ -31,6 +32,7 @@ def __init__( options: Optional[Options] = None, service: Optional[Service] = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Creates a new instance of the chrome driver. @@ -40,6 +42,23 @@ def __init__( options: This takes an instance of ChromeOptions. service: Service object for handling the browser driver if you need to pass extra details. keep_alive: Whether to configure ChromeRemoteConnection to use HTTP keep-alive. + This parameter is ignored if client_config is provided. + client_config: ClientConfig instance for advanced HTTP/WebSocket configuration. + If provided, takes precedence over individual parameters like keep_alive. + + Example: + Basic usage:: + + driver = webdriver.Chrome() + + With custom config:: + + from selenium.webdriver.remote.client_config import ClientConfig + config = ClientConfig( + remote_server_addr="http://localhost:9515", + websocket_timeout=10 + ) + driver = webdriver.Chrome(client_config=config) """ service = service if service else Service() options = options if options else Options() @@ -50,4 +69,5 @@ def __init__( options=options, service=service, keep_alive=keep_alive, + client_config=client_config, ) diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index 894e26df3e97b..3333daf796839 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -21,6 +21,8 @@ from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection from selenium.webdriver.chromium.service import ChromiumService from selenium.webdriver.common.driver_finder import DriverFinder +from selenium.webdriver.common.utils import normalize_local_driver_config +from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.command import Command from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver @@ -35,6 +37,7 @@ def __init__( options: Optional[ChromiumOptions] = None, service: Optional[ChromiumService] = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Create a new WebDriver instance, start the service, and create new ChromiumDriver instance. @@ -44,6 +47,9 @@ def __init__( options: This takes an instance of ChromiumOptions. service: Service object for handling the browser driver if you need to pass extra details. keep_alive: Whether to configure ChromiumRemoteConnection to use HTTP keep-alive. + This parameter is ignored if client_config is provided. + client_config: ClientConfig instance for advanced HTTP/WebSocket configuration. + If provided, takes precedence over individual parameters like keep_alive. """ self.service = service if service else ChromiumService() options = options if options else ChromiumOptions() @@ -56,12 +62,17 @@ def __init__( self.service.path = self.service.env_path() or finder.get_driver_path() self.service.start() + client_config = normalize_local_driver_config( + self.service.service_url, user_config=client_config, keep_alive=keep_alive, timeout=120 + ) + executor = ChromiumRemoteConnection( remote_server_addr=self.service.service_url, browser_name=browser_name, vendor_prefix=vendor_prefix, keep_alive=keep_alive, ignore_proxy=options._ignore_local_proxy, + client_config=client_config, ) try: diff --git a/py/selenium/webdriver/common/utils.py b/py/selenium/webdriver/common/utils.py index ac3087fc17ddf..0926bb1453ee6 100644 --- a/py/selenium/webdriver/common/utils.py +++ b/py/selenium/webdriver/common/utils.py @@ -24,6 +24,7 @@ from selenium.types import AnyKey from selenium.webdriver.common.keys import Keys +from selenium.webdriver.remote.client_config import ClientConfig _is_connectable_exceptions = (socket.error, ConnectionResetError) @@ -164,3 +165,18 @@ def keys_to_typing(value: Iterable[AnyKey]) -> list[str]: else: characters.extend(val) return characters + + +def normalize_local_driver_config( + service_url: str, user_config: Optional[ClientConfig] = None, **defaults +) -> ClientConfig: + """Creates a ClientConfig for local drivers.""" + if user_config is None: + return ClientConfig(remote_server_addr=service_url, **defaults) + + # Programmatically copy attributes to avoid brittleness + config_args = { + key.lstrip("_"): value for key, value in vars(user_config).items() + } + config_args["remote_server_addr"] = service_url + return ClientConfig(**config_args) \ No newline at end of file diff --git a/py/selenium/webdriver/edge/webdriver.py b/py/selenium/webdriver/edge/webdriver.py index 4d8d895ad48fa..21282e354a9c9 100644 --- a/py/selenium/webdriver/edge/webdriver.py +++ b/py/selenium/webdriver/edge/webdriver.py @@ -21,6 +21,7 @@ from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.edge.options import Options from selenium.webdriver.edge.service import Service +from selenium.webdriver.remote.client_config import ClientConfig class WebDriver(ChromiumDriver): @@ -31,6 +32,7 @@ def __init__( options: Optional[Options] = None, service: Optional[Service] = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Creates a new instance of the edge driver. @@ -41,7 +43,20 @@ def __init__( service: Service object for handling the browser driver if you need to pass extra details. keep_alive: Whether to configure EdgeRemoteConnection to use HTTP - keep-alive. + keep-alive. This parameter is ignored if client_config is provided. + client_config: ClientConfig instance for advanced HTTP/WebSocket configuration. + If provided, takes precedence over individual parameters like keep_alive. + + Example: + Basic usage:: + + driver = webdriver.Edge() + + With custom config:: + + from selenium.webdriver.remote.client_config import ClientConfig + config = ClientConfig(websocket_timeout=10) + driver = webdriver.Edge(client_config=config) """ service = service if service else Service() options = options if options else Options() @@ -52,4 +67,5 @@ def __init__( options=options, service=service, keep_alive=keep_alive, + client_config=client_config, ) diff --git a/py/selenium/webdriver/firefox/webdriver.py b/py/selenium/webdriver/firefox/webdriver.py index 65b9eb5760d47..ef7132364be42 100644 --- a/py/selenium/webdriver/firefox/webdriver.py +++ b/py/selenium/webdriver/firefox/webdriver.py @@ -23,9 +23,11 @@ from typing import Optional from selenium.webdriver.common.driver_finder import DriverFinder +from selenium.webdriver.common.utils import normalize_local_driver_config from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.remote_connection import FirefoxRemoteConnection from selenium.webdriver.firefox.service import Service +from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver @@ -40,6 +42,7 @@ def __init__( options: Optional[Options] = None, service: Optional[Service] = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Create a new instance of the Firefox driver, start the service, and create new instance. @@ -47,6 +50,20 @@ def __init__( options: Instance of ``options.Options``. service: (Optional) service instance for managing the starting and stopping of the driver. keep_alive: Whether to configure remote_connection.RemoteConnection to use HTTP keep-alive. + This parameter is ignored if client_config is provided. + client_config: ClientConfig instance for advanced HTTP/WebSocket configuration. + If provided, takes precedence over individual parameters like keep_alive. + + Example: + Basic usage:: + + driver = webdriver.Firefox() + + With custom config:: + + from selenium.webdriver.remote.client_config import ClientConfig + config = ClientConfig(websocket_timeout=10) + driver = webdriver.Firefox(client_config=config) """ self.service = service if service else Service() options = options if options else Options() @@ -59,10 +76,15 @@ def __init__( self.service.path = self.service.env_path() or finder.get_driver_path() self.service.start() + client_config = normalize_local_driver_config( + self.service.service_url, user_config=client_config, keep_alive=keep_alive, timeout=120 + ) + executor = FirefoxRemoteConnection( remote_server_addr=self.service.service_url, keep_alive=keep_alive, ignore_proxy=options._ignore_local_proxy, + client_config=client_config, ) try: diff --git a/py/selenium/webdriver/ie/webdriver.py b/py/selenium/webdriver/ie/webdriver.py index b2380e22550fd..31b5bd6471270 100644 --- a/py/selenium/webdriver/ie/webdriver.py +++ b/py/selenium/webdriver/ie/webdriver.py @@ -18,6 +18,7 @@ from typing import Optional from selenium.webdriver.common.driver_finder import DriverFinder +from selenium.webdriver.common.utils import normalize_local_driver_config from selenium.webdriver.ie.options import Options from selenium.webdriver.ie.service import Service from selenium.webdriver.remote.client_config import ClientConfig @@ -33,6 +34,7 @@ def __init__( options: Optional[Options] = None, service: Optional[Service] = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Creates a new instance of the Ie driver. @@ -42,6 +44,20 @@ def __init__( options: IE Options instance, providing additional IE options service: (Optional) service instance for managing the starting and stopping of the driver. keep_alive: Whether to configure RemoteConnection to use HTTP keep-alive. + This parameter is ignored if client_config is provided. + client_config: ClientConfig instance for advanced HTTP/WebSocket configuration. + If provided, takes precedence over individual parameters like keep_alive. + + Example: + Basic usage:: + + driver = webdriver.Ie() + + With custom config:: + + from selenium.webdriver.remote.client_config import ClientConfig + config = ClientConfig(websocket_timeout=10) + driver = webdriver.Ie(client_config=config) """ self.service = service if service else Service() options = options if options else Options() @@ -49,7 +65,10 @@ def __init__( self.service.path = self.service.env_path() or DriverFinder(self.service, options).get_driver_path() self.service.start() - client_config = ClientConfig(remote_server_addr=self.service.service_url, keep_alive=keep_alive, timeout=120) + client_config = normalize_local_driver_config( + self.service.service_url, user_config=client_config, keep_alive=keep_alive, timeout=120 + ) + executor = RemoteConnection( ignore_proxy=options._ignore_local_proxy, client_config=client_config, diff --git a/py/selenium/webdriver/remote/client_config.py b/py/selenium/webdriver/remote/client_config.py index 480c0c7e2fe41..aada34207eba0 100644 --- a/py/selenium/webdriver/remote/client_config.py +++ b/py/selenium/webdriver/remote/client_config.py @@ -78,7 +78,7 @@ class ClientConfig: def __init__( self, - remote_server_addr: str, + remote_server_addr: Optional[str] = None, keep_alive: Optional[bool] = True, proxy: Optional[Proxy] = Proxy(raw={"proxyType": ProxyType.SYSTEM}), ignore_certificates: Optional[bool] = False, diff --git a/py/selenium/webdriver/safari/webdriver.py b/py/selenium/webdriver/safari/webdriver.py index ed16c6df90ccc..101a1f420db43 100644 --- a/py/selenium/webdriver/safari/webdriver.py +++ b/py/selenium/webdriver/safari/webdriver.py @@ -19,6 +19,8 @@ from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.driver_finder import DriverFinder +from selenium.webdriver.common.utils import normalize_local_driver_config +from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver from selenium.webdriver.safari.options import Options from selenium.webdriver.safari.remote_connection import SafariRemoteConnection @@ -30,17 +32,32 @@ class WebDriver(RemoteWebDriver): def __init__( self, - keep_alive=True, + keep_alive: bool = True, options: Optional[Options] = None, service: Optional[Service] = None, + client_config: Optional[ClientConfig] = None, ) -> None: """Create a new Safari driver instance and launch or find a running safaridriver service. Args: keep_alive: Whether to configure SafariRemoteConnection to use HTTP keep-alive. Defaults to True. + This parameter is ignored if client_config is provided. options: Instance of ``options.Options``. service: Service object for handling the browser driver if you need to pass extra details + client_config: ClientConfig instance for advanced HTTP/WebSocket configuration. + If provided, takes precedence over individual parameters like keep_alive. + + Example: + Basic usage:: + + driver = webdriver.Safari() + + With custom config:: + + from selenium.webdriver.remote.client_config import ClientConfig + config = ClientConfig(websocket_timeout=10) + driver = webdriver.Safari(client_config=config) """ self.service = service if service else Service() options = options if options else Options() @@ -50,10 +67,15 @@ def __init__( if not self.service.reuse_service: self.service.start() + client_config = normalize_local_driver_config( + self.service.service_url, user_config=client_config, keep_alive=keep_alive, timeout=120 + ) + executor = SafariRemoteConnection( remote_server_addr=self.service.service_url, keep_alive=keep_alive, ignore_proxy=options._ignore_local_proxy, + client_config=client_config, ) try: diff --git a/py/test/selenium/webdriver/chromium/webdriver_timeout_test.py b/py/test/selenium/webdriver/chromium/webdriver_timeout_test.py new file mode 100644 index 0000000000000..fe6152046f7f2 --- /dev/null +++ b/py/test/selenium/webdriver/chromium/webdriver_timeout_test.py @@ -0,0 +1,349 @@ +# 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. + +"""Functional/Integration tests for ClientConfig timeout handling in ChromiumDriver. + +These tests verify that ClientConfig.timeout is actually applied and used when +creating WebDriver sessions. Similar to Java tests that validate timeout behavior. +""" + +from unittest.mock import Mock, patch, MagicMock +from urllib3.exceptions import ConnectTimeoutError, ReadTimeoutError + +import pytest + +from selenium.webdriver.chromium.options import ChromiumOptions +from selenium.webdriver.chromium.service import ChromiumService +from selenium.webdriver.chromium.webdriver import ChromiumDriver +from selenium.webdriver.remote.client_config import ClientConfig +from selenium.common.exceptions import SessionNotCreatedException, TimeoutException + + +@pytest.fixture +def mock_service(): + """Mock ChromiumService for testing.""" + service = Mock(spec=ChromiumService) + service.service_url = "http://localhost:9515" + return service + + +@pytest.fixture +def chromium_options(): + """Create ChromiumOptions for testing.""" + options = ChromiumOptions() + return options + + +class TestClientConfigTimeout: + """Functional tests for ClientConfig timeout application.""" + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_is_used_for_connection( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that ClientConfig timeout is used when creating RemoteConnection.""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + # Create a mock instance of RemoteConnection + mock_connection_instance = MagicMock() + mock_remote_connection_class.return_value = mock_connection_instance + + custom_timeout = 25.5 + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + timeout=custom_timeout, + ) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert - Verify RemoteConnection was created with correct timeout + assert mock_remote_connection_class.called + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == custom_timeout + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_zero_would_cause_immediate_timeout( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that timeout=0 in ClientConfig is preserved (would cause immediate timeout). + + This simulates Java behavior where timeout=0 causes SessionNotCreatedException. + In practice, this would fail immediately when trying to connect to the service. + """ + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + timeout=0, # Zero timeout - should cause immediate timeout + ) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert - Verify timeout=0 is preserved in config + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == 0 # Must be preserved exactly + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_default_client_config_timeout_is_120_seconds( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that default timeout is 120 seconds when not specified.""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + # Act - No client_config provided + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + ) + + # Assert + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == 120 # Default timeout + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_overrides_default( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that explicit timeout in ClientConfig overrides default.""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + explicit_timeout = 45 + client_config = ClientConfig(timeout=explicit_timeout) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == explicit_timeout + assert actual_config.timeout != 120 # Not the default + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_with_large_value( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that large timeout values are preserved.""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + large_timeout = 300 # 5 minutes + client_config = ClientConfig(timeout=large_timeout) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == large_timeout + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_with_small_fractional_value( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that fractional timeout values (e.g., for quick fail scenarios) are preserved.""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + fractional_timeout = 0.5 # 500 milliseconds + client_config = ClientConfig(timeout=fractional_timeout) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == fractional_timeout + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_none_uses_default( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that None timeout in ClientConfig is converted to default (120).""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + # ClientConfig with explicit None timeout (or not set) + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + # timeout not set, defaults to None in ClientConfig + ) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + # When driver normalizes config without remote_server_addr, it uses default timeout=120 + # if the client_config didn't specify a timeout + assert actual_config.timeout is None or actual_config.timeout == 120 + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_multiple_drivers_with_different_timeouts( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that different drivers can have different timeouts via ClientConfig.""" + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + timeout_1 = 30 + timeout_2 = 60 + + config_1 = ClientConfig(timeout=timeout_1) + config_2 = ClientConfig(timeout=timeout_2) + + # Act - Create first driver + with patch.object(mock_service, "start"): + driver_1 = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=config_1, + ) + + # Get timeout from first driver call + first_call_kwargs = mock_remote_connection_class.call_args_list[0][1] + first_config = first_call_kwargs["client_config"] + + # Act - Create second driver with different timeout + with patch.object(mock_service, "start"): + driver_2 = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=config_2, + ) + + # Get timeout from second driver call + second_call_kwargs = mock_remote_connection_class.call_args_list[1][1] + second_config = second_call_kwargs["client_config"] + + # Assert + assert first_config.timeout == timeout_1 + assert second_config.timeout == timeout_2 + assert first_config.timeout != second_config.timeout + + driver_1.quit() + driver_2.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_timeout_preserved_through_normalization( + self, mock_remote_connection_class, mock_finder, mock_service, chromium_options + ): + """Test that timeout is preserved even when ClientConfig is normalized. + + When remote_server_addr is None, ChromiumDriver normalizes it but must + preserve the timeout from the original ClientConfig. + """ + # Arrange + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + custom_timeout = 75 + # Create config without remote_server_addr - will be normalized + client_config = ClientConfig( + timeout=custom_timeout, + keep_alive=False, + ) + + # Act + with patch.object(mock_service, "start"): + driver = ChromiumDriver( + service=mock_service, + options=chromium_options, + client_config=client_config, + ) + + # Assert - timeout should be preserved after normalization + call_kwargs = mock_remote_connection_class.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.timeout == custom_timeout + assert actual_config.remote_server_addr == mock_service.service_url + + driver.quit() diff --git a/py/test/unit/selenium/webdriver/chrome/chrome_webdriver_tests.py b/py/test/unit/selenium/webdriver/chrome/chrome_webdriver_tests.py new file mode 100644 index 0000000000000..190a79138f356 --- /dev/null +++ b/py/test/unit/selenium/webdriver/chrome/chrome_webdriver_tests.py @@ -0,0 +1,193 @@ +# 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. + +"""Unit tests for ChromeDriver ClientConfig support.""" + +from unittest.mock import Mock, patch + +import pytest + +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.webdriver import WebDriver as ChromeDriver +from selenium.webdriver.remote.client_config import ClientConfig + + +@pytest.fixture +def mock_chrome_service(): + """Mock ChromeService for testing.""" + service = Mock(spec=Service) + service.service_url = "http://localhost:9515" + return service + + +@pytest.fixture +def chrome_options(): + """Create ChromeOptions for testing.""" + options = Options() + return options + + +class TestChromeDriverClientConfig: + """Test cases for ChromeDriver ClientConfig support.""" + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chrome_driver_accepts_client_config( + self, mock_remote_connection, mock_finder, mock_chrome_service, chrome_options + ): + """Test that ChromeDriver accepts ClientConfig parameter.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/chromedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_chrome_service, "start"): + driver = ChromeDriver( + service=mock_chrome_service, + options=chrome_options, + client_config=client_config, + ) + + assert mock_remote_connection.called + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert isinstance(actual_config, ClientConfig) + assert actual_config.remote_server_addr == "http://localhost:9515" + assert actual_config.keep_alive is True + assert actual_config.timeout == 30 + + driver.quit() + + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chrome_driver_passes_client_config_to_parent( + self, mock_remote_connection, mock_finder, mock_chrome_service, chrome_options + ): + """Test that ChromeDriver properly passes ClientConfig to ChromiumDriver.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/chromedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + timeout=45, + user_agent="Chrome/90.0", + ) + + with patch.object(mock_chrome_service, "start"): + driver = ChromeDriver( + service=mock_chrome_service, + options=chrome_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == "http://localhost:9515" + assert actual_config.keep_alive is True + assert actual_config.timeout == 45 + assert actual_config.user_agent == "Chrome/90.0" + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chrome_driver_creates_default_client_config( + self, mock_remote_connection, mock_finder, mock_chrome_service, chrome_options + ): + """Test that ChromeDriver creates default ClientConfig when not provided.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/chromedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_chrome_service, "start"): + driver = ChromeDriver( + service=mock_chrome_service, + options=chrome_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert isinstance(client_config, ClientConfig) + assert client_config.remote_server_addr == mock_chrome_service.service_url + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chrome_driver_goog_vendor_prefix_set( + self, mock_remote_connection, mock_finder, mock_chrome_service, chrome_options + ): + """Test that Chrome sets correct vendor prefix.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/chromedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_chrome_service, "start"): + driver = ChromeDriver( + service=mock_chrome_service, + options=chrome_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["vendor_prefix"] == "goog" + + driver.quit() diff --git a/py/test/unit/selenium/webdriver/chromium/webdriver_tests.py b/py/test/unit/selenium/webdriver/chromium/webdriver_tests.py new file mode 100644 index 0000000000000..6f8e4e5c9ec2b --- /dev/null +++ b/py/test/unit/selenium/webdriver/chromium/webdriver_tests.py @@ -0,0 +1,407 @@ +# 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. + +"""Unit tests for ChromiumDriver ClientConfig support.""" + +from unittest.mock import Mock, patch + +import pytest + +from selenium.webdriver.chromium.options import ChromiumOptions +from selenium.webdriver.chromium.service import ChromiumService +from selenium.webdriver.chromium.webdriver import ChromiumDriver +from selenium.webdriver.remote.client_config import ClientConfig + + +@pytest.fixture +def mock_driver_service(): + """Mock ChromiumService for testing.""" + service = Mock(spec=ChromiumService) + service.service_url = "http://localhost:9515" + return service + + +@pytest.fixture +def driver_options(): + """Create ChromiumOptions for testing.""" + options = ChromiumOptions() + return options + + +class TestChromiumDriverClientConfig: + """Test cases for ChromiumDriver ClientConfig initialization and usage.""" + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_accepts_client_config( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that ChromiumDriver accepts ClientConfig parameter.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + client_config=client_config, + ) + + assert mock_remote_connection.called + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["client_config"].__dict__ == client_config.__dict__ + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_creates_default_client_config_when_not_provided( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that ChromiumDriver creates default ClientConfig when not provided.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + keep_alive=True, + ) + + assert mock_remote_connection.called + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert isinstance(client_config, ClientConfig) + assert client_config.remote_server_addr == mock_driver_service.service_url + assert client_config.keep_alive is True + assert client_config.timeout == 120 + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_normalizes_remote_server_addr_from_service( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that ChromiumDriver normalizes remote_server_addr from service URL.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == mock_driver_service.service_url + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_client_config_takes_precedence_over_keep_alive_parameter( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that ClientConfig settings take precedence over individual parameters.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=False, # Explicitly set to False + timeout=60, + ) + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + keep_alive=True, # Trying to override with True + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.keep_alive is False + assert actual_config.timeout == 60 + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_preserves_client_config_attributes( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that all ClientConfig attributes are preserved when normalized.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + keep_alive=False, + timeout=45, + ignore_certificates=True, + user_agent="CustomAgent/1.0", + websocket_timeout=15, + websocket_interval=0.05, + ) + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == mock_driver_service.service_url + assert actual_config.keep_alive is False + assert actual_config.timeout == 45 + assert actual_config.ignore_certificates is True + assert actual_config.user_agent == "CustomAgent/1.0" + assert actual_config.websocket_timeout == 15 + assert actual_config.websocket_interval == 0.05 + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_passes_all_client_config_fields_to_remote_connection( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that all ClientConfig fields are passed to RemoteConnection.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + from selenium.webdriver.remote.client_config import AuthType + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + timeout=30, + ignore_certificates=False, + username="testuser", + password="testpass", + auth_type=AuthType.BASIC, + ca_certs="/path/to/certs", + user_agent="TestAgent", + websocket_timeout=10, + websocket_interval=0.1, + ) + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == "http://localhost:9515" + assert actual_config.keep_alive is True + assert actual_config.timeout == 30 + assert actual_config.ignore_certificates is False + assert actual_config.username == "testuser" + assert actual_config.password == "testpass" + assert actual_config.auth_type == AuthType.BASIC + assert actual_config.ca_certs == "/path/to/certs" + assert actual_config.user_agent == "TestAgent" + assert actual_config.websocket_timeout == 10 + assert actual_config.websocket_interval == 0.1 + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_browser_name_and_vendor_prefix_passed_correctly( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that browser_name and vendor_prefix are passed to RemoteConnection.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + ) + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + browser_name="chrome", + vendor_prefix="goog", + service=mock_driver_service, + options=driver_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["browser_name"] == "chrome" + assert call_kwargs["vendor_prefix"] == "goog" + assert call_kwargs["client_config"].__dict__ == client_config.__dict__ + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_default_keep_alive_is_true( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that default keep_alive parameter is True.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert client_config.keep_alive is True + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_chromium_driver_client_config_none_creates_from_params( + self, mock_remote_connection, mock_finder, mock_driver_service, driver_options + ): + """Test that None client_config uses keep_alive parameter.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/driver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "chrome", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_driver_service, "start"): + driver = ChromiumDriver( + service=mock_driver_service, + options=driver_options, + keep_alive=False, + client_config=None, + ) + + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert client_config.keep_alive is False + assert client_config.timeout == 120 # Default timeout + + driver.quit() diff --git a/py/test/unit/selenium/webdriver/edge/edge_webdriver_tests.py b/py/test/unit/selenium/webdriver/edge/edge_webdriver_tests.py new file mode 100644 index 0000000000000..25bcfc84967a3 --- /dev/null +++ b/py/test/unit/selenium/webdriver/edge/edge_webdriver_tests.py @@ -0,0 +1,188 @@ +# 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. + +"""Unit tests for EdgeDriver ClientConfig support.""" + +from unittest.mock import Mock, patch + +import pytest + +from selenium.webdriver.edge.options import Options +from selenium.webdriver.edge.service import Service +from selenium.webdriver.edge.webdriver import WebDriver as EdgeDriver +from selenium.webdriver.remote.client_config import ClientConfig + + +@pytest.fixture +def mock_edge_service(): + """Mock EdgeService for testing.""" + service = Mock(spec=Service) + service.service_url = "http://localhost:9515" + return service + + +@pytest.fixture +def edge_options(): + """Create EdgeOptions for testing.""" + options = Options() + return options + + +class TestEdgeDriverClientConfig: + """Test cases for EdgeDriver ClientConfig support.""" + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_edge_driver_accepts_client_config( + self, mock_remote_connection, mock_finder, mock_edge_service, edge_options + ): + """Test that EdgeDriver accepts ClientConfig parameter.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/msedgedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "msedge", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_edge_service, "start"): + driver = EdgeDriver( + service=mock_edge_service, + options=edge_options, + client_config=client_config, + ) + + assert mock_remote_connection.called + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["client_config"].__dict__ == client_config.__dict__ + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_edge_driver_passes_client_config_to_parent( + self, mock_remote_connection, mock_finder, mock_edge_service, edge_options + ): + """Test that EdgeDriver properly passes ClientConfig to ChromiumDriver.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/msedgedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "msedge", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:9515", + keep_alive=True, + timeout=45, + user_agent="Edge/90.0", + ) + + with patch.object(mock_edge_service, "start"): + driver = EdgeDriver( + service=mock_edge_service, + options=edge_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == "http://localhost:9515" + assert actual_config.keep_alive is True + assert actual_config.timeout == 45 + assert actual_config.user_agent == "Edge/90.0" + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_edge_driver_creates_default_client_config( + self, mock_remote_connection, mock_finder, mock_edge_service, edge_options + ): + """Test that EdgeDriver creates default ClientConfig when not provided.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/msedgedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "msedge", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_edge_service, "start"): + driver = EdgeDriver( + service=mock_edge_service, + options=edge_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert isinstance(client_config, ClientConfig) + assert client_config.remote_server_addr == mock_edge_service.service_url + + driver.quit() + + @patch("selenium.webdriver.chromium.webdriver.DriverFinder") + @patch("selenium.webdriver.chromium.webdriver.ChromiumRemoteConnection") + def test_edge_driver_ms_vendor_prefix_set( + self, mock_remote_connection, mock_finder, mock_edge_service, edge_options + ): + """Test that Edge sets correct vendor prefix.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/msedgedriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "msedge", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_edge_service, "start"): + driver = EdgeDriver( + service=mock_edge_service, + options=edge_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["vendor_prefix"] == "ms" + + driver.quit() diff --git a/py/test/unit/selenium/webdriver/firefox/firefox_webdriver_tests.py b/py/test/unit/selenium/webdriver/firefox/firefox_webdriver_tests.py new file mode 100644 index 0000000000000..206770e47bc05 --- /dev/null +++ b/py/test/unit/selenium/webdriver/firefox/firefox_webdriver_tests.py @@ -0,0 +1,191 @@ +# 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. + +"""Unit tests for FirefoxDriver ClientConfig support.""" + +from unittest.mock import Mock, patch + +import pytest + +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.firefox.service import Service +from selenium.webdriver.firefox.webdriver import WebDriver as FirefoxDriver +from selenium.webdriver.remote.client_config import ClientConfig + + +@pytest.fixture +def mock_firefox_service(): + """Mock FirefoxService for testing.""" + service = Mock(spec=Service) + service.service_url = "http://localhost:4444" + return service + + +@pytest.fixture +def firefox_options(): + """Create FirefoxOptions for testing.""" + options = Options() + return options + + +class TestFirefoxDriverClientConfig: + """Test cases for FirefoxDriver ClientConfig support.""" + + @patch("selenium.webdriver.firefox.webdriver.DriverFinder") + @patch("selenium.webdriver.firefox.webdriver.FirefoxRemoteConnection") + def test_firefox_driver_accepts_client_config( + self, mock_remote_connection, mock_finder, mock_firefox_service, firefox_options + ): + """Test that FirefoxDriver accepts ClientConfig parameter.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/geckodriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "firefox", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:4444", + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_firefox_service, "start"): + driver = FirefoxDriver( + service=mock_firefox_service, + options=firefox_options, + client_config=client_config, + ) + + assert mock_remote_connection.called + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["client_config"].__dict__ == client_config.__dict__ + + driver.quit() + + @patch("selenium.webdriver.firefox.webdriver.DriverFinder") + @patch("selenium.webdriver.firefox.webdriver.FirefoxRemoteConnection") + def test_firefox_driver_passes_client_config( + self, mock_remote_connection, mock_finder, mock_firefox_service, firefox_options + ): + """Test that FirefoxDriver properly passes ClientConfig to connection.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/geckodriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "firefox", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:4444", + keep_alive=False, + timeout=60, + websocket_timeout=20, + ) + + with patch.object(mock_firefox_service, "start"): + driver = FirefoxDriver( + service=mock_firefox_service, + options=firefox_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["client_config"].__dict__ == client_config.__dict__ + + driver.quit() + + @patch("selenium.webdriver.firefox.webdriver.DriverFinder") + @patch("selenium.webdriver.firefox.webdriver.FirefoxRemoteConnection") + def test_firefox_driver_creates_default_client_config( + self, mock_remote_connection, mock_finder, mock_firefox_service, firefox_options + ): + """Test that FirefoxDriver creates default ClientConfig when not provided.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/geckodriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "firefox", + "browserVersion": "91.0" + } + } + } + + with patch.object(mock_firefox_service, "start"): + driver = FirefoxDriver( + service=mock_firefox_service, + options=firefox_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert isinstance(client_config, ClientConfig) + assert client_config.remote_server_addr == mock_firefox_service.service_url + + driver.quit() + + @patch("selenium.webdriver.firefox.webdriver.DriverFinder") + @patch("selenium.webdriver.firefox.webdriver.FirefoxRemoteConnection") + def test_firefox_driver_normalizes_remote_server_addr_from_service( + self, mock_remote_connection, mock_finder, mock_firefox_service, firefox_options + ): + """Test that FirefoxDriver normalizes remote_server_addr from service URL.""" + mock_finder.return_value.get_browser_path.return_value = None + mock_finder.return_value.get_driver_path.return_value = "/path/to/geckodriver" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "firefox", + "browserVersion": "91.0" + } + } + } + + client_config = ClientConfig( + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_firefox_service, "start"): + driver = FirefoxDriver( + service=mock_firefox_service, + options=firefox_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == mock_firefox_service.service_url + + driver.quit() \ No newline at end of file diff --git a/py/test/unit/selenium/webdriver/ie/ie_webdriver_tests.py b/py/test/unit/selenium/webdriver/ie/ie_webdriver_tests.py new file mode 100644 index 0000000000000..397550784818e --- /dev/null +++ b/py/test/unit/selenium/webdriver/ie/ie_webdriver_tests.py @@ -0,0 +1,192 @@ +# 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. + +"""Unit tests for IE WebDriver ClientConfig support.""" + +from unittest.mock import Mock, patch + +import pytest + +from selenium.webdriver.ie.options import Options +from selenium.webdriver.ie.service import Service +from selenium.webdriver.ie.webdriver import WebDriver as IEDriver +from selenium.webdriver.remote.client_config import ClientConfig + + +@pytest.fixture +def mock_ie_service(): + """Mock IEServerDriver service for testing.""" + service = Mock(spec=Service) + service.service_url = "http://localhost:5555" + return service + + +@pytest.fixture +def ie_options(): + """Create IEOptions for testing.""" + options = Options() + return options + + +class TestIEDriverClientConfig: + """Test cases for IE Driver ClientConfig support.""" + + @patch("selenium.webdriver.ie.webdriver.DriverFinder") + @patch("selenium.webdriver.ie.webdriver.RemoteConnection") + def test_ie_driver_accepts_client_config( + self, mock_remote_connection, mock_finder, mock_ie_service, ie_options + ): + """Test that IE Driver accepts ClientConfig parameter.""" + mock_finder.return_value.get_driver_path.return_value = "/path/to/IEDriverServer" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "internet explorer", + "browserVersion": "11.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:5555", + keep_alive=True, + timeout=30, + ) + + with patch.object(mock_ie_service, "start"): + driver = IEDriver( + service=mock_ie_service, + options=ie_options, + client_config=client_config, + ) + + assert mock_remote_connection.called + call_kwargs = mock_remote_connection.call_args[1] + assert call_kwargs["client_config"].__dict__ == client_config.__dict__ + + driver.quit() + + @patch("selenium.webdriver.ie.webdriver.DriverFinder") + @patch("selenium.webdriver.ie.webdriver.RemoteConnection") + def test_ie_driver_passes_client_config( + self, mock_remote_connection, mock_finder, mock_ie_service, ie_options + ): + """Test that IE Driver properly passes ClientConfig to RemoteConnection.""" + mock_finder.return_value.get_driver_path.return_value = "/path/to/IEDriverServer" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "internet explorer", + "browserVersion": "11.0" + } + } + } + + client_config = ClientConfig( + remote_server_addr="http://localhost:5555", + keep_alive=False, + timeout=60, + ignore_certificates=True, + ) + + with patch.object(mock_ie_service, "start"): + driver = IEDriver( + service=mock_ie_service, + options=ie_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == "http://localhost:5555" + assert actual_config.keep_alive is False + assert actual_config.timeout == 60 + assert actual_config.ignore_certificates is True + + driver.quit() + + @patch("selenium.webdriver.ie.webdriver.DriverFinder") + @patch("selenium.webdriver.ie.webdriver.RemoteConnection") + def test_ie_driver_creates_default_client_config( + self, mock_remote_connection, mock_finder, mock_ie_service, ie_options + ): + """Test that IE Driver creates default ClientConfig when not provided.""" + mock_finder.return_value.get_driver_path.return_value = "/path/to/IEDriverServer" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "internet explorer", + "browserVersion": "11.0" + } + } + } + + with patch.object(mock_ie_service, "start"): + driver = IEDriver( + service=mock_ie_service, + options=ie_options, + ) + + call_kwargs = mock_remote_connection.call_args[1] + client_config = call_kwargs["client_config"] + assert isinstance(client_config, ClientConfig) + assert client_config.remote_server_addr == mock_ie_service.service_url + + driver.quit() + + @patch("selenium.webdriver.ie.webdriver.DriverFinder") + @patch("selenium.webdriver.ie.webdriver.RemoteConnection") + def test_ie_driver_normalizes_remote_server_addr_from_service( + self, mock_remote_connection, mock_finder, mock_ie_service, ie_options + ): + """Test that IE Driver normalizes remote_server_addr from service URL.""" + mock_finder.return_value.get_driver_path.return_value = "/path/to/IEDriverServer" + + mock_remote_connection.return_value.execute.return_value = { + "value": { + "sessionId": "test-session-id", + "capabilities": { + "browserName": "internet explorer", + "browserVersion": "11.0" + } + } + } + + client_config = ClientConfig( + keep_alive=True, + timeout=30, + remote_server_addr=None, + ) + + with patch.object(mock_ie_service, "start"): + driver = IEDriver( + service=mock_ie_service, + options=ie_options, + client_config=client_config, + ) + + call_kwargs = mock_remote_connection.call_args[1] + actual_config = call_kwargs["client_config"] + assert actual_config.remote_server_addr == mock_ie_service.service_url + + driver.quit()