Skip to content

Conversation

@iampopovich
Copy link
Contributor

@iampopovich iampopovich commented Nov 9, 2025

User description

🔗 Related Issues

relates to #16282

💥 What does this PR do?

This pull request introduces support for advanced HTTP and WebSocket configuration in all major Selenium WebDriver classes through a new client_config parameter. The change allows users to pass a ClientConfig instance for fine-grained control over connection settings, which takes precedence over legacy parameters like keep_alive. The constructors for Chrome, Edge, Firefox, Internet Explorer, and Safari drivers are updated to accept and normalize this new configuration, ensuring consistent behavior and clear documentation for users.

Major feature: Advanced connection configuration

  • Added an optional client_config parameter (type: ClientConfig) to the constructors of all major WebDriver classes (chrome, edge, firefox, ie, safari), enabling advanced HTTP/WebSocket settings. The parameter is documented to take precedence over keep_alive and other legacy options, and example usage is provided in each class docstring. [1] [2] [3] [4] [5]

  • For each driver, the constructor now normalizes client_config by always setting remote_server_addr to the local driver service URL. If a client_config is provided by the user, its fields are copied and merged with the service URL, ensuring local drivers always connect to the correct endpoint. [1] [2] [3] [4]

Integration and documentation

  • The new client_config parameter is passed through to the corresponding remote connection classes (ChromiumRemoteConnection, FirefoxRemoteConnection, SafariRemoteConnection, RemoteConnection), allowing these classes to use the advanced configuration for HTTP and WebSocket connections. [1] [2] [3] [4]

  • Imports for ClientConfig are added to all affected driver files to support the new parameter. [1] [2] [3] [4] [5]

  • Docstrings for all affected constructors are updated to clearly explain the purpose and precedence of client_config, including usage examples for both default and custom configurations. [1] [2] [3] [4] [5]

🔧 Implementation Notes

💡 Additional Considerations

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)

PR Type

Enhancement


Description

  • Added client_config parameter to all major WebDriver classes

  • Enables advanced HTTP/WebSocket configuration for local drivers

  • Normalizes remote_server_addr to service URL for local drivers

  • Comprehensive unit tests for ClientConfig support across drivers


Diagram Walkthrough

flowchart LR
  A["WebDriver Classes<br/>Chrome, Edge, Firefox, IE, Safari"] -->|"accept client_config"| B["ClientConfig Instance"]
  B -->|"normalize remote_server_addr"| C["Service URL"]
  B -->|"preserve user settings"| D["HTTP/WebSocket Config"]
  C --> E["RemoteConnection"]
  D --> E
  E -->|"establish connection"| F["Local Driver Service"]
Loading

File Walkthrough

Relevant files
Enhancement
7 files
webdriver.py
Add ClientConfig parameter to Chrome WebDriver                     
+20/-0   
webdriver.py
Implement ClientConfig normalization in ChromiumDriver     
+33/-0   
webdriver.py
Add ClientConfig parameter to Edge WebDriver                         
+17/-1   
webdriver.py
Add ClientConfig parameter to Firefox WebDriver                   
+44/-0   
webdriver.py
Add ClientConfig parameter to IE WebDriver                             
+42/-1   
webdriver.py
Add ClientConfig parameter to Safari WebDriver                     
+45/-1   
client_config.py
Make remote_server_addr optional in ClientConfig                 
+1/-1     
Tests
6 files
webdriver_timeout_test.py
Add functional tests for ClientConfig timeout handling     
+349/-0 
chrome_webdriver_tests.py
Add unit tests for Chrome ClientConfig support                     
+193/-0 
webdriver_tests.py
Add comprehensive unit tests for ChromiumDriver ClientConfig
+407/-0 
edge_webdriver_tests.py
Add unit tests for Edge ClientConfig support                         
+188/-0 
firefox_webdriver_tests.py
Add unit tests for Firefox ClientConfig support                   
+191/-0 
ie_webdriver_tests.py
Add unit tests for IE ClientConfig support                             
+192/-0 

@selenium-ci selenium-ci added the C-py Python Bindings label Nov 9, 2025
@iampopovich
Copy link
Contributor Author

iampopovich commented Nov 9, 2025

