diff --git a/py/selenium/webdriver/chrome/webdriver.py b/py/selenium/webdriver/chrome/webdriver.py index 747656fdb69fc..01cd5bfd8a93b 100644 --- a/py/selenium/webdriver/chrome/webdriver.py +++ b/py/selenium/webdriver/chrome/webdriver.py @@ -20,6 +20,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): @@ -30,6 +31,7 @@ def __init__( options: Options | None = None, service: Service | None = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Creates a new instance of the chrome driver. @@ -39,6 +41,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() @@ -49,4 +68,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 484fa132ad74d..426bfcd60492c 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -20,6 +20,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 @@ -34,6 +36,7 @@ def __init__( options: ChromiumOptions | None = None, service: ChromiumService | None = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Create a new WebDriver instance, start the service, and create new ChromiumDriver instance. @@ -43,6 +46,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() @@ -55,12 +61,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 52e54e63da875..03cbf0149c047 100644 --- a/py/selenium/webdriver/common/utils.py +++ b/py/selenium/webdriver/common/utils.py @@ -23,6 +23,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) @@ -163,3 +164,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 fee90b418bf53..331780aeb28a8 100644 --- a/py/selenium/webdriver/edge/webdriver.py +++ b/py/selenium/webdriver/edge/webdriver.py @@ -20,6 +20,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): @@ -30,6 +31,7 @@ def __init__( options: Options | None = None, service: Service | None = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Creates a new instance of the edge driver. @@ -40,7 +42,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() @@ -51,4 +66,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 fd953edd23e5b..cba8314acbf09 100644 --- a/py/selenium/webdriver/firefox/webdriver.py +++ b/py/selenium/webdriver/firefox/webdriver.py @@ -22,9 +22,11 @@ from io import BytesIO 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 @@ -39,6 +41,7 @@ def __init__( options: Options | None = None, service: Service | None = 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. @@ -46,6 +49,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() @@ -58,10 +75,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 47f3224e2123a..d84942f3d67c5 100644 --- a/py/selenium/webdriver/ie/webdriver.py +++ b/py/selenium/webdriver/ie/webdriver.py @@ -17,6 +17,7 @@ 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 @@ -32,6 +33,7 @@ def __init__( options: Options | None = None, service: Service | None = None, keep_alive: bool = True, + client_config: Optional[ClientConfig] = None, ) -> None: """Creates a new instance of the Ie driver. @@ -41,6 +43,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() @@ -48,7 +64,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 98019b8f15834..e866c4148fd54 100644 --- a/py/selenium/webdriver/remote/client_config.py +++ b/py/selenium/webdriver/remote/client_config.py @@ -77,7 +77,7 @@ class ClientConfig: def __init__( self, - remote_server_addr: str, + remote_server_addr: str | None = None, keep_alive: bool | None = True, proxy: Proxy | None = Proxy(raw={"proxyType": ProxyType.SYSTEM}), ignore_certificates: bool | None = False, @@ -91,7 +91,7 @@ def __init__( user_agent: str | None = None, extra_headers: dict | None = None, websocket_timeout: float | None = 30.0, - websocket_interval: float | None = 0.1, + websocket_interval: float | None = 0.1 ) -> None: self.remote_server_addr = remote_server_addr self.keep_alive = keep_alive diff --git a/py/selenium/webdriver/safari/webdriver.py b/py/selenium/webdriver/safari/webdriver.py index 8f17758f1b96d..fe27c2b8ed7b5 100644 --- a/py/selenium/webdriver/safari/webdriver.py +++ b/py/selenium/webdriver/safari/webdriver.py @@ -18,6 +18,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 @@ -29,17 +31,32 @@ class WebDriver(RemoteWebDriver): def __init__( self, - keep_alive=True, + keep_alive: bool = True, options: Options | None = None, service: Service | None = None, + client_config: ClientConfig | None = 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() @@ -49,10 +66,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()