Skip to content

Instantly share code, notes, and snippets.

@spinylobster
Created April 10, 2022 09:08
Show Gist options
  • Save spinylobster/d3683002ef96b0ee52dc59ca385ee2c0 to your computer and use it in GitHub Desktop.
Save spinylobster/d3683002ef96b0ee52dc59ca385ee2c0 to your computer and use it in GitHub Desktop.
// <copyright file="ChromeDriver.cs" company="WebDriver Committers">
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Chromium;
namespace OpenQA.Selenium.Chrome
{
/// <summary>
/// Provides a mechanism to write tests against Chrome
/// </summary>
/// <example>
/// <code>
/// [TestFixture]
/// public class Testing
/// {
/// private IWebDriver driver;
/// <para></para>
/// [SetUp]
/// public void SetUp()
/// {
/// driver = new ChromeDriver();
/// }
/// <para></para>
/// [Test]
/// public void TestGoogle()
/// {
/// driver.Navigate().GoToUrl("http://www.google.co.uk");
/// /*
/// * Rest of the test
/// */
/// }
/// <para></para>
/// [TearDown]
/// public void TearDown()
/// {
/// driver.Quit();
/// }
/// }
/// </code>
/// </example>
public class MyChromeDriver : MyChromiumDriver
{
private static Dictionary<string, CommandInfo> chromeCustomCommands = new Dictionary<string, CommandInfo>()
{
{ ExecuteCdp, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/goog/cdp/execute") },
{ GetCastSinksCommand, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/goog/cast/get_sinks") },
{ SelectCastSinkCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/goog/cast/set_sink_to_use") },
{ StartCastTabMirroringCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/goog/cast/start_tab_mirroring") },
{ StartCastDesktopMirroringCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/goog/cast/start_desktop_mirroring") },
{ GetCastIssueMessageCommand, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/goog/cast/get_issue_message") },
{ StopCastingCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/goog/cast/stop_casting") }
};
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class.
/// </summary>
public MyChromeDriver()
: this(new ChromeOptions())
{
}
/// <summary>
/// Initializes 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>
public MyChromeDriver(ChromeOptions options)
: this(ChromeDriverService.CreateDefaultService(), options, RemoteWebDriver.DefaultCommandTimeout)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified driver service.
/// </summary>
/// <param name="service">The <see cref="ChromeDriverService"/> used to initialize the driver.</param>
public MyChromeDriver(ChromeDriverService service)
: this(service, new ChromeOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified path
/// to the directory containing ChromeDriver.exe.
/// </summary>
/// <param name="chromeDriverDirectory">The full path to the directory containing ChromeDriver.exe.</param>
public MyChromeDriver(string chromeDriverDirectory)
: this(chromeDriverDirectory, new ChromeOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified path
/// to the directory containing ChromeDriver.exe and options.
/// </summary>
/// <param name="chromeDriverDirectory">The full path to the directory containing ChromeDriver.exe.</param>
/// <param name="options">The <see cref="ChromeOptions"/> to be used with the Chrome driver.</param>
public MyChromeDriver(string chromeDriverDirectory, ChromeOptions options)
: this(chromeDriverDirectory, options, RemoteWebDriver.DefaultCommandTimeout)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified path
/// to the directory containing ChromeDriver.exe, options, and command timeout.
/// </summary>
/// <param name="chromeDriverDirectory">The full path to the directory containing ChromeDriver.exe.</param>
/// <param name="options">The <see cref="ChromeOptions"/> to be used with the Chrome driver.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
public MyChromeDriver(string chromeDriverDirectory, ChromeOptions options, TimeSpan commandTimeout)
: this(ChromeDriverService.CreateDefaultService(chromeDriverDirectory), options, commandTimeout)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified
/// <see cref="ChromeDriverService"/> and options.
/// </summary>
/// <param name="service">The <see cref="ChromeDriverService"/> to use.</param>
/// <param name="options">The <see cref="ChromeOptions"/> used to initialize the driver.</param>
public MyChromeDriver(ChromeDriverService service, ChromeOptions options)
: this(service, options, RemoteWebDriver.DefaultCommandTimeout)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChromeDriver"/> class using the specified <see cref="ChromeDriverService"/>.
/// </summary>
/// <param name="service">The <see cref="ChromeDriverService"/> to use.</param>
/// <param name="options">The <see cref="ChromeOptions"/> to be used with the Chrome driver.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
public MyChromeDriver(ChromeDriverService service, ChromeOptions options, TimeSpan commandTimeout)
: base(service, options, commandTimeout)
{
this.AddCustomChromeCommands();
}
/// <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
/// <see cref="CommandInfo"/> objects describing the command behavior.
/// </summary>
public static IReadOnlyDictionary<string, CommandInfo> CustomCommandDefinitions
{
get
{
Dictionary<string, CommandInfo> customCommands = new Dictionary<string, CommandInfo>();
foreach (KeyValuePair<string, CommandInfo> entry in ChromiumCustomCommands)
{
customCommands[entry.Key] = entry.Value;
}
foreach (KeyValuePair<string, CommandInfo> entry in chromeCustomCommands)
{
customCommands[entry.Key] = entry.Value;
}
return new ReadOnlyDictionary<string, CommandInfo>(customCommands);
}
}
private void AddCustomChromeCommands()
{
foreach (KeyValuePair<string, CommandInfo> entry in CustomCommandDefinitions)
{
this.RegisterInternalDriverCommand(entry.Key, entry.Value);
}
}
}
}
// <copyright file="ChromiumDriver.cs" company="WebDriver Committers">
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using OpenQA.Selenium.DevTools;
using OpenQA.Selenium.Remote;
namespace OpenQA.Selenium.Chromium
{
/// <summary>
/// Provides an abstract way to access Chromium-based browsers to run tests.
/// </summary>
public class MyChromiumDriver : WebDriver, ISupportsLogs, IDevTools
{
/// <summary>
/// Accept untrusted SSL Certificates
/// </summary>
public static readonly bool AcceptUntrustedCertificates = true;
/// <summary>
/// Command for executing a Chrome DevTools Protocol command in a driver for a Chromium-based browser.
/// </summary>
public static readonly string ExecuteCdp = "executeCdpCommand";
/// <summary>
/// Command for getting cast sinks in a driver for a Chromium-based browser.
/// </summary>
public static readonly string GetCastSinksCommand = "getCastSinks";
/// <summary>
/// Command for selecting a cast sink in a driver for a Chromium-based browser.
/// </summary>
public static readonly string SelectCastSinkCommand = "selectCastSink";
/// <summary>
/// Command for starting cast tab mirroring in a driver for a Chromium-based browser.
/// </summary>
public static readonly string StartCastTabMirroringCommand = "startCastTabMirroring";
/// <summary>
/// Command for starting cast desktop mirroring in a driver for a Chromium-based browser.
/// </summary>
public static readonly string StartCastDesktopMirroringCommand = "startCastDesktopMirroring";
/// <summary>
/// Command for getting a cast issued message in a driver for a Chromium-based browser.
/// </summary>
public static readonly string GetCastIssueMessageCommand = "getCastIssueMessage";
/// <summary>
/// Command for stopping casting in a driver for a Chromium-based browser.
/// </summary>
public static readonly string StopCastingCommand = "stopCasting";
/// <summary>
/// Command for getting the simulated network conditions in a driver for a Chromium-based browser.
/// </summary>
public static readonly string GetNetworkConditionsCommand = "getNetworkConditions";
/// <summary>
/// Command for setting the simulated network conditions in a driver for a Chromium-based browser.
/// </summary>
public static readonly string SetNetworkConditionsCommand = "setNetworkConditions";
/// <summary>
/// Command for deleting the simulated network conditions in a driver for a Chromium-based browser.
/// </summary>
public static readonly string DeleteNetworkConditionsCommand = "deleteNetworkConditions";
/// <summary>
/// Command for executing a Chrome DevTools Protocol command in a driver for a Chromium-based browser.
/// </summary>
public static readonly string SendChromeCommand = "sendChromeCommand";
/// <summary>
/// Command for executing a Chrome DevTools Protocol command that returns a result in a driver for a Chromium-based browser.
/// </summary>
public static readonly string SendChromeCommandWithResult = "sendChromeCommandWithResult";
/// <summary>
/// Command for launching an app in a driver for a Chromium-based browser.
/// </summary>
public static readonly string LaunchAppCommand = "launchAppCommand";
/// <summary>
/// Command for setting permissions in a driver for a Chromium-based browser.
/// </summary>
public static readonly string SetPermissionCommand = "setPermission";
private readonly string optionsCapabilityName;
private DevToolsSession devToolsSession;
private static Dictionary<string, CommandInfo> chromiumCustomCommands = new Dictionary<string, CommandInfo>()
{
{ GetNetworkConditionsCommand, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/chromium/network_conditions") },
{ SetNetworkConditionsCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/chromium/network_conditions") },
{ DeleteNetworkConditionsCommand, new HttpCommandInfo(HttpCommandInfo.DeleteCommand, "/session/{sessionId}/chromium/network_conditions") },
{ SendChromeCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/chromium/send_command") },
{ SendChromeCommandWithResult, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/chromium/send_command_and_get_result") },
{ LaunchAppCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/chromium/launch_app") },
{ SetPermissionCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/permissions") }
};
/// <summary>
/// Initializes a new instance of the <see cref="ChromiumDriver"/> class using the specified <see cref="ChromiumDriverService"/>.
/// </summary>
/// <param name="service">The <see cref="ChromiumDriverService"/> to use.</param>
/// <param name="options">The <see cref="ChromiumOptions"/> to be used with the ChromiumDriver.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
protected MyChromiumDriver(ChromiumDriverService service, ChromiumOptions options, TimeSpan commandTimeout)
: base(new MyDriverServiceCommandExecutor(service, commandTimeout), ConvertOptionsToCapabilities(options))
{
this.optionsCapabilityName = options.CapabilityName;
}
protected static IReadOnlyDictionary<string, CommandInfo> ChromiumCustomCommands
{
get { return new ReadOnlyDictionary<string, CommandInfo>(chromiumCustomCommands); }
}
/// <summary>
/// Gets or sets the <see cref="IFileDetector"/> responsible for detecting
/// sequences of keystrokes representing file paths and names.
/// </summary>
/// <remarks>The Chromium driver does not allow a file detector to be set,
/// as the server component of the Chromium driver only
/// allows uploads from the local computer environment. Attempting to set
/// this property has no effect, but does not throw an exception. If you
/// are attempting to run the Chromium driver remotely, use <see cref="RemoteWebDriver"/>
/// in conjunction with a standalone WebDriver server.</remarks>
public override IFileDetector FileDetector
{
get { return base.FileDetector; }
set { }
}
/// <summary>
/// Gets a value indicating whether a DevTools session is active.
/// </summary>
public bool HasActiveDevToolsSession
{
get { return this.devToolsSession != null; }
}
/// <summary>
/// Gets or sets the network condition emulation for Chromium.
/// </summary>
public ChromiumNetworkConditions NetworkConditions
{
get
{
Response response = this.Execute(GetNetworkConditionsCommand, null);
return ChromiumNetworkConditions.FromDictionary(response.Value as Dictionary<string, object>);
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value), "value must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["network_conditions"] = value;
this.Execute(SetNetworkConditionsCommand, parameters);
}
}
/// <summary>
/// Launches a Chromium based application.
/// </summary>
/// <param name="id">ID of the chromium app to launch.</param>
public void LaunchApp(string id)
{
if (id == null)
{
throw new ArgumentNullException(nameof(id), "id must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["id"] = id;
this.Execute(LaunchAppCommand, parameters);
}
/// <summary>
/// Set supported permission on browser.
/// </summary>
/// <param name="permissionName">Name of item to set the permission on.</param>
/// <param name="permissionValue">Value to set the permission to.</param>
public void SetPermission(string permissionName, string permissionValue)
{
if (permissionName == null)
{
throw new ArgumentNullException(nameof(permissionName), "name must not be null");
}
if (permissionValue == null)
{
throw new ArgumentNullException(nameof(permissionValue), "value must not be null");
}
Dictionary<string, object> nameParameter = new Dictionary<string, object>();
nameParameter["name"] = permissionName;
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["descriptor"] = nameParameter;
parameters["state"] = permissionValue;
this.Execute(SetPermissionCommand, parameters);
}
/// <summary>
/// Executes a custom Chrome Dev Tools Protocol Command.
/// </summary>
/// <param name="commandName">Name of the command to execute.</param>
/// <param name="commandParameters">Parameters of the command to execute.</param>
/// <returns>An object representing the result of the command, if applicable.</returns>
public object ExecuteCdpCommand(string commandName, Dictionary<string, object> commandParameters)
{
if (commandName == null)
{
throw new ArgumentNullException(nameof(commandName), "commandName must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["cmd"] = commandName;
parameters["params"] = commandParameters;
Response response = this.Execute(ExecuteCdp, parameters);
return response.Value;
}
/// <summary>
/// Executes a custom Chrome command.
/// </summary>
/// <param name="commandName">Name of the command to execute.</param>
/// <param name="commandParameters">Parameters of the command to execute.</param>
[Obsolete("ExecuteChromeCommand is deprecated in favor of ExecuteCdpCommand.")]
public void ExecuteChromeCommand(string commandName, Dictionary<string, object> commandParameters)
{
ExecuteCdpCommand(commandName, commandParameters);
}
/// <summary>
/// Executes a custom Chrome command.
/// </summary>
/// <param name="commandName">Name of the command to execute.</param>
/// <param name="commandParameters">Parameters of the command to execute.</param>
/// <returns>An object representing the result of the command.</returns>
[Obsolete("ExecuteChromeCommandWithResult is deprecated in favor of ExecuteCdpCommand.")]
public object ExecuteChromeCommandWithResult(string commandName, Dictionary<string, object> commandParameters)
{
return ExecuteCdpCommand(commandName, commandParameters);
}
/// <summary>
/// Creates a session to communicate with a browser using the Chromium Developer Tools debugging protocol.
/// </summary>
/// <param name="devToolsProtocolVersion">The version of the Chromium Developer Tools protocol to use. Defaults to autodetect the protocol version.</param>
/// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns>
public DevToolsSession GetDevToolsSession()
{
return GetDevToolsSession(DevToolsSession.AutoDetectDevToolsProtocolVersion);
}
/// <summary>
/// Creates a session to communicate with a browser using the Chromium Developer Tools debugging protocol.
/// </summary>
/// <param name="devToolsProtocolVersion">The version of the Chromium Developer Tools protocol to use. Defaults to autodetect the protocol version.</param>
/// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns>
public DevToolsSession GetDevToolsSession(int devToolsProtocolVersion)
{
if (this.devToolsSession == null)
{
if (!this.Capabilities.HasCapability(this.optionsCapabilityName))
{
throw new WebDriverException("Cannot find " + this.optionsCapabilityName + " capability for driver");
}
Dictionary<string, object> options = this.Capabilities.GetCapability(this.optionsCapabilityName) as Dictionary<string, object>;
if (options == null)
{
throw new WebDriverException("Found " + this.optionsCapabilityName + " capability, but is not an object");
}
if (!options.ContainsKey("debuggerAddress"))
{
throw new WebDriverException("Did not find debuggerAddress capability in " + this.optionsCapabilityName);
}
string debuggerAddress = options["debuggerAddress"].ToString();
try
{
// DevToolsSession session = new DevToolsSession(debuggerAddress);
// session.StartSession(devToolsProtocolVersion).ConfigureAwait(false).GetAwaiter().GetResult();
// this.devToolsSession = session;
}
catch (Exception e)
{
throw new WebDriverException("Unexpected error creating WebSocket DevTools session.", e);
}
}
return this.devToolsSession;
}
/// <summary>
/// Closes a DevTools session.
/// </summary>
public void CloseDevToolsSession()
{
if (this.devToolsSession != null)
{
// this.devToolsSession.StopSession(true).ConfigureAwait(false).GetAwaiter().GetResult();
}
}
/// <summary>
/// Clears simulated network conditions.
/// </summary>
public void ClearNetworkConditions()
{
this.Execute(DeleteNetworkConditionsCommand, null);
}
/// <summary>
/// Returns the list of cast sinks (Cast devices) available to the Chrome media router.
/// </summary>
/// <returns>The list of available sinks.</returns>
public List<Dictionary<string, string>> GetCastSinks()
{
List<Dictionary<string, string>> returnValue = new List<Dictionary<string, string>>();
Response response = this.Execute(GetCastSinksCommand, null);
object[] responseValue = response.Value as object[];
if (responseValue != null)
{
foreach (object entry in responseValue)
{
Dictionary<string, object> entryValue = entry as Dictionary<string, object>;
if (entryValue != null)
{
Dictionary<string, string> sink = new Dictionary<string, string>();
foreach (KeyValuePair<string, object> pair in entryValue)
{
sink[pair.Key] = pair.Value.ToString();
}
returnValue.Add(sink);
}
}
}
return returnValue;
}
/// <summary>
/// Selects a cast sink (Cast device) as the recipient of media router intents (connect or play).
/// </summary>
/// <param name="deviceName">Name of the target sink (device).</param>
public void SelectCastSink(string deviceName)
{
if (deviceName == null)
{
throw new ArgumentNullException(nameof(deviceName), "deviceName must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["sinkName"] = deviceName;
this.Execute(SelectCastSinkCommand, parameters);
}
/// <summary>
/// Initiates tab mirroring for the current browser tab on the specified device.
/// </summary>
/// <param name="deviceName">Name of the target sink (device).</param>
public void StartTabMirroring(string deviceName)
{
if (deviceName == null)
{
throw new ArgumentNullException(nameof(deviceName), "deviceName must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["sinkName"] = deviceName;
this.Execute(StartCastTabMirroringCommand, parameters);
}
/// <summary>
/// Initiates mirroring of the desktop on the specified device.
/// </summary>
/// <param name="deviceName">Name of the target sink (device).</param>
public void StartDesktopMirroring(string deviceName)
{
if (deviceName == null)
{
throw new ArgumentNullException(nameof(deviceName), "deviceName must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["sinkName"] = deviceName;
this.Execute(StartCastDesktopMirroringCommand, parameters);
}
/// <summary>
/// Returns the error message if there is any issue in a Cast session.
/// </summary>
/// <returns>An error message.</returns>
public String GetCastIssueMessage()
{
Response response = this.Execute(GetCastIssueMessageCommand, null);
return (string)response.Value;
}
/// <summary>
/// Stops casting from media router to the specified device, if connected.
/// </summary>
/// <param name="deviceName">Name of the target sink (device).</param>
public void StopCasting(string deviceName)
{
if (deviceName == null)
{
throw new ArgumentNullException(nameof(deviceName), "deviceName must not be null");
}
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters["sinkName"] = deviceName;
this.Execute(StopCastingCommand, parameters);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (this.devToolsSession != null)
{
this.devToolsSession.Dispose();
this.devToolsSession = null;
}
}
base.Dispose(disposing);
}
private static ICapabilities ConvertOptionsToCapabilities(ChromiumOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options), "options must not be null");
}
return options.ToCapabilities();
}
}
}
// <copyright file="DriverServiceCommandExecutor.cs" company="WebDriver Committers">
// 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.
// </copyright>
using System;
namespace OpenQA.Selenium.Remote
{
/// <summary>
/// Provides a mechanism to execute commands on the browser
/// </summary>
public class MyDriverServiceCommandExecutor : ICommandExecutor
{
private DriverService service;
private MyHttpCommandExecutor internalExecutor;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="DriverServiceCommandExecutor"/> class.
/// </summary>
/// <param name="driverService">The <see cref="DriverService"/> that drives the browser.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
public MyDriverServiceCommandExecutor(DriverService driverService, TimeSpan commandTimeout)
: this(driverService, commandTimeout, true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DriverServiceCommandExecutor"/> class.
/// </summary>
/// <param name="driverService">The <see cref="DriverService"/> that drives the browser.</param>
/// <param name="commandTimeout">The maximum amount of time to wait for each command.</param>
/// <param name="enableKeepAlive"><see langword="true"/> if the KeepAlive header should be sent
/// with HTTP requests; otherwise, <see langword="false"/>.</param>
public MyDriverServiceCommandExecutor(DriverService driverService, TimeSpan commandTimeout, bool enableKeepAlive)
{
this.service = driverService;
this.internalExecutor = new MyHttpCommandExecutor(driverService.ServiceUrl, commandTimeout, enableKeepAlive);
}
/// <summary>
/// Initializes a new instance of the <see cref="DriverServiceCommandExecutor"/> class.
/// </summary>
/// <param name="service">The <see cref="DriverService"/> that drives the browser.</param>
/// <param name="commandExecutor">The <see cref="HttpCommandExecutor"/> object used to execute commands,
/// communicating with the service via HTTP.</param>
public MyDriverServiceCommandExecutor(DriverService service, MyHttpCommandExecutor commandExecutor)
{
this.service = service;
this.internalExecutor = commandExecutor;
}
/// <summary>
/// Gets the <see cref="CommandInfoRepository"/> object associated with this executor.
/// </summary>
//public CommandInfoRepository CommandInfoRepository
//{
// get { return this.internalExecutor.CommandInfoRepository; }
//}
public bool TryAddCommand(string commandName, CommandInfo info)
{
return this.internalExecutor.TryAddCommand(commandName, info);
}
/// <summary>
/// Gets the <see cref="HttpCommandExecutor"/> that sends commands to the remote
/// end WebDriver implementation.
/// </summary>
public MyHttpCommandExecutor HttpExecutor
{
get { return this.internalExecutor; }
}
/// <summary>
/// Executes a command
/// </summary>
/// <param name="commandToExecute">The command you wish to execute</param>
/// <returns>A response from the browser</returns>
public Response Execute(Command commandToExecute)
{
if (commandToExecute == null)
{
throw new ArgumentNullException(nameof(commandToExecute), "Command to execute cannot be null");
}
Response toReturn = null;
if (commandToExecute.Name == DriverCommand.NewSession)
{
this.service.Start();
}
// Use a try-catch block to catch exceptions for the Quit
// command, so that we can get the finally block.
try
{
toReturn = this.internalExecutor.Execute(commandToExecute);
}
finally
{
if (commandToExecute.Name == DriverCommand.Quit)
{
this.Dispose();
}
}
return toReturn;
}
/// <summary>
/// Releases all resources used by the <see cref="DriverServiceCommandExecutor"/>.
/// </summary>
public void Dispose()
{
this.Dispose(true);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="HttpCommandExecutor"/> and
/// optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release managed and resources;
/// <see langword="false"/> to only release unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!this.isDisposed)
{
if (disposing)
{
this.internalExecutor.Dispose();
this.service.Dispose();
}
this.isDisposed = true;
}
}
}
}
// <copyright file="MyHttpCommandExecutor.cs" company="WebDriver Committers">
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OpenQA.Selenium.Internal;
namespace OpenQA.Selenium.Remote
{
/// <summary>
/// Provides a way of executing Commands over HTTP
/// </summary>
public class MyHttpCommandExecutor : ICommandExecutor
{
private const string JsonMimeType = "application/json";
private const string PngMimeType = "image/png";
private const string Utf8CharsetType = "utf-8";
private const string RequestAcceptHeader = JsonMimeType + ", " + PngMimeType;
private const string RequestContentTypeHeader = JsonMimeType + "; charset=" + Utf8CharsetType;
private const string UserAgentHeaderTemplate = "selenium/{0} (.net {1})";
private Uri remoteServerUri;
private TimeSpan serverResponseTimeout;
private string userAgent;
private bool enableKeepAlive;
private bool isDisposed;
private IWebProxy proxy;
private CommandInfoRepository commandInfoRepository = new W3CWireProtocolCommandInfoRepository();
private HttpClient client;
/// <summary>
/// Initializes a new instance of the <see cref="MyHttpCommandExecutor"/> class
/// </summary>
/// <param name="addressOfRemoteServer">Address of the WebDriver Server</param>
/// <param name="timeout">The timeout within which the server must respond.</param>
public MyHttpCommandExecutor(Uri addressOfRemoteServer, TimeSpan timeout)
: this(addressOfRemoteServer, timeout, true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MyHttpCommandExecutor"/> class
/// </summary>
/// <param name="addressOfRemoteServer">Address of the WebDriver Server</param>
/// <param name="timeout">The timeout within which the server must respond.</param>
/// <param name="enableKeepAlive"><see langword="true"/> if the KeepAlive header should be sent
/// with HTTP requests; otherwise, <see langword="false"/>.</param>
public MyHttpCommandExecutor(Uri addressOfRemoteServer, TimeSpan timeout, bool enableKeepAlive)
{
if (addressOfRemoteServer == null)
{
throw new ArgumentNullException(nameof(addressOfRemoteServer), "You must specify a remote address to connect to");
}
if (!addressOfRemoteServer.AbsoluteUri.EndsWith("/", StringComparison.OrdinalIgnoreCase))
{
addressOfRemoteServer = new Uri(addressOfRemoteServer.ToString() + "/");
}
this.userAgent = string.Format(CultureInfo.InvariantCulture, UserAgentHeaderTemplate, ResourceUtilities.AssemblyVersion, ResourceUtilities.PlatformFamily);
this.remoteServerUri = addressOfRemoteServer;
this.serverResponseTimeout = timeout;
this.enableKeepAlive = enableKeepAlive;
}
/// <summary>
/// Occurs when the <see cref="MyHttpCommandExecutor"/> is sending an HTTP
/// request to the remote end WebDriver implementation.
/// </summary>
public event EventHandler<SendingRemoteHttpRequestEventArgs> SendingRemoteHttpRequest;
/// <summary>
/// Gets or sets an <see cref="IWebProxy"/> object to be used to proxy requests
/// between this <see cref="MyHttpCommandExecutor"/> and the remote end WebDriver
/// implementation.
/// </summary>
public IWebProxy Proxy
{
get { return this.proxy; }
set { this.proxy = value; }
}
/// <summary>
/// Gets or sets a value indicating whether keep-alive is enabled for HTTP
/// communication between this <see cref="MyHttpCommandExecutor"/> and the
/// remote end WebDriver implementation.
/// </summary>
public bool IsKeepAliveEnabled
{
get { return this.enableKeepAlive; }
set { this.enableKeepAlive = value; }
}
/// <summary>
/// Gets or sets the user agent string used for HTTP communication
/// batween this <see cref="MyHttpCommandExecutor"/> and the remote end
/// WebDriver implementation
/// </summary>
public string UserAgent
{
get { return this.userAgent; }
set { this.userAgent = value; }
}
/// <summary>
/// Gets the repository of objects containing information about commands.
/// </summary>
protected CommandInfoRepository CommandInfoRepository
{
get { return this.commandInfoRepository; }
set { this.commandInfoRepository = value; }
}
/// <summary>
/// Attempts to add a command to the repository of commands known to this executor.
/// </summary>
/// <param name="commandName">The name of the command to attempt to add.</param>
/// <param name="info">The <see cref="CommandInfo"/> describing the commnd to add.</param>
/// <returns><see langword="true"/> if the new command has been added successfully; otherwise, <see langword="false"/>.</returns>
public bool TryAddCommand(string commandName, CommandInfo info)
{
HttpCommandInfo commandInfo = info as HttpCommandInfo;
if (commandInfo == null)
{
return false;
}
return this.commandInfoRepository.TryAddCommand(commandName, commandInfo);
}
/// <summary>
/// Executes a command
/// </summary>
/// <param name="commandToExecute">The command you wish to execute</param>
/// <returns>A response from the browser</returns>
public virtual Response Execute(Command commandToExecute)
{
if (commandToExecute == null)
{
throw new ArgumentNullException(nameof(commandToExecute), "commandToExecute cannot be null");
}
HttpCommandInfo info = this.commandInfoRepository.GetCommandInfo<HttpCommandInfo>(commandToExecute.Name);
if (info == null)
{
throw new NotImplementedException(string.Format("The command you are attempting to execute, {0}, does not exist in the protocol dialect used by the remote end.", commandToExecute.Name));
}
if (this.client == null)
{
this.CreateHttpClient();
}
HttpRequestInfo requestInfo = new HttpRequestInfo(this.remoteServerUri, commandToExecute, info);
HttpResponseInfo responseInfo = null;
try
{
// Use TaskFactory to avoid deadlock in multithreaded implementations.
responseInfo = new TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default)
.StartNew(() => this.MakeHttpRequest(requestInfo))
.Unwrap()
.GetAwaiter()
.GetResult();
}
catch (HttpRequestException ex)
{
WebException innerWebException = ex.InnerException as WebException;
if (innerWebException != null)
{
if (innerWebException.Status == WebExceptionStatus.Timeout)
{
string timeoutMessage = "The HTTP request to the remote WebDriver server for URL {0} timed out after {1} seconds.";
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, timeoutMessage, requestInfo.FullUri.AbsoluteUri, this.serverResponseTimeout.TotalSeconds), ex);
}
else if (innerWebException.Status == WebExceptionStatus.ConnectFailure)
{
string connectFailureMessage = "Could not connect to the remote WebDriver server for URL {0}.";
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, connectFailureMessage, requestInfo.FullUri.AbsoluteUri, this.serverResponseTimeout.TotalSeconds), ex);
}
else if (innerWebException.Response == null)
{
string nullResponseMessage = "A exception with a null response was thrown sending an HTTP request to the remote WebDriver server for URL {0}. The status of the exception was {1}, and the message was: {2}";
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, nullResponseMessage, requestInfo.FullUri.AbsoluteUri, innerWebException.Status, innerWebException.Message), innerWebException);
}
}
string unknownErrorMessage = "An unknown exception was encountered sending an HTTP request to the remote WebDriver server for URL {0}. The exception message was: {1}";
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, unknownErrorMessage, requestInfo.FullUri.AbsoluteUri, ex.Message), ex);
}
catch (TaskCanceledException ex)
{
string timeoutMessage = "The HTTP request to the remote WebDriver server for URL {0} timed out after {1} seconds.";
throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, timeoutMessage, requestInfo.FullUri.AbsoluteUri, this.serverResponseTimeout.TotalSeconds), ex);
}
Response toReturn = this.CreateResponse(responseInfo);
return toReturn;
}
/// <summary>
/// Raises the <see cref="SendingRemoteHttpRequest"/> event.
/// </summary>
/// <param name="eventArgs">A <see cref="SendingRemoteHttpRequestEventArgs"/> that contains the event data.</param>
protected virtual void OnSendingRemoteHttpRequest(SendingRemoteHttpRequestEventArgs eventArgs)
{
if (eventArgs == null)
{
throw new ArgumentNullException(nameof(eventArgs), "eventArgs must not be null");
}
if (this.SendingRemoteHttpRequest != null)
{
this.SendingRemoteHttpRequest(this, eventArgs);
}
}
private void CreateHttpClient()
{
HttpClientHandler httpClientHandler = new HttpClientHandler();
string userInfo = this.remoteServerUri.UserInfo;
if (!string.IsNullOrEmpty(userInfo) && userInfo.Contains(":"))
{
string[] userInfoComponents = this.remoteServerUri.UserInfo.Split(new char[] { ':' }, 2);
httpClientHandler.Credentials = new NetworkCredential(userInfoComponents[0], userInfoComponents[1]);
httpClientHandler.PreAuthenticate = true;
}
// httpClientHandler.Proxy = this.Proxy;
this.client = new HttpClient(httpClientHandler);
this.client.DefaultRequestHeaders.UserAgent.ParseAdd(this.UserAgent);
this.client.DefaultRequestHeaders.Accept.ParseAdd(RequestAcceptHeader);
this.client.DefaultRequestHeaders.ExpectContinue = false;
if (!this.IsKeepAliveEnabled)
{
this.client.DefaultRequestHeaders.Connection.ParseAdd("close");
}
this.client.Timeout = this.serverResponseTimeout;
}
private async Task<HttpResponseInfo> MakeHttpRequest(HttpRequestInfo requestInfo)
{
SendingRemoteHttpRequestEventArgs eventArgs = new SendingRemoteHttpRequestEventArgs(requestInfo.HttpMethod, requestInfo.FullUri.ToString(), requestInfo.RequestBody);
this.OnSendingRemoteHttpRequest(eventArgs);
HttpMethod method = new HttpMethod(requestInfo.HttpMethod);
using (HttpRequestMessage requestMessage = new HttpRequestMessage(method, requestInfo.FullUri))
{
foreach (KeyValuePair<string, string> header in eventArgs.Headers)
{
requestMessage.Headers.Add(header.Key, header.Value);
}
if (requestInfo.HttpMethod == HttpCommandInfo.GetCommand)
{
CacheControlHeaderValue cacheControlHeader = new CacheControlHeaderValue();
cacheControlHeader.NoCache = true;
requestMessage.Headers.CacheControl = cacheControlHeader;
}
if (requestInfo.HttpMethod == HttpCommandInfo.PostCommand)
{
MediaTypeWithQualityHeaderValue acceptHeader = new MediaTypeWithQualityHeaderValue(JsonMimeType);
acceptHeader.CharSet = Utf8CharsetType;
requestMessage.Headers.Accept.Add(acceptHeader);
byte[] bytes = Encoding.UTF8.GetBytes(eventArgs.RequestBody);
requestMessage.Content = new ByteArrayContent(bytes, 0, bytes.Length);
MediaTypeHeaderValue contentTypeHeader = new MediaTypeHeaderValue(JsonMimeType);
contentTypeHeader.CharSet = Utf8CharsetType;
requestMessage.Content.Headers.ContentType = contentTypeHeader;
}
using (HttpResponseMessage responseMessage = await this.client.SendAsync(requestMessage))
{
HttpResponseInfo httpResponseInfo = new HttpResponseInfo();
httpResponseInfo.Body = await responseMessage.Content.ReadAsStringAsync();
httpResponseInfo.ContentType = responseMessage.Content.Headers.ContentType.ToString();
httpResponseInfo.StatusCode = responseMessage.StatusCode;
return httpResponseInfo;
}
}
}
private Response CreateResponse(HttpResponseInfo responseInfo)
{
Response response = new Response();
string body = responseInfo.Body;
if (responseInfo.ContentType != null && responseInfo.ContentType.StartsWith(JsonMimeType, StringComparison.OrdinalIgnoreCase))
{
response = Response.FromJson(body);
}
else
{
response.Value = body;
}
if (response.Value is string)
{
response.Value = ((string)response.Value).Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
}
return response;
}
/// <summary>
/// Releases all resources used by the <see cref="MyHttpCommandExecutor"/>.
/// </summary>
public void Dispose()
{
this.Dispose(true);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="MyHttpCommandExecutor"/> and
/// optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release managed and resources;
/// <see langword="false"/> to only release unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!this.isDisposed)
{
if (this.client != null)
{
this.client.Dispose();
}
this.isDisposed = true;
}
}
private class HttpRequestInfo
{
public HttpRequestInfo(Uri serverUri, Command commandToExecute, HttpCommandInfo commandInfo)
{
this.FullUri = commandInfo.CreateCommandUri(serverUri, commandToExecute);
this.HttpMethod = commandInfo.Method;
this.RequestBody = commandToExecute.ParametersAsJsonString;
}
public Uri FullUri { get; set; }
public string HttpMethod { get; set; }
public string RequestBody { get; set; }
}
private class HttpResponseInfo
{
public HttpStatusCode StatusCode { get; set; }
public string Body { get; set; }
public string ContentType { get; set; }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment