Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions dotnet/src/webdriver/Chrome/ChromeDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,61 @@ public ChromeDriver(ChromeDriverService service, ChromeOptions options, TimeSpan
this.AddCustomChromeCommands();
}

/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified command executor and options.
/// </summary>
/// <param name="commandExecutor">The <see cref="ICommandExecutor"/> to use for executing commands.</param>
/// <param name="options">The <see cref="ChromeOptions"/> to use for this driver.</param>
/// <param name="autoStartSession">Whether to automatically start the session.</param>
protected ChromeDriver(ICommandExecutor commandExecutor, ChromeOptions options, bool autoStartSession)
: base(commandExecutor, options, autoStartSession)
{
if (autoStartSession)
{
this.AddCustomChromeCommands();
}
}

/// <summary>
/// Asynchronously creates and starts a new instance of the <see cref="ChromeDriver"/> class with default options.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the initialized <see cref="ChromeDriver"/>.</returns>
public static Task<ChromeDriver> StartAsync()
{
return StartAsync(new ChromeOptions());
}

/// <summary>
/// Asynchronously creates and starts a new instance of the <see cref="ChromeDriver"/> class using the specified options.
/// </summary>
/// <param name="options">The <see cref="ChromeOptions"/> to be used with the Chrome driver.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the initialized <see cref="ChromeDriver"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception>
public static async Task<ChromeDriver> StartAsync(ChromeOptions options)
Comment on lines +175 to +190
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The new ChromeDriver.StartAsync(...) entry point is user-facing behavior, but there are no tests validating it (successful startup path and failure/disposal behavior). Please add coverage in the existing .NET test suite (e.g., under dotnet/test/chrome or dotnet/test/common) to ensure StartAsync actually creates a usable session and that resources are cleaned up on exceptions.

Copilot uses AI. Check for mistakes.
{
if (options is null)
{
throw new ArgumentNullException(nameof(options), "Chrome options must not be null");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw new ArgumentNullException(nameof(options), "Chrome options must not be null");
throw new ArgumentNullException(nameof(options));

I would remove the custom exception message. The default one is fine.

}

ChromeDriverService service = ChromeDriverService.CreateDefaultService();
ICommandExecutor executor = await GenerateDriverServiceCommandExecutorAsync(service, options, DefaultCommandTimeout).ConfigureAwait(false);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

StartAsync(ChromeOptions) creates a ChromeDriverService before calling GenerateDriverServiceCommandExecutorAsync(...), but if that call throws (e.g., driver/browser discovery fails before the internal try/catch that disposes the service), the newly created service instance is never disposed. Wrap executor creation in a try/catch and dispose the service on failure (prefer DisposeAsync if appropriate) to avoid leaking resources when startup fails early.

Suggested change
ICommandExecutor executor = await GenerateDriverServiceCommandExecutorAsync(service, options, DefaultCommandTimeout).ConfigureAwait(false);
ICommandExecutor executor;
try
{
executor = await GenerateDriverServiceCommandExecutorAsync(service, options, DefaultCommandTimeout).ConfigureAwait(false);
}
catch
{
service.Dispose();
throw;
}

Copilot uses AI. Check for mistakes.

ChromeDriver driver = new(executor, options, autoStartSession: false);
driver.AddCustomChromeCommands();

try
{
await driver.StartSessionAsync(options.ToCapabilities()).ConfigureAwait(false);
return driver;
}
catch
{
driver.Dispose();
throw;
}
}

/// <summary>
/// Gets a read-only dictionary of the custom WebDriver commands defined for ChromeDriver.
/// The keys of the dictionary are the names assigned to the command; the values are the
Expand Down
24 changes: 23 additions & 1 deletion dotnet/src/webdriver/Chromium/ChromiumDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ protected ChromiumDriver(ChromiumDriverService service, ChromiumOptions options,
this.optionsCapabilityName = options.CapabilityName ?? throw new ArgumentException("No chromium options capability name specified", nameof(options));
}

/// <summary>
/// Initializes a new instance of the <see cref="ChromiumDriver"/> class using the specified command executor and options.
/// </summary>
/// <param name="commandExecutor">The <see cref="ICommandExecutor"/> to use for executing commands.</param>
/// <param name="options">The <see cref="ChromiumOptions"/> to be used with the ChromiumDriver.</param>
/// <param name="autoStartSession">Whether to automatically start the session.</param>
/// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">If the Chromium options capability name is <see langword="null"/>.</exception>
protected ChromiumDriver(ICommandExecutor commandExecutor, ChromiumOptions options, bool autoStartSession)
: base(commandExecutor, ConvertOptionsToCapabilities(options), autoStartSession)
{
this.optionsCapabilityName = options.CapabilityName ?? throw new ArgumentException("No chromium options capability name specified", nameof(options));
}

/// <summary>
/// Gets the dictionary of custom Chromium commands registered with the driver.
/// </summary>
Expand All @@ -144,7 +158,15 @@ await GenerateDriverServiceCommandExecutorAsync(service, options, commandTimeout
.GetAwaiter().GetResult();
}

private static async Task<ICommandExecutor> GenerateDriverServiceCommandExecutorAsync(DriverService service, DriverOptions options, TimeSpan commandTimeout)
/// <summary>
/// Asynchronously generates a driver service command executor.
/// </summary>
/// <param name="service">The <see cref="DriverService"/> to use.</param>
/// <param name="options">The <see cref="DriverOptions"/> to be used with the driver.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="ICommandExecutor"/>.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="service"/> or <paramref name="options"/> are <see langword="null"/>.</exception>
protected static async Task<ICommandExecutor> GenerateDriverServiceCommandExecutorAsync(DriverService service, DriverOptions options, TimeSpan commandTimeout)
{
if (service is null)
{
Expand Down
67 changes: 46 additions & 21 deletions dotnet/src/webdriver/WebDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,22 @@ public class WebDriver : IWebDriver, ISearchContext, IJavaScriptExecutor, IFinds
private readonly List<string> registeredCommands = new List<string>();

/// <summary>
/// Initializes a new instance of the <see cref="WebDriver"/> class.
/// Initializes a new instance of the <see cref="WebDriver"/> class and automatically starts the driver session.
/// </summary>
/// <param name="executor">The <see cref="ICommandExecutor"/> object used to execute commands.</param>
/// <param name="capabilities">The <see cref="ICapabilities"/> object used to configure the driver session.</param>
protected WebDriver(ICommandExecutor executor, ICapabilities capabilities)
: this(executor, capabilities, true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="WebDriver"/> class with optional automatic session initialization.
/// </summary>
/// <param name="executor">The <see cref="ICommandExecutor"/> object used to execute commands.</param>
/// <param name="capabilities">The <see cref="ICapabilities"/> object used to configure the driver session.</param>
/// <param name="autoStartSession">Whether to automatically start the driver session. When <see langword="true"/>, the session is started immediately; when <see langword="false"/>, the session must be started manually.</param>
protected WebDriver(ICommandExecutor executor, ICapabilities capabilities, bool autoStartSession)
{
this.CommandExecutor = executor;
this.elementFactory = new WebElementFactory(this);
Expand All @@ -61,22 +72,25 @@ protected WebDriver(ICommandExecutor executor, ICapabilities capabilities)
this.RegisterDriverCommand(DriverCommand.GetLog, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/se/log"), true);
}

try
{
this.StartSession(capabilities);
}
catch (Exception)
if (autoStartSession)
{
try
{
// Failed to start driver session, disposing of driver
this.Dispose();
this.StartSession(capabilities);
}
catch
catch (Exception)
{
// Ignore the clean-up exception. We'll propagate the original failure.
try
{
// Failed to start driver session, disposing of driver
this.Dispose();
}
catch
{
// Ignore the clean-up exception. We'll propagate the original failure.
}
throw;
}
throw;
}
}

Expand All @@ -88,7 +102,7 @@ protected WebDriver(ICommandExecutor executor, ICapabilities capabilities)
/// <summary>
/// Gets the <see cref="ICapabilities"/> that the driver session was created with, which may be different from those requested.
/// </summary>
public ICapabilities Capabilities { get; private set; }
public ICapabilities Capabilities { get; private set; } = null!;

/// <summary>
/// Gets or sets the URL the browser is currently displaying.
Expand Down Expand Up @@ -180,7 +194,7 @@ public ReadOnlyCollection<string> WindowHandles
/// <summary>
/// Gets the <see cref="Selenium.SessionId"/> for the current session of this driver.
/// </summary>
public SessionId SessionId { get; private set; }
public SessionId SessionId { get; private set; } = null!;

/// <summary>
/// Gets or sets the <see cref="IFileDetector"/> responsible for detecting
Expand Down Expand Up @@ -584,11 +598,20 @@ protected internal virtual async Task<Response> ExecuteAsync(string driverComman
/// Starts a session with the driver
/// </summary>
/// <param name="capabilities">Capabilities of the browser</param>
[MemberNotNull(nameof(SessionId))]
[MemberNotNull(nameof(Capabilities))]
protected void StartSession(ICapabilities capabilities)
{
Dictionary<string, object?> parameters = new Dictionary<string, object?>();
Task.Run(() => this.StartSessionAsync(capabilities)).GetAwaiter().GetResult();
}

/// <summary>
/// Asynchronously starts a session with the driver.
/// </summary>
/// <param name="capabilities">Capabilities of the browser.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <exception cref="WebDriverException">If the session cannot be started or the response is invalid.</exception>
protected async Task StartSessionAsync(ICapabilities capabilities)
{
Dictionary<string, object?> parameters = [];

// If the object passed into the RemoteWebDriver constructor is a
// RemoteSessionSettings object, it is expected that all intermediate
Expand All @@ -599,11 +622,12 @@ protected void StartSession(ICapabilities capabilities)
{
Dictionary<string, object> matchCapabilities = this.GetCapabilitiesDictionary(capabilities);

List<object> firstMatchCapabilitiesList = new List<object>();
firstMatchCapabilitiesList.Add(matchCapabilities);
List<object> firstMatchCapabilitiesList = [matchCapabilities];

Dictionary<string, object> specCompliantCapabilitiesDictionary = new Dictionary<string, object>();
specCompliantCapabilitiesDictionary["firstMatch"] = firstMatchCapabilitiesList;
Dictionary<string, object> specCompliantCapabilitiesDictionary = new()
{
["firstMatch"] = firstMatchCapabilitiesList
};

parameters.Add("capabilities", specCompliantCapabilitiesDictionary);
}
Expand All @@ -612,9 +636,10 @@ protected void StartSession(ICapabilities capabilities)
parameters.Add("capabilities", remoteSettings.ToDictionary());
}

Response response = this.Execute(DriverCommand.NewSession, parameters);
Response response = await this.ExecuteAsync(DriverCommand.NewSession, parameters).ConfigureAwait(false);

response.EnsureValueIsNotNull();

if (response.Value is not Dictionary<string, object> rawCapabilities)
{
string errorMessage = string.Format(CultureInfo.InvariantCulture, "The new session command returned a value ('{0}') that is not a valid JSON object.", response.Value);
Expand Down