https://github.com/iampopovich/selenium/actions/runs/19208403903
The existing tests are passing. I'll study them in more detail soon and write new tests if necessary.
@cgoldberg jfyi

Add unit tests for ClientConfig support across multiple WebDriver implementations

Add unit tests for ClientConfig support in Firefox, IE, and Chrome WebDrivers

- Implemented unit tests for FirefoxDriver to validate ClientConfig handling.
- Added tests for IE WebDriver to ensure proper ClientConfig acceptance and passing.
- Created tests for Chrome WebDriver to verify ClientConfig integration.
- All tests check for correct handling of remote server address, keep-alive settings, and timeouts.
- Fixed assertion issues in tests to compare ClientConfig attributes correctly.
@iampopovich
Copy link
Contributor Author

@iampopovich iampopovich marked this pull request as ready for review November 12, 2025 13:40
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Nov 12, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logging: The new client configuration handling and driver instantiation add no audit trail for
critical actions like service start and connection setup, but this library code may
intentionally avoid logging.

Referred Code
# Normalize client_config: always set remote_server_addr to service.service_url
# for local drivers (it cannot be overridden by user for local drivers)
if client_config is None:
    client_config = ClientConfig(
        remote_server_addr=self.service.service_url,
        keep_alive=keep_alive,
        timeout=120,
    )
