| // <copyright file="ChromiumDriver.cs" company="Selenium 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 OpenQA.Selenium.DevTools; |
| using OpenQA.Selenium.Remote; |
| using System; |
| using System.Collections.Generic; |
| using System.Collections.ObjectModel; |
| using System.Diagnostics.CodeAnalysis; |
| using System.IO; |
| using System.Threading.Tasks; |
| |
| #nullable enable |
| |
| namespace OpenQA.Selenium.Chromium |
| { |
| /// <summary> |
| /// Provides an abstract way to access Chromium-based browsers to run tests. |
| /// </summary> |
| public class ChromiumDriver : 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 readonly 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> |
| /// <exception cref="ArgumentNullException">If <paramref name="service"/> or <paramref name="options"/> are <see langword="null"/>.</exception> |
| /// <exception cref="ArgumentException">If the Chromium options capability name is <see langword="null"/>.</exception> |
| protected ChromiumDriver(ChromiumDriverService service, ChromiumOptions options, TimeSpan commandTimeout) |
| : base(GenerateDriverServiceCommandExecutor(service, options, commandTimeout), ConvertOptionsToCapabilities(options)) |
| { |
| 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> |
| protected static IReadOnlyDictionary<string, CommandInfo> ChromiumCustomCommands => new ReadOnlyDictionary<string, CommandInfo>(chromiumCustomCommands); |
| |
| /// <summary> |
| /// Uses DriverFinder to set Service attributes if necessary when creating the command executor |
| /// </summary> |
| /// <param name="service"></param> |
| /// <param name="commandTimeout"></param> |
| /// <param name="options"></param> |
| /// <returns></returns> |
| private static ICommandExecutor GenerateDriverServiceCommandExecutor(DriverService service, DriverOptions options, TimeSpan commandTimeout) |
| { |
| if (service is null) |
| { |
| throw new ArgumentNullException(nameof(service)); |
| } |
| |
| if (options is null) |
| { |
| throw new ArgumentNullException(nameof(options)); |
| } |
| |
| if (service.DriverServicePath == null) |
| { |
| DriverFinder finder = new DriverFinder(options); |
| string fullServicePath = finder.GetDriverPath(); |
| service.DriverServicePath = Path.GetDirectoryName(fullServicePath); |
| service.DriverServiceExecutableName = Path.GetFileName(fullServicePath); |
| if (finder.HasBrowserPath()) |
| { |
| options.BinaryLocation = finder.GetBrowserPath(); |
| options.BrowserVersion = null; |
| } |
| } |
| return new DriverServiceCommandExecutor(service, commandTimeout); |
| } |
| |
| /// <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 => base.FileDetector; |
| set { } |
| } |
| |
| /// <summary> |
| /// Gets a value indicating whether a DevTools session is active. |
| /// </summary> |
| [MemberNotNullWhen(true, nameof(devToolsSession))] |
| public bool HasActiveDevToolsSession => this.devToolsSession != null; |
| |
| /// <summary> |
| /// Gets or sets the network condition emulation for Chromium. |
| /// </summary> |
| /// <exception cref="ArgumentNullException">If the value is set to <see langword="null"/>.</exception> |
| public ChromiumNetworkConditions NetworkConditions |
| { |
| get |
| { |
| Response response = this.Execute(GetNetworkConditionsCommand, null); |
| if (response.Value is not Dictionary<string, object?> responseDictionary) |
| { |
| throw new WebDriverException($"GetNetworkConditions command returned successfully, but data was not an object: {response.Value}"); |
| } |
| |
| return ChromiumNetworkConditions.FromDictionary(responseDictionary); |
| } |
| |
| 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> |
| /// <exception cref="ArgumentNullException">If <paramref name="id"/> is <see langword="null"/>.</exception> |
| 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> |
| /// <exception cref="ArgumentNullException">If <paramref name="permissionName"/> or <paramref name="permissionValue"/> are <see langword="null"/>.</exception> |
| 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> |
| /// <exception cref="ArgumentNullException">If <paramref name="commandName"/> is <see langword="null"/>.</exception> |
| 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> |
| /// Creates a session to communicate with a browser using the Chromium Developer Tools debugging protocol. |
| /// </summary> |
| /// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns> |
| public DevToolsSession GetDevToolsSession() |
| { |
| return GetDevToolsSession(new DevToolsOptions() { ProtocolVersion = DevToolsSession.AutoDetectDevToolsProtocolVersion }); |
| } |
| |
| /// <summary> |
| /// Creates a session to communicate with a browser using the Chromium Developer Tools debugging protocol. |
| /// </summary> |
| /// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns> |
| public DevToolsSession GetDevToolsSession(DevToolsOptions options) |
| { |
| if (this.devToolsSession == null) |
| { |
| if (!this.Capabilities.HasCapability(this.optionsCapabilityName)) |
| { |
| throw new WebDriverException("Cannot find " + this.optionsCapabilityName + " capability for driver"); |
| } |
| |
| object? optionsCapabilityObject = this.Capabilities.GetCapability(this.optionsCapabilityName); |
| if (optionsCapabilityObject is not Dictionary<string, object?> optionsCapability) |
| { |
| throw new WebDriverException($"Found {this.optionsCapabilityName} capability, but is not an object: {optionsCapabilityObject}"); |
| } |
| |
| if (!optionsCapability.TryGetValue("debuggerAddress", out object? debuggerAddress)) |
| { |
| throw new WebDriverException("Did not find debuggerAddress capability in " + this.optionsCapabilityName); |
| } |
| |
| try |
| { |
| DevToolsSession session = new DevToolsSession(debuggerAddress?.ToString(), options); |
| Task.Run(async () => await session.StartSession()).GetAwaiter().GetResult(); |
| this.devToolsSession = session; |
| } |
| catch (Exception e) |
| { |
| throw new WebDriverException("Unexpected error creating WebSocket DevTools session.", e); |
| } |
| } |
| |
| return this.devToolsSession; |
| } |
| |
| /// <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> |
| [Obsolete("Use GetDevToolsSession(DevToolsOptions options)")] |
| public DevToolsSession GetDevToolsSession(int devToolsProtocolVersion) |
| { |
| return GetDevToolsSession(new DevToolsOptions() { ProtocolVersion = devToolsProtocolVersion }); |
| } |
| |
| /// <summary> |
| /// Closes a DevTools session. |
| /// </summary> |
| public void CloseDevToolsSession() |
| { |
| if (this.devToolsSession != null) |
| { |
| Task.Run(async () => await this.devToolsSession.StopSession(manualDetach: true)).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); |
| if (response.Value is object?[] responseValue) |
| { |
| foreach (object? entry in responseValue) |
| { |
| if (entry is Dictionary<string, object?> entryValue) |
| { |
| 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); |
| } |
| |
| /// <summary> |
| /// Stops the driver from running |
| /// </summary> |
| /// <param name="disposing">if its in the process of disposing</param> |
| 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(); |
| } |
| } |
| } |