Path: blob/trunk/dotnet/src/support/Events/EventFiringWebDriver.cs
2885 views
// <copyright file="EventFiringWebDriver.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 System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.Threading.Tasks; namespace OpenQA.Selenium.Support.Events; /// <summary> /// A wrapper around an arbitrary WebDriver instance which supports registering for /// events, e.g. for logging purposes. /// </summary> public class EventFiringWebDriver : IWebDriver, IJavaScriptExecutor, ITakesScreenshot, IWrapsDriver { /// <summary> /// Initializes a new instance of the <see cref="EventFiringWebDriver"/> class. /// </summary> /// <param name="parentDriver">The driver to register events for.</param> /// <exception cref="ArgumentNullException">If <paramref name="parentDriver"/> is <see langword="null"/>.</exception> public EventFiringWebDriver(IWebDriver parentDriver) { this.WrappedDriver = parentDriver ?? throw new ArgumentNullException(nameof(parentDriver)); } /// <summary> /// Fires before the driver begins navigation. /// </summary> public event EventHandler<WebDriverNavigationEventArgs>? Navigating; /// <summary> /// Fires after the driver completes navigation /// </summary> public event EventHandler<WebDriverNavigationEventArgs>? Navigated; /// <summary> /// Fires before the driver begins navigation back one entry in the browser history list. /// </summary> public event EventHandler<WebDriverNavigationEventArgs>? NavigatingBack; /// <summary> /// Fires after the driver completes navigation back one entry in the browser history list. /// </summary> public event EventHandler<WebDriverNavigationEventArgs>? NavigatedBack; /// <summary> /// Fires before the driver begins navigation forward one entry in the browser history list. /// </summary> public event EventHandler<WebDriverNavigationEventArgs>? NavigatingForward; /// <summary> /// Fires after the driver completes navigation forward one entry in the browser history list. /// </summary> public event EventHandler<WebDriverNavigationEventArgs>? NavigatedForward; /// <summary> /// Fires before the driver clicks on an element. /// </summary> public event EventHandler<WebElementEventArgs>? ElementClicking; /// <summary> /// Fires after the driver has clicked on an element. /// </summary> public event EventHandler<WebElementEventArgs>? ElementClicked; /// <summary> /// Fires before the driver changes the value of an element via Clear(), SendKeys() or Toggle(). /// </summary> public event EventHandler<WebElementValueEventArgs>? ElementValueChanging; /// <summary> /// Fires after the driver has changed the value of an element via Clear(), SendKeys() or Toggle(). /// </summary> public event EventHandler<WebElementValueEventArgs>? ElementValueChanged; /// <summary> /// Fires before the driver starts to find an element. /// </summary> public event EventHandler<FindElementEventArgs>? FindingElement; /// <summary> /// Fires after the driver completes finding an element. /// </summary> public event EventHandler<FindElementEventArgs>? FindElementCompleted; /// <summary> /// Fires before the driver starts to get a shadow root. /// </summary> public event EventHandler<GetShadowRootEventArgs>? GettingShadowRoot; /// <summary> /// Fires after the driver completes getting a shadow root. /// </summary> public event EventHandler<GetShadowRootEventArgs>? GetShadowRootCompleted; /// <summary> /// Fires before a script is executed. /// </summary> public event EventHandler<WebDriverScriptEventArgs>? ScriptExecuting; /// <summary> /// Fires after a script is executed. /// </summary> public event EventHandler<WebDriverScriptEventArgs>? ScriptExecuted; /// <summary> /// Fires when an exception is thrown. /// </summary> public event EventHandler<WebDriverExceptionEventArgs>? ExceptionThrown; /// <summary> /// Gets the <see cref="IWebDriver"/> wrapped by this EventsFiringWebDriver instance. /// </summary> public IWebDriver WrappedDriver { get; } /// <summary> /// Gets or sets the URL the browser is currently displaying. /// </summary> /// <remarks> /// Setting the <see cref="Url"/> property will load a new web page in the current browser window. /// This is done using an HTTP GET operation, and the method will block until the /// load is complete. This will follow redirects issued either by the server or /// as a meta-redirect from within the returned HTML. Should a meta-redirect "rest" /// for any duration of time, it is best to wait until this timeout is over, since /// should the underlying page change while your test is executing the results of /// future calls against this interface will be against the freshly loaded page. /// </remarks> /// <seealso cref="INavigation.GoToUrl(string)"/> /// <seealso cref="INavigation.GoToUrl(System.Uri)"/> public string Url { get { string url; try { url = this.WrappedDriver.Url; } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return url; } set { try { WebDriverNavigationEventArgs e = new WebDriverNavigationEventArgs(this.WrappedDriver, value); this.OnNavigating(e); this.WrappedDriver.Url = value; this.OnNavigated(e); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } } } /// <summary> /// Gets the title of the current browser window. /// </summary> public string Title { get { string title; try { title = this.WrappedDriver.Title; } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return title; } } /// <summary> /// Gets the source of the page last loaded by the browser. /// </summary> /// <remarks> /// If the page has been modified after loading (for example, by JavaScript) /// there is no guarantee that the returned text is that of the modified page. /// Please consult the documentation of the particular driver being used to /// determine whether the returned text reflects the current state of the page /// or the text last sent by the web server. The page source returned is a /// representation of the underlying DOM: do not expect it to be formatted /// or escaped in the same way as the response sent from the web server. /// </remarks> public string PageSource { get { string source; try { source = this.WrappedDriver.PageSource; } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return source; } } /// <summary> /// Gets the current window handle, which is an opaque handle to this /// window that uniquely identifies it within this driver instance. /// </summary> public string CurrentWindowHandle { get { string handle; try { handle = this.WrappedDriver.CurrentWindowHandle; } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return handle; } } /// <summary> /// Gets the window handles of open browser windows. /// </summary> public ReadOnlyCollection<string> WindowHandles { get { ReadOnlyCollection<string> handles; try { handles = this.WrappedDriver.WindowHandles; } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return handles; } } /// <summary> /// Close the current window, quitting the browser if it is the last window currently open. /// </summary> public void Close() { try { this.WrappedDriver.Close(); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } } /// <summary> /// Quits this driver, closing every associated window. /// </summary> public void Quit() { try { this.WrappedDriver.Quit(); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } } /// <summary> /// Instructs the driver to change its settings. /// </summary> /// <returns>An <see cref="IOptions"/> object allowing the user to change /// the settings of the driver.</returns> public IOptions Manage() { return new EventFiringOptions(this); } /// <summary> /// Instructs the driver to navigate the browser to another location. /// </summary> /// <returns>An <see cref="INavigation"/> object allowing the user to access /// the browser's history and to navigate to a given URL.</returns> public INavigation Navigate() { return new EventFiringNavigation(this); } /// <summary> /// Instructs the driver to send future commands to a different frame or window. /// </summary> /// <returns>An <see cref="ITargetLocator"/> object which can be used to select /// a frame or window.</returns> public ITargetLocator SwitchTo() { return new EventFiringTargetLocator(this); } /// <summary> /// Find the first <see cref="IWebElement"/> using the given method. /// </summary> /// <param name="by">The locating mechanism to use.</param> /// <returns>The first matching <see cref="IWebElement"/> on the current context.</returns> /// <exception cref="NoSuchElementException">If no element matches the criteria.</exception> public IWebElement FindElement(By by) { IWebElement wrappedElement; try { FindElementEventArgs e = new FindElementEventArgs(this.WrappedDriver, by); this.OnFindingElement(e); IWebElement element = this.WrappedDriver.FindElement(by); this.OnFindElementCompleted(e); wrappedElement = this.WrapElement(element); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return wrappedElement; } /// <summary> /// Find all <see cref="IWebElement">IWebElements</see> within the current context /// using the given mechanism. /// </summary> /// <param name="by">The locating mechanism to use.</param> /// <returns>A <see cref="ReadOnlyCollection{T}"/> of all <see cref="IWebElement">WebElements</see> /// matching the current criteria, or an empty list if nothing matches.</returns> public ReadOnlyCollection<IWebElement> FindElements(By by) { try { FindElementEventArgs e = new FindElementEventArgs(this.WrappedDriver, by); this.OnFindingElement(e); ReadOnlyCollection<IWebElement> elements = this.WrappedDriver.FindElements(by); this.OnFindElementCompleted(e); List<IWebElement> wrappedElementList = new List<IWebElement>(elements.Count); foreach (IWebElement element in elements) { IWebElement wrappedElement = this.WrapElement(element); wrappedElementList.Add(wrappedElement); } return wrappedElementList.AsReadOnly(); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } } /// <summary> /// Frees all managed and unmanaged resources used by this instance. /// </summary> public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Executes JavaScript in the context of the currently selected frame or window. /// </summary> /// <param name="script">The JavaScript code to execute.</param> /// <param name="args">The arguments to the script.</param> /// <returns>The value returned by the script.</returns> /// <remarks> /// <para> /// The ExecuteScript method executes JavaScript in the context of /// the currently selected frame or window. This means that "document" will refer /// to the current document. If the script has a return value, then the following /// steps will be taken: /// </para> /// <para> /// <list type="bullet"> /// <item><description>For an HTML element, this method returns a <see cref="IWebElement"/></description></item> /// <item><description>For a number, a <see cref="long"/> is returned</description></item> /// <item><description>For a boolean, a <see cref="bool"/> is returned</description></item> /// <item><description>For all other cases a <see cref="string"/> is returned.</description></item> /// <item><description>For an array,we check the first element, and attempt to return a /// <see cref="List{T}"/> of that type, following the rules above. Nested lists are not /// supported.</description></item> /// <item><description>If the value is null or there is no return value, /// <see langword="null"/> is returned.</description></item> /// </list> /// </para> /// <para> /// Arguments must be a number (which will be converted to a <see cref="long"/>), /// a <see cref="bool"/>, a <see cref="string"/> or a <see cref="IWebElement"/>, /// or a <see cref="IWrapsElement"/>. /// An exception will be thrown if the arguments do not meet these criteria. /// The arguments will be made available to the JavaScript via the "arguments" magic /// variable, as if the function were called via "Function.apply" /// </para> /// </remarks> public object? ExecuteScript(string script, params object?[] args) { if (this.WrappedDriver is not IJavaScriptExecutor javascriptDriver) { throw new NotSupportedException("Underlying driver instance does not support executing JavaScript"); } object? scriptResult; try { object?[] unwrappedArgs = UnwrapElementArguments(args); WebDriverScriptEventArgs e = new WebDriverScriptEventArgs(this.WrappedDriver, script); this.OnScriptExecuting(e); scriptResult = javascriptDriver.ExecuteScript(script, unwrappedArgs); this.OnScriptExecuted(e); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return scriptResult; } /// <summary> /// Executes JavaScript in the context of the currently selected frame or window. /// </summary> /// <param name="script">A <see cref="PinnedScript"/> object containing the code to execute.</param> /// <param name="args">The arguments to the script.</param> /// <returns>The value returned by the script.</returns> /// <exception cref="ArgumentNullException">If <paramref name="script"/> is <see langword="null"/>.</exception> /// <remarks> /// <para> /// The ExecuteScript method executes JavaScript in the context of /// the currently selected frame or window. This means that "document" will refer /// to the current document. If the script has a return value, then the following /// steps will be taken: /// </para> /// <para> /// <list type="bullet"> /// <item><description>For an HTML element, this method returns a <see cref="IWebElement"/></description></item> /// <item><description>For a number, a <see cref="long"/> is returned</description></item> /// <item><description>For a boolean, a <see cref="bool"/> is returned</description></item> /// <item><description>For all other cases a <see cref="string"/> is returned.</description></item> /// <item><description>For an array,we check the first element, and attempt to return a /// <see cref="List{T}"/> of that type, following the rules above. Nested lists are not /// supported.</description></item> /// <item><description>If the value is null or there is no return value, /// <see langword="null"/> is returned.</description></item> /// </list> /// </para> /// <para> /// Arguments must be a number (which will be converted to a <see cref="long"/>), /// a <see cref="bool"/>, a <see cref="string"/> or a <see cref="IWebElement"/>, /// or a <see cref="IWrapsElement"/>. /// An exception will be thrown if the arguments do not meet these criteria. /// The arguments will be made available to the JavaScript via the "arguments" magic /// variable, as if the function were called via "Function.apply" /// </para> /// </remarks> public object? ExecuteScript(PinnedScript script, params object?[] args) { if (script == null) { throw new ArgumentNullException(nameof(script)); } if (this.WrappedDriver is not IJavaScriptExecutor javascriptDriver) { throw new NotSupportedException("Underlying driver instance does not support executing JavaScript"); } object? scriptResult; try { object?[] unwrappedArgs = UnwrapElementArguments(args); WebDriverScriptEventArgs e = new WebDriverScriptEventArgs(this.WrappedDriver, script.Source); this.OnScriptExecuting(e); scriptResult = javascriptDriver.ExecuteScript(script, unwrappedArgs); this.OnScriptExecuted(e); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return scriptResult; } /// <summary> /// Executes JavaScript asynchronously in the context of the currently selected frame or window. /// </summary> /// <param name="script">The JavaScript code to execute.</param> /// <param name="args">The arguments to the script.</param> /// <returns>The value returned by the script.</returns> public object? ExecuteAsyncScript(string script, params object?[] args) { if (this.WrappedDriver is not IJavaScriptExecutor javascriptDriver) { throw new NotSupportedException("Underlying driver instance does not support executing JavaScript"); } object? scriptResult; try { object?[] unwrappedArgs = UnwrapElementArguments(args); WebDriverScriptEventArgs e = new WebDriverScriptEventArgs(this.WrappedDriver, script); this.OnScriptExecuting(e); scriptResult = javascriptDriver.ExecuteAsyncScript(script, unwrappedArgs); this.OnScriptExecuted(e); } catch (Exception ex) { this.OnException(new WebDriverExceptionEventArgs(this.WrappedDriver, ex)); throw; } return scriptResult; } /// <summary> /// Gets a <see cref="Screenshot"/> object representing the image of the page on the screen. /// </summary> /// <returns>A <see cref="Screenshot"/> object containing the image.</returns> public Screenshot GetScreenshot() { if (this.WrappedDriver is not ITakesScreenshot screenshotDriver) { throw new NotSupportedException("Underlying driver instance does not support taking screenshots"); } return screenshotDriver.GetScreenshot(); } /// <summary> /// Frees all managed and, optionally, unmanaged resources used by this instance. /// </summary> /// <param name="disposing"><see langword="true"/> to dispose of only managed resources; /// <see langword="false"/> to dispose of managed and unmanaged resources.</param> protected virtual void Dispose(bool disposing) { if (disposing) { this.WrappedDriver.Dispose(); } } /// <summary> /// Raises the <see cref="Navigating"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverNavigationEventArgs"/> that contains the event data.</param> protected virtual void OnNavigating(WebDriverNavigationEventArgs e) { if (this.Navigating != null) { this.Navigating(this, e); } } /// <summary> /// Raises the <see cref="Navigated"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverNavigationEventArgs"/> that contains the event data.</param> protected virtual void OnNavigated(WebDriverNavigationEventArgs e) { if (this.Navigated != null) { this.Navigated(this, e); } } /// <summary> /// Raises the <see cref="NavigatingBack"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverNavigationEventArgs"/> that contains the event data.</param> protected virtual void OnNavigatingBack(WebDriverNavigationEventArgs e) { if (this.NavigatingBack != null) { this.NavigatingBack(this, e); } } /// <summary> /// Raises the <see cref="NavigatedBack"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverNavigationEventArgs"/> that contains the event data.</param> protected virtual void OnNavigatedBack(WebDriverNavigationEventArgs e) { if (this.NavigatedBack != null) { this.NavigatedBack(this, e); } } /// <summary> /// Raises the <see cref="NavigatingForward"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverNavigationEventArgs"/> that contains the event data.</param> protected virtual void OnNavigatingForward(WebDriverNavigationEventArgs e) { if (this.NavigatingForward != null) { this.NavigatingForward(this, e); } } /// <summary> /// Raises the <see cref="NavigatedForward"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverNavigationEventArgs"/> that contains the event data.</param> protected virtual void OnNavigatedForward(WebDriverNavigationEventArgs e) { if (this.NavigatedForward != null) { this.NavigatedForward(this, e); } } /// <summary> /// Raises the <see cref="ElementClicking"/> event. /// </summary> /// <param name="e">A <see cref="WebElementEventArgs"/> that contains the event data.</param> protected virtual void OnElementClicking(WebElementEventArgs e) { if (this.ElementClicking != null) { this.ElementClicking(this, e); } } /// <summary> /// Raises the <see cref="ElementClicked"/> event. /// </summary> /// <param name="e">A <see cref="WebElementEventArgs"/> that contains the event data.</param> protected virtual void OnElementClicked(WebElementEventArgs e) { if (this.ElementClicked != null) { this.ElementClicked(this, e); } } /// <summary> /// Raises the <see cref="ElementValueChanging"/> event. /// </summary> /// <param name="e">A <see cref="WebElementValueEventArgs"/> that contains the event data.</param> protected virtual void OnElementValueChanging(WebElementValueEventArgs e) { if (this.ElementValueChanging != null) { this.ElementValueChanging(this, e); } } /// <summary> /// Raises the <see cref="ElementValueChanged"/> event. /// </summary> /// <param name="e">A <see cref="WebElementValueEventArgs"/> that contains the event data.</param> protected virtual void OnElementValueChanged(WebElementValueEventArgs e) { if (this.ElementValueChanged != null) { this.ElementValueChanged(this, e); } } /// <summary> /// Raises the <see cref="FindingElement"/> event. /// </summary> /// <param name="e">A <see cref="FindElementEventArgs"/> that contains the event data.</param> protected virtual void OnFindingElement(FindElementEventArgs e) { if (this.FindingElement != null) { this.FindingElement(this, e); } } /// <summary> /// Raises the <see cref="FindElementCompleted"/> event. /// </summary> /// <param name="e">A <see cref="FindElementEventArgs"/> that contains the event data.</param> protected virtual void OnFindElementCompleted(FindElementEventArgs e) { if (this.FindElementCompleted != null) { this.FindElementCompleted(this, e); } } /// <summary> /// Raises the <see cref="OnGettingShadowRoot"/> event. /// </summary> /// <param name="e">A <see cref="GetShadowRootEventArgs"/> that contains the event data.</param> protected virtual void OnGettingShadowRoot(GetShadowRootEventArgs e) { if (this.GettingShadowRoot != null) { this.GettingShadowRoot(this, e); } } /// <summary> /// Raises the <see cref="OnGetShadowRootCompleted"/> event. /// </summary> /// <param name="e">A <see cref="GetShadowRootEventArgs"/> that contains the event data.</param> protected virtual void OnGetShadowRootCompleted(GetShadowRootEventArgs e) { if (this.GetShadowRootCompleted != null) { this.GetShadowRootCompleted(this, e); } } /// <summary> /// Raises the <see cref="ScriptExecuting"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverScriptEventArgs"/> that contains the event data.</param> protected virtual void OnScriptExecuting(WebDriverScriptEventArgs e) { if (this.ScriptExecuting != null) { this.ScriptExecuting(this, e); } } /// <summary> /// Raises the <see cref="ScriptExecuted"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverScriptEventArgs"/> that contains the event data.</param> protected virtual void OnScriptExecuted(WebDriverScriptEventArgs e) { if (this.ScriptExecuted != null) { this.ScriptExecuted(this, e); } } /// <summary> /// Raises the <see cref="ExceptionThrown"/> event. /// </summary> /// <param name="e">A <see cref="WebDriverExceptionEventArgs"/> that contains the event data.</param> protected virtual void OnException(WebDriverExceptionEventArgs e) { if (this.ExceptionThrown != null) { this.ExceptionThrown(this, e); } } private static object?[] UnwrapElementArguments(object?[] args) { if (args is null) { throw new InvalidOperationException("Cannot unwrap null args"); } // Walk the args: the various drivers expect unwrapped versions of the elements List<object?> unwrappedArgs = new List<object?>(args.Length); foreach (object? arg in args) { if (arg is IWrapsElement eventElementArg) { unwrappedArgs.Add(eventElementArg.WrappedElement); } else { unwrappedArgs.Add(arg); } } return unwrappedArgs.ToArray(); } private IWebElement WrapElement(IWebElement underlyingElement) { return new EventFiringWebElement(this, underlyingElement); } /// <summary> /// Provides a mechanism for Navigating with the driver. /// </summary> private class EventFiringNavigation : INavigation { private readonly EventFiringWebDriver parentDriver; private readonly INavigation wrappedNavigation; /// <summary> /// Initializes a new instance of the <see cref="EventFiringNavigation"/> class /// </summary> /// <param name="driver">Driver in use</param> public EventFiringNavigation(EventFiringWebDriver driver) { this.parentDriver = driver ?? throw new ArgumentNullException(nameof(driver)); this.wrappedNavigation = this.parentDriver.WrappedDriver.Navigate(); } /// <summary> /// Move the browser back /// </summary> public void Back() { Task.Run(async delegate { await this.BackAsync(); }).GetAwaiter().GetResult(); } /// <summary> /// Move the browser back as an asynchronous task. /// </summary> /// <returns>A task object representing the asynchronous operation</returns> public async Task BackAsync() { try { WebDriverNavigationEventArgs e = new WebDriverNavigationEventArgs(this.parentDriver); this.parentDriver.OnNavigatingBack(e); await this.wrappedNavigation.BackAsync().ConfigureAwait(false); this.parentDriver.OnNavigatedBack(e); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Move a single "item" forward in the browser's history. /// </summary> public void Forward() { Task.Run(async delegate { await this.ForwardAsync(); }).GetAwaiter().GetResult(); } /// <summary> /// Move a single "item" forward in the browser's history as an asynchronous task. /// </summary> /// <returns>A task object representing the asynchronous operation.</returns> public async Task ForwardAsync() { try { WebDriverNavigationEventArgs e = new WebDriverNavigationEventArgs(this.parentDriver); this.parentDriver.OnNavigatingForward(e); await this.wrappedNavigation.ForwardAsync().ConfigureAwait(false); this.parentDriver.OnNavigatedForward(e); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Navigate to a url. /// </summary> /// <param name="url">String of where you want the browser to go to</param> public void GoToUrl(string url) { Task.Run(async delegate { await this.GoToUrlAsync(url); }).GetAwaiter().GetResult(); } /// <summary> /// Navigate to a url as an asynchronous task. /// </summary> /// <param name="url">String of where you want the browser to go.</param> /// <returns>A task object representing the asynchronous operation.</returns> public async Task GoToUrlAsync(string url) { if (url == null) { throw new ArgumentNullException(nameof(url), "url cannot be null"); } try { WebDriverNavigationEventArgs e = new WebDriverNavigationEventArgs(this.parentDriver, url); this.parentDriver.OnNavigating(e); await this.wrappedNavigation.GoToUrlAsync(url).ConfigureAwait(false); this.parentDriver.OnNavigated(e); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Navigate to a url. /// </summary> /// <param name="url">Uri object of where you want the browser to go to</param> public void GoToUrl(Uri url) { Task.Run(async delegate { await this.GoToUrlAsync(url); }).GetAwaiter().GetResult(); } /// <summary> /// Navigate to a url as an asynchronous task. /// </summary> /// <param name="url">Uri object of where you want the browser to go.</param> /// <returns>A task object representing the asynchronous operation.</returns> public async Task GoToUrlAsync(Uri url) { if (url == null) { throw new ArgumentNullException(nameof(url), "url cannot be null"); } await this.GoToUrlAsync(url.ToString()).ConfigureAwait(false); } /// <summary> /// Reload the current page. /// </summary> public void Refresh() { Task.Run(async delegate { await this.RefreshAsync(); }).GetAwaiter().GetResult(); } /// <summary> /// Reload the current page as an asynchronous task. /// </summary> /// <returns>A task object representing the asynchronous operation.</returns> public async Task RefreshAsync() { try { await this.wrappedNavigation.RefreshAsync().ConfigureAwait(false); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } } /// <summary> /// Provides a mechanism for setting options needed for the driver during the test. /// </summary> private class EventFiringOptions : IOptions { private readonly IOptions wrappedOptions; /// <summary> /// Initializes a new instance of the <see cref="EventFiringOptions"/> class /// </summary> /// <param name="driver">Instance of the driver currently in use</param> public EventFiringOptions(EventFiringWebDriver driver) { this.wrappedOptions = driver.WrappedDriver.Manage(); } /// <summary> /// Gets an object allowing the user to manipulate cookies on the page. /// </summary> public ICookieJar Cookies => this.wrappedOptions.Cookies; /// <summary> /// Gets an object allowing the user to manipulate the currently-focused browser window. /// </summary> /// <remarks>"Currently-focused" is defined as the browser window having the window handle /// returned when IWebDriver.CurrentWindowHandle is called.</remarks> public IWindow Window => this.wrappedOptions.Window; public ILogs Logs => this.wrappedOptions.Logs; public INetwork Network => this.wrappedOptions.Network; /// <summary> /// Provides access to the timeouts defined for this driver. /// </summary> /// <returns>An object implementing the <see cref="ITimeouts"/> interface.</returns> public ITimeouts Timeouts() { return new EventFiringTimeouts(this.wrappedOptions); } } /// <summary> /// Provides a mechanism for finding elements on the page with locators. /// </summary> private class EventFiringTargetLocator : ITargetLocator { private readonly ITargetLocator wrappedLocator; private readonly EventFiringWebDriver parentDriver; /// <summary> /// Initializes a new instance of the <see cref="EventFiringTargetLocator"/> class /// </summary> /// <param name="driver">The driver that is currently in use</param> public EventFiringTargetLocator(EventFiringWebDriver driver) { this.parentDriver = driver ?? throw new ArgumentNullException(nameof(driver)); this.wrappedLocator = this.parentDriver.WrappedDriver.SwitchTo(); } /// <summary> /// Move to a different frame using its index /// </summary> /// <param name="frameIndex">The index of the </param> /// <returns>A WebDriver instance that is currently in use</returns> public IWebDriver Frame(int frameIndex) { IWebDriver driver; try { driver = this.wrappedLocator.Frame(frameIndex); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Move to different frame using its name /// </summary> /// <param name="frameName">name of the frame</param> /// <returns>A WebDriver instance that is currently in use</returns> public IWebDriver Frame(string frameName) { IWebDriver driver; try { driver = this.wrappedLocator.Frame(frameName); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Move to a frame element. /// </summary> /// <param name="frameElement">a previously found FRAME or IFRAME element.</param> /// <returns>A WebDriver instance that is currently in use.</returns> public IWebDriver Frame(IWebElement frameElement) { IWebDriver driver; try { IWrapsElement wrapper = (IWrapsElement)frameElement; driver = this.wrappedLocator.Frame(wrapper.WrappedElement); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Select the parent frame of the currently selected frame. /// </summary> /// <returns>An <see cref="IWebDriver"/> instance focused on the specified frame.</returns> public IWebDriver ParentFrame() { IWebDriver driver; try { driver = this.wrappedLocator.ParentFrame(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Change to the Window by passing in the name /// </summary> /// <param name="windowName">name of the window that you wish to move to</param> /// <returns>A WebDriver instance that is currently in use</returns> public IWebDriver Window(string windowName) { IWebDriver driver; try { driver = this.wrappedLocator.Window(windowName); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Creates a new browser window and switches the focus for future commands /// of this driver to the new window. /// </summary> /// <param name="typeHint">The type of new browser window to be created. /// The created window is not guaranteed to be of the requested type; if /// the driver does not support the requested type, a new browser window /// will be created of whatever type the driver does support.</param> /// <returns>An <see cref="IWebDriver"/> instance focused on the new browser.</returns> public IWebDriver NewWindow(WindowType typeHint) { IWebDriver driver; try { driver = this.wrappedLocator.NewWindow(typeHint); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Change the active frame to the default /// </summary> /// <returns>Element of the default</returns> public IWebDriver DefaultContent() { IWebDriver driver; try { driver = this.wrappedLocator.DefaultContent(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return driver; } /// <summary> /// Finds the active element on the page and returns it /// </summary> /// <returns>Element that is active</returns> public IWebElement ActiveElement() { IWebElement element; try { element = this.wrappedLocator.ActiveElement(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return element; } /// <summary> /// Switches to the currently active modal dialog for this particular driver instance. /// </summary> /// <returns>A handle to the dialog.</returns> public IAlert Alert() { IAlert alert; try { alert = this.wrappedLocator.Alert(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return alert; } } /// <summary> /// Defines the interface through which the user can define timeouts. /// </summary> private class EventFiringTimeouts : ITimeouts { private readonly ITimeouts wrappedTimeouts; /// <summary> /// Initializes a new instance of the <see cref="EventFiringTimeouts"/> class /// </summary> /// <param name="options">The <see cref="IOptions"/> object to wrap.</param> public EventFiringTimeouts(IOptions options) { this.wrappedTimeouts = options.Timeouts(); } /// <summary> /// Gets or sets the implicit wait timeout, which is the amount of time the /// driver should wait when searching for an element if it is not immediately /// present. /// </summary> /// <remarks> /// When searching for a single element, the driver should poll the page /// until the element has been found, or this timeout expires before throwing /// a <see cref="NoSuchElementException"/>. When searching for multiple elements, /// the driver should poll the page until at least one element has been found /// or this timeout has expired. /// <para> /// Increasing the implicit wait timeout should be used judiciously as it /// will have an adverse effect on test run time, especially when used with /// slower location strategies like XPath. /// </para> /// </remarks> public TimeSpan ImplicitWait { get => this.wrappedTimeouts.ImplicitWait; set => this.wrappedTimeouts.ImplicitWait = value; } /// <summary> /// Gets or sets the asynchronous script timeout, which is the amount /// of time the driver should wait when executing JavaScript asynchronously. /// This timeout only affects the <see cref="IJavaScriptExecutor.ExecuteAsyncScript(string, object[])"/> /// method. /// </summary> public TimeSpan AsynchronousJavaScript { get => this.wrappedTimeouts.AsynchronousJavaScript; set => this.wrappedTimeouts.AsynchronousJavaScript = value; } /// <summary> /// Gets or sets the page load timeout, which is the amount of time the driver /// should wait for a page to load when setting the <see cref="IWebDriver.Url"/> /// property. /// </summary> public TimeSpan PageLoad { get => this.wrappedTimeouts.PageLoad; set => this.wrappedTimeouts.PageLoad = value; } } /// <summary> /// EventFiringWebElement allows you to have access to specific items that are found on the page /// </summary> private class EventFiringWebElement : ITakesScreenshot, IWebElement, IWrapsElement, IWrapsDriver { private readonly EventFiringWebDriver parentDriver; /// <summary> /// Initializes a new instance of the <see cref="EventFiringWebElement"/> class. /// </summary> /// <param name="driver">The <see cref="EventFiringWebDriver"/> instance hosting this element.</param> /// <param name="element">The <see cref="IWebElement"/> to wrap for event firing.</param> public EventFiringWebElement(EventFiringWebDriver driver, IWebElement element) { this.WrappedElement = element ?? throw new ArgumentNullException(nameof(element)); this.parentDriver = driver ?? throw new ArgumentNullException(nameof(driver)); } /// <summary> /// Gets the underlying wrapped <see cref="IWebElement"/>. /// </summary> public IWebElement WrappedElement { get; } /// <summary> /// Gets the underlying parent wrapped <see cref="IWebDriver"/> /// </summary> public IWebDriver WrappedDriver => this.parentDriver; /// <summary> /// Gets the DOM Tag of element /// </summary> public string TagName { get { string tagName; try { tagName = this.WrappedElement.TagName; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return tagName; } } /// <summary> /// Gets the text from the element /// </summary> public string Text { get { string text; try { text = this.WrappedElement.Text; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return text; } } /// <summary> /// Gets a value indicating whether an element is currently enabled /// </summary> public bool Enabled { get { bool enabled; try { enabled = this.WrappedElement.Enabled; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return enabled; } } /// <summary> /// Gets a value indicating whether this element is selected or not. This operation only applies to input elements such as checkboxes, options in a select and radio buttons. /// </summary> public bool Selected { get { bool selected; try { selected = this.WrappedElement.Selected; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return selected; } } /// <summary> /// Gets the Location of an element and returns a Point object /// </summary> public Point Location { get { Point location; try { location = this.WrappedElement.Location; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return location; } } /// <summary> /// Gets the <see cref="Size"/> of the element on the page /// </summary> public Size Size { get { Size size; try { size = this.WrappedElement.Size; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return size; } } /// <summary> /// Gets a value indicating whether the element is currently being displayed /// </summary> public bool Displayed { get { bool displayed; try { displayed = this.WrappedElement.Displayed; } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return displayed; } } /// <summary> /// Gets the underlying EventFiringWebDriver for this element. /// </summary> protected EventFiringWebDriver ParentDriver => this.parentDriver; /// <summary> /// Method to clear the text out of an Input element /// </summary> public void Clear() { try { WebElementValueEventArgs e = new WebElementValueEventArgs(this.parentDriver.WrappedDriver, this.WrappedElement, null); this.parentDriver.OnElementValueChanging(e); this.WrappedElement.Clear(); this.parentDriver.OnElementValueChanged(e); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Method for sending native key strokes to the browser /// </summary> /// <param name="text">String containing what you would like to type onto the screen</param> public void SendKeys(string text) { try { WebElementValueEventArgs e = new WebElementValueEventArgs(this.parentDriver.WrappedDriver, this.WrappedElement, text); this.parentDriver.OnElementValueChanging(e); this.WrappedElement.SendKeys(text); this.parentDriver.OnElementValueChanged(e); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// If this current element is a form, or an element within a form, then this will be submitted to the remote server. /// If this causes the current page to change, then this method will block until the new page is loaded. /// </summary> public void Submit() { try { this.WrappedElement.Submit(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Click this element. If this causes a new page to load, this method will block until /// the page has loaded. At this point, you should discard all references to this element /// and any further operations performed on this element will have undefined behavior unless /// you know that the element and the page will still be present. If this element is not /// clickable, then this operation is a no-op since it's pretty common for someone to /// accidentally miss the target when clicking in Real Life /// </summary> public void Click() { try { WebElementEventArgs e = new WebElementEventArgs(this.parentDriver.WrappedDriver, this.WrappedElement); this.parentDriver.OnElementClicking(e); this.WrappedElement.Click(); this.parentDriver.OnElementClicked(e); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// If this current element is a form, or an element within a form, then this will be submitted to the remote server. If this causes the current page to change, then this method will block until the new page is loaded. /// </summary> /// <param name="attributeName">Attribute you wish to get details of</param> /// <returns>The attribute's current value or null if the value is not set.</returns> public string? GetAttribute(string attributeName) { try { return this.WrappedElement.GetAttribute(attributeName); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Gets the value of a declared HTML attribute of this element. /// </summary> /// <param name="attributeName">The name of the HTML attribute to get the value of.</param> /// <returns>The HTML attribute's current value. Returns a <see langword="null"/> if the /// value is not set or the declared attribute does not exist.</returns> /// <remarks> /// As opposed to the <see cref="GetAttribute(string)"/> method, this method /// only returns attributes declared in the element's HTML markup. To access the value /// of an IDL property of the element, either use the <see cref="GetAttribute(string)"/> /// method or the <see cref="GetDomProperty(string)"/> method. /// </remarks> public string? GetDomAttribute(string attributeName) { try { return this.WrappedElement.GetDomAttribute(attributeName); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Gets the value of a JavaScript property of this element. /// </summary> /// <param name="propertyName">The name of the JavaScript property to get the value of.</param> /// <returns>The JavaScript property's current value. Returns a <see langword="null"/> if the /// value is not set or the property does not exist.</returns> public string? GetDomProperty(string propertyName) { try { return this.WrappedElement.GetDomProperty(propertyName); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Method to return the value of a CSS Property /// </summary> /// <param name="propertyName">CSS property key</param> /// <returns>string value of the CSS property</returns> public string GetCssValue(string propertyName) { string cssValue; try { cssValue = this.WrappedElement.GetCssValue(propertyName); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return cssValue; } /// <summary> /// Gets the representation of an element's shadow root for accessing the shadow DOM of a web component. /// </summary> /// <exception cref="NoSuchShadowRootException">Thrown when this element does not have a shadow root.</exception> /// <returns>A shadow root representation.</returns> public ISearchContext GetShadowRoot() { try { GetShadowRootEventArgs e = new GetShadowRootEventArgs(this.parentDriver.WrappedDriver, this.WrappedElement); this.parentDriver.OnGettingShadowRoot(e); ISearchContext shadowRoot = this.WrappedElement.GetShadowRoot(); this.parentDriver.OnGetShadowRootCompleted(e); return new EventFiringShadowRoot(this.parentDriver, shadowRoot); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Finds the first element in the page that matches the <see cref="By"/> object /// </summary> /// <param name="by">By mechanism to find the element</param> /// <returns>IWebElement object so that you can interaction that object</returns> public IWebElement FindElement(By by) { IWebElement wrappedElement; try { FindElementEventArgs e = new FindElementEventArgs(this.parentDriver.WrappedDriver, this.WrappedElement, by); this.parentDriver.OnFindingElement(e); IWebElement element = this.WrappedElement.FindElement(by); this.parentDriver.OnFindElementCompleted(e); wrappedElement = this.parentDriver.WrapElement(element); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return wrappedElement; } /// <summary> /// Finds the elements on the page by using the <see cref="By"/> object and returns a ReadOnlyCollection of the Elements on the page /// </summary> /// <param name="by">By mechanism to find the element</param> /// <returns>ReadOnlyCollection of IWebElement</returns> public ReadOnlyCollection<IWebElement> FindElements(By by) { try { FindElementEventArgs e = new FindElementEventArgs(this.parentDriver.WrappedDriver, this.WrappedElement, by); this.parentDriver.OnFindingElement(e); ReadOnlyCollection<IWebElement> elements = this.WrappedElement.FindElements(by); this.parentDriver.OnFindElementCompleted(e); List<IWebElement> wrappedElementList = new List<IWebElement>(elements.Count); foreach (IWebElement element in elements) { IWebElement wrappedElement = this.parentDriver.WrapElement(element); wrappedElementList.Add(wrappedElement); } return wrappedElementList.AsReadOnly(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Gets a <see cref="Screenshot"/> object representing the image of the page on the screen. /// </summary> /// <returns>A <see cref="Screenshot"/> object containing the image.</returns> public Screenshot GetScreenshot() { if (this.WrappedElement is not ITakesScreenshot screenshotDriver) { throw new NotSupportedException("Underlying element instance does not support taking screenshots"); } return screenshotDriver.GetScreenshot(); } /// <summary> /// Determines whether the specified <see cref="EventFiringWebElement"/> is equal to the current <see cref="EventFiringWebElement"/>. /// </summary> /// <param name="obj">The <see cref="EventFiringWebElement"/> to compare to the current <see cref="EventFiringWebElement"/>.</param> /// <returns><see langword="true"/> if the specified <see cref="EventFiringWebElement"/> is equal to the current <see cref="EventFiringWebElement"/>; otherwise, <see langword="false"/>.</returns> public override bool Equals(object obj) { if (obj is not IWebElement other) { return false; } if (other is IWrapsElement otherWrapper) { other = otherWrapper.WrappedElement; } return WrappedElement.Equals(other); } /// <summary> /// Return the hash code for this <see cref="EventFiringWebElement"/>. /// </summary> /// <returns>A 32-bit signed integer hash code.</returns> public override int GetHashCode() { return this.WrappedElement.GetHashCode(); } } /// <summary> /// EventFiringShadowElement allows you to have access to specific shadow elements /// </summary> private class EventFiringShadowRoot : ISearchContext, IWrapsDriver { private readonly EventFiringWebDriver parentDriver; /// <summary> /// Initializes a new instance of the <see cref="EventFiringShadowRoot"/> class. /// </summary> /// <param name="driver">The <see cref="EventFiringWebDriver"/> instance hosting this element.</param> /// <param name="searchContext">The <see cref="ISearchContext"/> to wrap for event firing.</param> public EventFiringShadowRoot(EventFiringWebDriver driver, ISearchContext searchContext) { this.WrappedSearchContext = searchContext ?? throw new ArgumentNullException(nameof(searchContext)); this.parentDriver = driver; } /// <summary> /// Gets the underlying wrapped <see cref="ISearchContext"/>. /// </summary> public ISearchContext WrappedSearchContext { get; } /// <summary> /// Gets the underlying parent wrapped <see cref="IWebDriver"/> /// </summary> public IWebDriver WrappedDriver => this.parentDriver; /// <summary> /// Finds the first element in the page that matches the <see cref="By"/> object /// </summary> /// <param name="by">By mechanism to find the element</param> /// <returns>IWebElement object so that you can interaction that object</returns> public IWebElement FindElement(By by) { IWebElement wrappedElement; try { GetShadowRootEventArgs e = new GetShadowRootEventArgs(this.parentDriver.WrappedDriver, this.WrappedSearchContext); this.parentDriver.OnGettingShadowRoot(e); IWebElement element = this.WrappedSearchContext.FindElement(by); this.parentDriver.OnGetShadowRootCompleted(e); wrappedElement = new EventFiringWebElement(this.parentDriver, element); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } return wrappedElement; } /// <summary> /// Finds the elements on the page by using the <see cref="By"/> object and returns a ReadOnlyCollection of the Elements on the page /// </summary> /// <param name="by">By mechanism to find the element</param> /// <returns>ReadOnlyCollection of IWebElement</returns> public ReadOnlyCollection<IWebElement> FindElements(By by) { try { GetShadowRootEventArgs e = new GetShadowRootEventArgs(this.parentDriver.WrappedDriver, this.WrappedSearchContext); this.parentDriver.OnGettingShadowRoot(e); ReadOnlyCollection<IWebElement> elements = this.WrappedSearchContext.FindElements(by); this.parentDriver.OnGetShadowRootCompleted(e); List<IWebElement> wrappedElementList = new List<IWebElement>(elements.Count); foreach (IWebElement element in elements) { IWebElement wrappedElement = this.parentDriver.WrapElement(element); wrappedElementList.Add(wrappedElement); } return wrappedElementList.AsReadOnly(); } catch (Exception ex) { this.parentDriver.OnException(new WebDriverExceptionEventArgs(this.parentDriver, ex)); throw; } } /// <summary> /// Determines whether the specified <see cref="EventFiringShadowRoot"/> is equal to the current <see cref="EventFiringShadowRoot"/>. /// </summary> /// <param name="obj">The <see cref="EventFiringWebElement"/> to compare to the current <see cref="EventFiringShadowRoot"/>.</param> /// <returns><see langword="true"/> if the specified <see cref="EventFiringShadowRoot"/> is equal to the current <see cref="EventFiringShadowRoot"/>; otherwise, <see langword="false"/>.</returns> public override bool Equals(object obj) { if (obj is not ISearchContext other) { return false; } return WrappedSearchContext.Equals(other); } /// <summary> /// Return the hash code for this <see cref="EventFiringWebElement"/>. /// </summary> /// <returns>A 32-bit signed integer hash code.</returns> public override int GetHashCode() { return this.WrappedSearchContext.GetHashCode(); } } }