else:
    # Always create new ClientConfig with service.service_url and copy all user fields
    client_config = ClientConfig(
        remote_server_addr=self.service.service_url,
        keep_alive=client_config.keep_alive,
        proxy=client_config.proxy,
        ignore_certificates=client_config.ignore_certificates,
        timeout=client_config.timeout,
        ca_certs=client_config.ca_certs,
        username=client_config.username,
        password=client_config.password,
        auth_type=client_config.auth_type,
        token=client_config.token,


 ... (clipped 15 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Edge cases partial: The normalization overwrites user-provided remote_server_addr without explicit validation
or warnings and relies on external connection classes for failures, which may be
acceptable but is not evident in this diff.

Referred Code
# Normalize client_config: always set remote_server_addr to service.service_url
# for local drivers (it cannot be overridden by user for local drivers)
if client_config is None:
    client_config = ClientConfig(
        remote_server_addr=self.service.service_url,
        keep_alive=keep_alive,
        timeout=120,
    )
else:
    # Always create new ClientConfig with service.service_url and copy all user fields
    client_config = ClientConfig(
        remote_server_addr=self.service.service_url,
        keep_alive=client_config.keep_alive,
        proxy=client_config.proxy,
        ignore_certificates=client_config.ignore_certificates,
        timeout=client_config.timeout,
        ca_certs=client_config.ca_certs,
        username=client_config.username,
        password=client_config.password,
        auth_type=client_config.auth_type,
        token=client_config.token,


 ... (clipped 5 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Optional endpoint: Making remote_server_addr optional shifts validation to later stages; while local drivers
set it, ensuring it is non-empty for all paths is not fully verifiable from this diff.

Referred Code
def __init__(
    self,
    remote_server_addr: Optional[str] = None,
    keep_alive: Optional[bool] = True,
    proxy: Optional[Proxy] = Proxy(raw={"proxyType": ProxyType.SYSTEM}),
    ignore_certificates: Optional[bool] = False,
    init_args_for_pool_manager: Optional[dict] = None,

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Nov 12, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Refactor duplicated and brittle code
Suggestion Impact:The commit removes the duplicated/manual ClientConfig reconstruction in ChromiumDriver and Firefox WebDriver and replaces it with a shared helper (normalize_local_driver_config). It also imports and uses this utility in both files, aligning with the proposed refactor to reduce brittleness.

code diff:

 from selenium.webdriver.chromium.options import ChromiumOptions
 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.common.webdriver import LocalWebDriver
 from selenium.webdriver.remote.command import Command
-from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
-
-
-class ChromiumDriver(RemoteWebDriver):
+
+
+class ChromiumDriver(LocalWebDriver):
     """Control the WebDriver instance of ChromiumDriver and drive the browser."""
 
     def __init__(
         self,
-        browser_name: Optional[str] = None,
-        vendor_prefix: Optional[str] = None,
-        options: Optional[ChromiumOptions] = None,
-        service: Optional[ChromiumService] = None,
+        browser_name: str | None = None,
+        vendor_prefix: str | None = None,
+        options: ChromiumOptions | None = None,
+        service: ChromiumService | None = None,
         keep_alive: bool = True,
         client_config: Optional[ClientConfig] = None,
     ) -> None:
@@ -61,32 +61,9 @@
         self.service.path = self.service.env_path() or finder.get_driver_path()
         self.service.start()
 
-        # Normalize client_config: always set remote_server_addr to service.service_url
-        # for local drivers (it cannot be overridden by user for local drivers)
-        if client_config is None:
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=keep_alive,
-                timeout=120,
-            )
-        else:
-            # Always create new ClientConfig with service.service_url and copy all user fields
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=client_config.keep_alive,
-                proxy=client_config.proxy,
-                ignore_certificates=client_config.ignore_certificates,
-                timeout=client_config.timeout,
-                ca_certs=client_config.ca_certs,
-                username=client_config.username,
-                password=client_config.password,
-                auth_type=client_config.auth_type,
-                token=client_config.token,
-                user_agent=client_config.user_agent,
-                extra_headers=client_config.extra_headers,
-                websocket_timeout=client_config.websocket_timeout,
-                websocket_interval=client_config.websocket_interval,
-            )
+        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,
@@ -103,8 +80,6 @@
             self.quit()
             raise
 
-        self._is_remote = False
-
     def launch_app(self, id):
         """Launches Chromium app specified by id.
 
@@ -240,25 +215,3 @@
             sink_name: Name of the sink to stop the Cast session.
         """
         return self.execute("stopCasting", {"sinkName": sink_name})
-
-    def quit(self) -> None:
-        """Closes the browser and shuts down the ChromiumDriver executable."""
-        try:
-            super().quit()
-        except Exception:
-            # We don't care about the message because something probably has gone wrong
-            pass
-        finally:
-            self.service.stop()
-
-    def download_file(self, *args, **kwargs):
-        """Download file functionality is not implemented for Chromium driver."""
-        raise NotImplementedError
-
-    def get_downloadable_files(self, *args, **kwargs):
-        """Get downloadable files functionality is not implemented for Chromium driver."""
-        raise NotImplementedError
-
-    def delete_downloadable_files(self, *args, **kwargs):
-        """Delete downloadable files functionality is not implemented for Chromium driver."""
-        raise NotImplementedError

# File: py/selenium/webdriver/firefox/webdriver.py
@@ -14,23 +14,25 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+
 import base64
 import os
 import warnings
 import zipfile
 from contextlib import contextmanager
 from io import BytesIO
-from typing import Optional
 
 from selenium.webdriver.common.driver_finder import DriverFinder
+from selenium.webdriver.common.webdriver import LocalWebDriver
+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
-
-
-class WebDriver(RemoteWebDriver):
+
+
+class WebDriver(LocalWebDriver):
     """Controls the GeckoDriver and allows you to drive the browser."""
 
     CONTEXT_CHROME = "chrome"
@@ -38,8 +40,8 @@
 
     def __init__(
         self,
-        options: Optional[Options] = None,
-        service: Optional[Service] = None,
+        options: Options | None = None,
+        service: Service | None = None,
         keep_alive: bool = True,
         client_config: Optional[ClientConfig] = None,
     ) -> None:
@@ -75,32 +77,9 @@
         self.service.path = self.service.env_path() or finder.get_driver_path()
         self.service.start()
 
-        # Normalize client_config: always set remote_server_addr to service.service_url
-        # for local drivers (it cannot be overridden by user for local drivers)
-        if client_config is None:
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=keep_alive,
-                timeout=120,
-            )
-        else:
-            # Always create new ClientConfig with service.service_url and copy all user fields
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=client_config.keep_alive,
-                proxy=client_config.proxy,
-                ignore_certificates=client_config.ignore_certificates,
-                timeout=client_config.timeout,
-                ca_certs=client_config.ca_certs,
-                username=client_config.username,
-                password=client_config.password,
-                auth_type=client_config.auth_type,
-                token=client_config.token,
-                user_agent=client_config.user_agent,
-                extra_headers=client_config.extra_headers,
-                websocket_timeout=client_config.websocket_timeout,
-                websocket_interval=client_config.websocket_interval,
-            )
+        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,
@@ -114,18 +93,6 @@
         except Exception:
             self.quit()
             raise
-
-        self._is_remote = False
-
-    def quit(self) -> None:
-        """Closes the browser and shuts down the GeckoDriver executable."""
-        try:
-            super().quit()
-        except Exception:
-            # We don't care about the message because something probably has gone wrong
-            pass
-        finally:
-            self.service.stop()
 

The client_config normalization logic is duplicated and brittle across four
webdriver classes. This should be refactored into a single, shared utility
function that programmatically copies attributes to improve maintainability.

Examples:

py/selenium/webdriver/chromium/webdriver.py [66-89]
        if client_config is None:
            client_config = ClientConfig(
                remote_server_addr=self.service.service_url,
                keep_alive=keep_alive,
                timeout=120,
            )
        else:
            # Always create new ClientConfig with service.service_url and copy all user fields
            client_config = ClientConfig(
                remote_server_addr=self.service.service_url,

 ... (clipped 14 lines)
py/selenium/webdriver/firefox/webdriver.py [80-103]
        if client_config is None:
            client_config = ClientConfig(
                remote_server_addr=self.service.service_url,
                keep_alive=keep_alive,
                timeout=120,
            )
        else:
            # Always create new ClientConfig with service.service_url and copy all user fields
            client_config = ClientConfig(
                remote_server_addr=self.service.service_url,

 ... (clipped 14 lines)

Solution Walkthrough:

Before:

# In ChromiumDriver, FirefoxDriver, IEDriver, SafariDriver...

if client_config is None:
    client_config = ClientConfig(
        remote_server_addr=self.service.service_url,
        keep_alive=keep_alive,
        timeout=120,
    )
else:
    # Brittle: all attributes are manually copied
    client_config = ClientConfig(
        remote_server_addr=self.service.service_url,
        keep_alive=client_config.keep_alive,
        proxy=client_config.proxy,
        ignore_certificates=client_config.ignore_certificates,
        timeout=client_config.timeout,
        # ... and so on for all other attributes
    )

After:

# In a new shared utility function
def normalize_local_driver_config(service_url, user_config=None, **defaults):
    if user_config is None:
        return ClientConfig(remote_server_addr=service_url, **defaults)

    # Programmatically copy attributes to avoid brittleness
    config_args = vars(user_config).copy()
    config_args["remote_server_addr"] = service_url
    return ClientConfig(**config_args)

# In ChromiumDriver, FirefoxDriver, etc.
client_config = normalize_local_driver_config(
    self.service.service_url,
    user_config=client_config,
    keep_alive=keep_alive,
    timeout=120
)
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies significant code duplication and a brittle, hard-to-maintain implementation for client_config normalization across multiple files, proposing a valid refactoring that improves code quality and future maintainability.

Medium
Possible issue
Improve ClientConfig cloning to be robust
Suggestion Impact:The commit replaced the manual ClientConfig reconstruction with a utility function normalize_local_driver_config, which centralizes and likely robustly clones/normalizes the config instead of explicitly listing attributes. This addresses the brittleness highlighted by the suggestion.

code diff:

-        # Normalize client_config: always set remote_server_addr to service.service_url
-        # for local drivers (it cannot be overridden by user for local drivers)
-        if client_config is None:
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=keep_alive,
-                timeout=120,
-            )
-        else:
-            # Always create new ClientConfig with service.service_url and copy all user fields
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=client_config.keep_alive,
-                proxy=client_config.proxy,
-                ignore_certificates=client_config.ignore_certificates,
-                timeout=client_config.timeout,
-                ca_certs=client_config.ca_certs,
-                username=client_config.username,
-                password=client_config.password,
-                auth_type=client_config.auth_type,
-                token=client_config.token,
-                user_agent=client_config.user_agent,
-                extra_headers=client_config.extra_headers,
-                websocket_timeout=client_config.websocket_timeout,
-                websocket_interval=client_config.websocket_interval,
-            )
+        client_config = normalize_local_driver_config(
+            self.service.service_url, user_config=client_config, keep_alive=keep_alive, timeout=120
+        )

Refactor the manual copying of ClientConfig attributes to a more robust method
using dict to prevent missing attributes like init_args_for_pool_manager.

py/selenium/webdriver/chromium/webdriver.py [73-89]

 # Always create new ClientConfig with service.service_url and copy all user fields
-client_config = ClientConfig(
-    remote_server_addr=self.service.service_url,
-    keep_alive=client_config.keep_alive,
-    proxy=client_config.proxy,
-    ignore_certificates=client_config.ignore_certificates,
-    timeout=client_config.timeout,
-    ca_certs=client_config.ca_certs,
-    username=client_config.username,
-    password=client_config.password,
-    auth_type=client_config.auth_type,
-    token=client_config.token,
-    user_agent=client_config.user_agent,
-    extra_headers=client_config.extra_headers,
-    websocket_timeout=client_config.websocket_timeout,
-    websocket_interval=client_config.websocket_interval,
-)
+config_dict = client_config.__dict__.copy()
+config_dict["remote_server_addr"] = self.service.service_url
+client_config = ClientConfig(**config_dict)

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the manual attribute copying is brittle and misses the init_args_for_pool_manager attribute, which is a potential bug. The proposed change makes the code more robust and maintainable.

Medium
Learned
best practice
Validate client_config parameters
Suggestion Impact:The commit replaced inline client_config normalization with a call to normalize_local_driver_config, centralizing the normalization/validation logic. While the commit does not show explicit validations in this file, delegating to a normalization helper likely implements the suggested validation behavior.

code diff:

+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
@@ -31,10 +31,10 @@
 
     def __init__(
         self,
-        browser_name: Optional[str] = None,
-        vendor_prefix: Optional[str] = None,
-        options: Optional[ChromiumOptions] = None,
-        service: Optional[ChromiumService] = None,
+        browser_name: str | None = None,
+        vendor_prefix: str | None = None,
+        options: ChromiumOptions | None = None,
+        service: ChromiumService | None = None,
         keep_alive: bool = True,
         client_config: Optional[ClientConfig] = None,
     ) -> None:
@@ -61,32 +61,9 @@
         self.service.path = self.service.env_path() or finder.get_driver_path()
         self.service.start()
 
-        # Normalize client_config: always set remote_server_addr to service.service_url
-        # for local drivers (it cannot be overridden by user for local drivers)
-        if client_config is None:
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=keep_alive,
-                timeout=120,
-            )
-        else:
-            # Always create new ClientConfig with service.service_url and copy all user fields
-            client_config = ClientConfig(
-                remote_server_addr=self.service.service_url,
-                keep_alive=client_config.keep_alive,
-                proxy=client_config.proxy,
-                ignore_certificates=client_config.ignore_certificates,
-                timeout=client_config.timeout,
-                ca_certs=client_config.ca_certs,
-                username=client_config.username,
-                password=client_config.password,
-                auth_type=client_config.auth_type,
-                token=client_config.token,
-                user_agent=client_config.user_agent,
-                extra_headers=client_config.extra_headers,
-                websocket_timeout=client_config.websocket_timeout,
-                websocket_interval=client_config.websocket_interval,
-            )
+        client_config = normalize_local_driver_config(
+            self.service.service_url, user_config=client_config, keep_alive=keep_alive, timeout=120
+        )

