| // <copyright file="FirefoxDriver.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.Remote; |
| using System; |
| using System.Collections.Generic; |
| using System.Collections.ObjectModel; |
| using System.Globalization; |
| using System.IO; |
| using System.IO.Compression; |
| |
| namespace OpenQA.Selenium.Firefox; |
| |
| /// <summary> |
| /// Provides a way to access Firefox to run tests. |
| /// </summary> |
| /// <remarks> |
| /// When the FirefoxDriver object has been instantiated the browser will load. The test can then navigate to the URL under test and |
| /// start your test. |
| /// <para> |
| /// In the case of the FirefoxDriver, you can specify a named profile to be used, or you can let the |
| /// driver create a temporary, anonymous profile. A custom extension allowing the driver to communicate |
| /// to the browser will be installed into the profile. |
| /// </para> |
| /// </remarks> |
| /// <example> |
| /// <code> |
| /// [TestFixture] |
| /// public class Testing |
| /// { |
| /// private IWebDriver driver; |
| /// <para></para> |
| /// [SetUp] |
| /// public void SetUp() |
| /// { |
| /// driver = new FirefoxDriver(); |
| /// } |
| /// <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 FirefoxDriver : WebDriver |
| { |
| /// <summary> |
| /// Command for setting the command context of a Firefox driver. |
| /// </summary> |
| public static readonly string SetContextCommand = "setContext"; |
| |
| /// <summary> |
| /// Command for getting the command context of a Firefox driver. |
| /// </summary> |
| public static readonly string GetContextCommand = "getContext"; |
| |
| /// <summary> |
| /// Command for installing an addon to a Firefox driver. |
| /// </summary> |
| public static readonly string InstallAddOnCommand = "installAddOn"; |
| |
| /// <summary> |
| /// Command for uninstalling an addon from a Firefox driver. |
| /// </summary> |
| public static readonly string UninstallAddOnCommand = "uninstallAddOn"; |
| |
| /// <summary> |
| /// Command for getting aa full page screenshot from a Firefox driver. |
| /// </summary> |
| public static readonly string GetFullPageScreenshotCommand = "fullPageScreenshot"; |
| |
| private static readonly Dictionary<string, CommandInfo> firefoxCustomCommands = new Dictionary<string, CommandInfo>() |
| { |
| { SetContextCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/moz/context") }, |
| { GetContextCommand, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/moz/context") }, |
| { InstallAddOnCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/moz/addon/install") }, |
| { UninstallAddOnCommand, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/moz/addon/uninstall") }, |
| { GetFullPageScreenshotCommand, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/moz/screenshot/full") } |
| }; |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class. |
| /// </summary> |
| public FirefoxDriver() |
| : this(new FirefoxOptions()) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified options. Uses the Mozilla-provided Marionette driver implementation. |
| /// </summary> |
| /// <param name="options">The <see cref="FirefoxOptions"/> to be used with the Firefox driver.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception> |
| public FirefoxDriver(FirefoxOptions options) |
| : this(FirefoxDriverService.CreateDefaultService(), options, RemoteWebDriver.DefaultCommandTimeout) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified driver service. Uses the Mozilla-provided Marionette driver implementation. |
| /// </summary> |
| /// <param name="service">The <see cref="FirefoxDriverService"/> used to initialize the driver.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="service"/> is <see langword="null"/>.</exception> |
| public FirefoxDriver(FirefoxDriverService service) |
| : this(service, new FirefoxOptions(), RemoteWebDriver.DefaultCommandTimeout) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified path |
| /// to the directory containing <c>geckodriver.exe</c>. |
| /// </summary> |
| /// <param name="geckoDriverDirectory">The full path to the directory containing <c>geckodriver.exe</c>.</param> |
| public FirefoxDriver(string geckoDriverDirectory) |
| : this(geckoDriverDirectory, new FirefoxOptions()) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified path |
| /// to the directory containing <c>geckodriver.exe</c> and options. |
| /// </summary> |
| /// <param name="geckoDriverDirectory">The full path to the directory containing <c>geckodriver.exe</c>.</param> |
| /// <param name="options">The <see cref="FirefoxOptions"/> to be used with the Firefox driver.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception> |
| public FirefoxDriver(string geckoDriverDirectory, FirefoxOptions options) |
| : this(geckoDriverDirectory, options, RemoteWebDriver.DefaultCommandTimeout) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified path |
| /// to the directory containing <c>geckodriver.exe</c>, options, and command timeout. |
| /// </summary> |
| /// <param name="geckoDriverDirectory">The full path to the directory containing <c>geckodriver.exe</c>.</param> |
| /// <param name="options">The <see cref="FirefoxOptions"/> to be used with the Firefox driver.</param> |
| /// <param name="commandTimeout">The maximum amount of time to wait for each command.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception> |
| public FirefoxDriver(string geckoDriverDirectory, FirefoxOptions options, TimeSpan commandTimeout) |
| : this(FirefoxDriverService.CreateDefaultService(geckoDriverDirectory), options, commandTimeout) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified options, driver service, and timeout. Uses the Mozilla-provided Marionette driver implementation. |
| /// </summary> |
| /// <param name="service">The <see cref="FirefoxDriverService"/> to use.</param> |
| /// <param name="options">The <see cref="FirefoxOptions"/> to be used with the Firefox driver.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="service"/> or <paramref name="options"/> are <see langword="null"/>.</exception> |
| public FirefoxDriver(FirefoxDriverService service, FirefoxOptions options) |
| : this(service, options, RemoteWebDriver.DefaultCommandTimeout) |
| { |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="FirefoxDriver"/> class using the specified options, driver service, and timeout. Uses the Mozilla-provided Marionette driver implementation. |
| /// </summary> |
| /// <param name="service">The <see cref="FirefoxDriverService"/> to use.</param> |
| /// <param name="options">The <see cref="FirefoxOptions"/> to be used with the Firefox driver.</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> |
| public FirefoxDriver(FirefoxDriverService service, FirefoxOptions options, TimeSpan commandTimeout) |
| : base(GenerateDriverServiceCommandExecutor(service, options, commandTimeout), ConvertOptionsToCapabilities(options)) |
| { |
| // Add the custom commands unique to Firefox |
| this.AddCustomFirefoxCommands(); |
| } |
| |
| /// <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> |
| /// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception> |
| private static ICommandExecutor GenerateDriverServiceCommandExecutor(DriverService service, DriverOptions options, TimeSpan commandTimeout) |
| { |
| if (options is null) |
| { |
| throw new ArgumentNullException(nameof(options)); |
| } |
| |
| if (service is null) |
| { |
| throw new ArgumentNullException(nameof(service)); |
| } |
| |
| 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.TryGetBrowserPath(out string? browserPath)) |
| { |
| options.BinaryLocation = browserPath; |
| options.BrowserVersion = null; |
| } |
| } |
| return new DriverServiceCommandExecutor(service, commandTimeout); |
| } |
| |
| /// <summary> |
| /// Gets a read-only dictionary of the custom WebDriver commands defined for FirefoxDriver. |
| /// 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 => new ReadOnlyDictionary<string, CommandInfo>(firefoxCustomCommands); |
| |
| /// <summary> |
| /// Gets or sets the <see cref="IFileDetector"/> responsible for detecting |
| /// sequences of keystrokes representing file paths and names. |
| /// </summary> |
| /// <remarks>The Firefox driver does not allow a file detector to be set, |
| /// as the server component of the Firefox 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 Firefox driver remotely, use <see cref="RemoteWebDriver"/> in |
| /// conjunction with a standalone WebDriver server.</remarks> |
| public override IFileDetector FileDetector |
| { |
| get => base.FileDetector; |
| set { } |
| } |
| |
| /// <summary> |
| /// Gets the command context used when issuing commands to <c>geckodriver</c>. |
| /// </summary> |
| /// <exception cref="WebDriverException">If response is not recognized</exception> |
| /// <returns>The context of commands.</returns> |
| public FirefoxCommandContext GetContext() |
| { |
| Response commandResponse = this.Execute(GetContextCommand, null); |
| |
| if (commandResponse.Value is not string response |
| || !Enum.TryParse(response, ignoreCase: true, out FirefoxCommandContext output)) |
| { |
| throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, "Could not recognize the response: {0}; expected 'Content' or 'Chrome'", commandResponse.Value)); |
| } |
| |
| return output; |
| } |
| |
| /// <summary> |
| /// Sets the command context used when issuing commands to <c>geckodriver</c>. |
| /// </summary> |
| /// <param name="context">The <see cref="FirefoxCommandContext"/> value to which to set the context.</param> |
| public void SetContext(FirefoxCommandContext context) |
| { |
| string contextValue = context.ToString().ToLowerInvariant(); |
| Dictionary<string, object> parameters = new Dictionary<string, object>(); |
| parameters["context"] = contextValue; |
| this.Execute(SetContextCommand, parameters); |
| } |
| |
| /// <summary> |
| /// Installs a Firefox add-on from a directory. |
| /// </summary> |
| /// <param name="addOnDirectoryToInstall">Full path of the directory of the add-on to install.</param> |
| /// <param name="temporary">Whether the add-on is temporary; required for unsigned add-ons.</param> |
| /// <returns>The unique identifier of the installed add-on.</returns> |
| /// <exception cref="ArgumentNullException">If <paramref name="addOnDirectoryToInstall"/> is null or empty.</exception> |
| /// <exception cref="ArgumentException">If the directory at <paramref name="addOnDirectoryToInstall"/> does not exist.</exception> |
| public string InstallAddOnFromDirectory(string addOnDirectoryToInstall, bool temporary = false) |
| { |
| if (string.IsNullOrEmpty(addOnDirectoryToInstall)) |
| { |
| throw new ArgumentNullException(nameof(addOnDirectoryToInstall), "Add-on file name must not be null or the empty string"); |
| } |
| |
| if (!Directory.Exists(addOnDirectoryToInstall)) |
| { |
| throw new ArgumentException("Directory " + addOnDirectoryToInstall + " does not exist", nameof(addOnDirectoryToInstall)); |
| } |
| |
| string addOnFileToInstall = Path.Combine(Path.GetTempPath(), "addon" + new Random().Next() + ".zip"); |
| ZipFile.CreateFromDirectory(addOnDirectoryToInstall, addOnFileToInstall); |
| |
| return this.InstallAddOnFromFile(addOnFileToInstall, temporary); |
| } |
| |
| /// <summary> |
| /// Installs a Firefox add-on from a file, typically a .xpi file. |
| /// </summary> |
| /// <param name="addOnFileToInstall">Full path and file name of the add-on to install.</param> |
| /// <param name="temporary">Whether the add-on is temporary; required for unsigned add-ons.</param> |
| /// <returns>The unique identifier of the installed add-on.</returns> |
| /// <exception cref="ArgumentNullException"> |
| /// <para>If <paramref name="addOnFileToInstall"/> is null or empty.</para> |
| /// or |
| /// <para>If the file at <paramref name="addOnFileToInstall"/> does not exist.</para> |
| /// </exception> |
| public string InstallAddOnFromFile(string addOnFileToInstall, bool temporary = false) |
| { |
| if (string.IsNullOrEmpty(addOnFileToInstall)) |
| { |
| throw new ArgumentNullException(nameof(addOnFileToInstall), "Add-on file name must not be null or the empty string"); |
| } |
| |
| byte[] addOnBytes; |
| try |
| { |
| addOnBytes = File.ReadAllBytes(addOnFileToInstall); |
| } |
| catch (Exception ex) |
| { |
| throw new ArgumentException($"Failed to read from file {addOnFileToInstall}", nameof(addOnFileToInstall), ex); |
| } |
| |
| string base64EncodedAddOn = Convert.ToBase64String(addOnBytes); |
| |
| return this.InstallAddOn(base64EncodedAddOn, temporary); |
| } |
| |
| /// <summary> |
| /// Installs a Firefox add-on. |
| /// </summary> |
| /// <param name="base64EncodedAddOn">The base64-encoded string representation of the add-on binary.</param> |
| /// <param name="temporary">Whether the add-on is temporary; required for unsigned add-ons.</param> |
| /// <returns>The unique identifier of the installed add-on.</returns> |
| /// <exception cref="ArgumentNullException">If <paramref name="base64EncodedAddOn"/> is null or empty.</exception> |
| public string InstallAddOn(string base64EncodedAddOn, bool temporary = false) |
| { |
| if (string.IsNullOrEmpty(base64EncodedAddOn)) |
| { |
| throw new ArgumentNullException(nameof(base64EncodedAddOn), "Base64 encoded add-on must not be null or the empty string"); |
| } |
| |
| Dictionary<string, object> parameters = new Dictionary<string, object> |
| { |
| ["addon"] = base64EncodedAddOn, |
| ["temporary"] = temporary |
| }; |
| Response response = this.Execute(InstallAddOnCommand, parameters); |
| |
| return (string)response.Value!; |
| } |
| |
| /// <summary> |
| /// Uninstalls a Firefox add-on. |
| /// </summary> |
| /// <param name="addOnId">The ID of the add-on to uninstall.</param> |
| /// <exception cref="ArgumentNullException">If <paramref name="addOnId"/> is null or empty.</exception> |
| public void UninstallAddOn(string addOnId) |
| { |
| if (string.IsNullOrEmpty(addOnId)) |
| { |
| throw new ArgumentNullException(nameof(addOnId), "Base64 encoded add-on must not be null or the empty string"); |
| } |
| |
| Dictionary<string, object> parameters = new Dictionary<string, object>(); |
| parameters["id"] = addOnId; |
| this.Execute(UninstallAddOnCommand, parameters); |
| } |
| |
| /// <summary> |
| /// Gets a <see cref="Screenshot"/> object representing the image of the full page on the screen. |
| /// </summary> |
| /// <returns>A <see cref="Screenshot"/> object containing the image.</returns> |
| public Screenshot GetFullPageScreenshot() |
| { |
| Response screenshotResponse = this.Execute(GetFullPageScreenshotCommand, null); |
| |
| screenshotResponse.EnsureValueIsNotNull(); |
| string base64 = screenshotResponse.Value.ToString()!; |
| return new Screenshot(base64); |
| } |
| |
| /// <summary> |
| /// In derived classes, the <see cref="PrepareEnvironment"/> method prepares the environment for test execution. |
| /// </summary> |
| protected virtual void PrepareEnvironment() |
| { |
| // Does nothing, but provides a hook for subclasses to do "stuff" |
| } |
| |
| /// <summary> |
| /// Disposes of the FirefoxDriver and frees all resources. |
| /// </summary> |
| /// <param name="disposing">A value indicating whether the user initiated the |
| /// disposal of the object. Pass <see langword="true"/> if the user is actively |
| /// disposing the object; otherwise <see langword="false"/>.</param> |
| protected override void Dispose(bool disposing) |
| { |
| base.Dispose(disposing); |
| } |
| |
| private static ICapabilities ConvertOptionsToCapabilities(FirefoxOptions options) |
| { |
| if (options == null) |
| { |
| throw new ArgumentNullException(nameof(options), "options must not be null"); |
| } |
| |
| return options.ToCapabilities(); |
| } |
| |
| private void AddCustomFirefoxCommands() |
| { |
| foreach (KeyValuePair<string, CommandInfo> entry in CustomCommandDefinitions) |
| { |
| this.RegisterInternalDriverCommand(entry.Key, entry.Value); |
| } |
| } |
| } |