Validate client_config fields (e.g., timeouts non-negative numbers) and raise a
clear ValueError before constructing the connection.

py/selenium/webdriver/chromium/webdriver.py [32-89]

 def __init__(
     self,
     browser_name: Optional[str] = None,
     vendor_prefix: Optional[str] = None,
     options: Optional[ChromiumOptions] = None,
     service: Optional[ChromiumService] = None,
     keep_alive: bool = True,
     client_config: Optional[ClientConfig] = None,
 ) -> None:
-...
+    ...
+    # Normalize client_config and validate
     if client_config is None:
         client_config = ClientConfig(
             remote_server_addr=self.service.service_url,
             keep_alive=keep_alive,
             timeout=120,
         )
     else:
-        # Always create new ClientConfig with service.service_url and copy all user fields
+        # Basic validation for external I/O related parameters
+        if client_config.timeout is not None and client_config.timeout < 0:
+            raise ValueError("client_config.timeout must be >= 0")
+        if client_config.websocket_timeout is not None and client_config.websocket_timeout <= 0:
+            raise ValueError("client_config.websocket_timeout must be > 0")
+        if client_config.websocket_interval is not None and client_config.websocket_interval <= 0:
+            raise ValueError("client_config.websocket_interval must be > 0")
+
         client_config = ClientConfig(
             remote_server_addr=self.service.service_url,
             keep_alive=client_config.keep_alive,
             proxy=client_config.proxy,
             ignore_certificates=client_config.ignore_certificates,
             timeout=client_config.timeout,
             ca_certs=client_config.ca_certs,
             username=client_config.username,
             password=client_config.password,
             auth_type=client_config.auth_type,
             token=client_config.token,
             user_agent=client_config.user_agent,
             extra_headers=client_config.extra_headers,
             websocket_timeout=client_config.websocket_timeout,
             websocket_interval=client_config.websocket_interval,
         )

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Guard external API and I/O configuration with validation and clear errors to avoid crashes (validate constructor args and enforce reasonable types/ranges).

Low
  • Update

Copilot AI review requested due to automatic review settings December 22, 2025 20:24
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds ClientConfig support to all local WebDriver classes (Chrome, Edge, Firefox, IE, Safari), enabling advanced HTTP and WebSocket configuration for local driver connections. The implementation makes remote_server_addr optional in ClientConfig and introduces a normalization utility that automatically sets it to the local service URL.

Key Changes:

  • Made remote_server_addr parameter optional in ClientConfig class
  • Added client_config parameter to all WebDriver constructors with precedence over legacy keep_alive parameter
  • Implemented normalize_local_driver_config() utility to ensure local drivers always connect to the correct service URL
  • Added comprehensive unit and functional tests for the new functionality

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
py/selenium/webdriver/remote/client_config.py Made remote_server_addr optional to support local driver configuration
py/selenium/webdriver/common/utils.py Added normalize_local_driver_config() utility to handle ClientConfig normalization for local drivers
py/selenium/webdriver/chrome/webdriver.py Added client_config parameter with documentation and example usage
py/selenium/webdriver/edge/webdriver.py Added client_config parameter and forwarded to ChromiumDriver parent
py/selenium/webdriver/firefox/webdriver.py Added client_config parameter with normalization and connection setup
py/selenium/webdriver/ie/webdriver.py Added client_config parameter with normalization and connection setup
py/selenium/webdriver/safari/webdriver.py Added client_config parameter with normalization and connection setup
py/selenium/webdriver/chromium/webdriver.py Implemented client_config normalization in base ChromiumDriver class
py/test/unit/selenium/webdriver/chrome/chrome_webdriver_tests.py Unit tests for Chrome driver ClientConfig support
py/test/unit/selenium/webdriver/edge/edge_webdriver_tests.py Unit tests for Edge driver ClientConfig support
py/test/unit/selenium/webdriver/firefox/firefox_webdriver_tests.py Unit tests for Firefox driver ClientConfig support
py/test/unit/selenium/webdriver/ie/ie_webdriver_tests.py Unit tests for IE driver ClientConfig support
py/test/unit/selenium/webdriver/chromium/webdriver_tests.py Comprehensive unit tests for ChromiumDriver ClientConfig functionality
py/test/selenium/webdriver/chromium/webdriver_timeout_test.py Functional tests for ClientConfig timeout handling and preservation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

options: Options | None = None,
service: Service | None = None,
keep_alive: bool = True,
client_config: Optional[ClientConfig] = None,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type hint Optional[ClientConfig] is used but Optional is not imported. This will cause a NameError at runtime. Add from typing import Optional to the imports.

Copilot uses AI. Check for mistakes.
options: ChromiumOptions | None = None,
service: ChromiumService | None = None,
keep_alive: bool = True,
client_config: Optional[ClientConfig] = None,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type hint Optional[ClientConfig] is used but Optional is not imported. This will cause a NameError at runtime. Add from typing import Optional to the imports.

Copilot uses AI. Check for mistakes.

executor = SafariRemoteConnection(
remote_server_addr=self.service.service_url,
keep_alive=keep_alive,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keep_alive parameter is passed to SafariRemoteConnection even though it's already included in the client_config. This creates redundancy and could lead to confusion about which value takes precedence. Consider removing the keep_alive parameter since client_config should contain all necessary configuration.

Suggested change
keep_alive=keep_alive,

Copilot uses AI. Check for mistakes.

executor = FirefoxRemoteConnection(
remote_server_addr=self.service.service_url,
keep_alive=keep_alive,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keep_alive parameter is passed to FirefoxRemoteConnection even though it's already included in the client_config. This creates redundancy and could lead to confusion about which value takes precedence. Consider removing the keep_alive parameter since client_config should contain all necessary configuration.

Suggested change
keep_alive=keep_alive,

Copilot uses AI. Check for mistakes.
remote_server_addr=self.service.service_url,
browser_name=browser_name,
vendor_prefix=vendor_prefix,
keep_alive=keep_alive,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keep_alive parameter is passed to ChromiumRemoteConnection even though it's already included in the client_config. This creates redundancy and could lead to confusion about which value takes precedence. Consider removing the keep_alive parameter since client_config should contain all necessary configuration.

Suggested change
keep_alive=keep_alive,

Copilot uses AI. Check for mistakes.
def normalize_local_driver_config(
service_url: str, user_config: Optional[ClientConfig] = None, **defaults
) -> ClientConfig:
"""Creates a ClientConfig for local drivers."""
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks comprehensive documentation. It should clearly document that when user_config is provided, the defaults parameters are ignored and only the service_url is normalized to ensure local drivers connect to the correct endpoint. This is important for users to understand the precedence behavior.

Suggested change
"""Creates a ClientConfig for local drivers."""
"""Create a ClientConfig for local drivers, with clear precedence rules.
When ``user_config`` is not provided, a new ``ClientConfig`` is created
using ``service_url`` (as ``remote_server_addr``) and any additional
keyword arguments supplied via ``defaults``.
When ``user_config`` is provided, its existing values are copied and
only ``remote_server_addr`` is updated to the given ``service_url`` so
that local drivers connect to the correct endpoint. In this case, all
values passed via ``defaults`` are ignored.
Args:
service_url: Base URL of the local WebDriver service; normalized into
``remote_server_addr`` on the returned configuration.
user_config: Existing ``ClientConfig`` to start from. If not ``None``,
its fields are copied and only ``remote_server_addr`` is
overridden with ``service_url``.
**defaults: Additional keyword arguments used only when
``user_config`` is ``None`` to construct a new ``ClientConfig``.
Returns:
A ``ClientConfig`` instance configured for a local driver with the
precedence behavior described above.
"""

Copilot uses AI. Check for mistakes.
Comment on lines 56 to 58
config = ClientConfig(
remote_server_addr="http://localhost:9515",
websocket_timeout=10
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example shows setting remote_server_addr in the ClientConfig for a local driver, but according to the PR description and implementation, this value will be overridden by the service URL. This could mislead users into thinking they need to provide this parameter. Consider either removing it from the example or adding a comment explaining that it will be automatically set to the service URL for local drivers.

Suggested change
config = ClientConfig(
remote_server_addr="http://localhost:9515",
websocket_timeout=10
# For local drivers, remote_server_addr is automatically set from the Service URL.
config = ClientConfig(
websocket_timeout=10,

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +262
"""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
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test comment and assertion are misleading. When a ClientConfig is provided with no explicit timeout, the timeout is set to socket.getdefaulttimeout() by ClientConfig's constructor, not 120. The normalize_local_driver_config function only applies the default timeout=120 when user_config is None. In this test case, since a ClientConfig instance is provided, the timeout will be socket.getdefaulttimeout() (typically None), not 120. The assertion should only check for None or clarify the expected behavior.

Copilot uses AI. Check for mistakes.
"""

from unittest.mock import Mock, patch, MagicMock
from urllib3.exceptions import ConnectTimeoutError, ReadTimeoutError
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'ConnectTimeoutError' is not used.
Import of 'ReadTimeoutError' is not used.

Suggested change
from urllib3.exceptions import ConnectTimeoutError, ReadTimeoutError

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'SessionNotCreatedException' is not used.
Import of 'TimeoutException' is not used.

Suggested change
from selenium.common.exceptions import SessionNotCreatedException, TimeoutException

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants