diff --git a/dotnet/src/support/Extensions/WebDriverExtensions.cs b/dotnet/src/support/Extensions/WebDriverExtensions.cs index 8ef995719dfda..ec1145681d3ca 100644 --- a/dotnet/src/support/Extensions/WebDriverExtensions.cs +++ b/dotnet/src/support/Extensions/WebDriverExtensions.cs @@ -17,6 +17,7 @@ // under the License. // +using OpenQA.Selenium.Support.UI; using System; using System.Reflection; @@ -117,7 +118,7 @@ public static void ExecuteJavaScript(this IWebDriver driver, string script, para private static object? ExecuteJavaScriptInternal(IWebDriver driver, string script, object?[] args) { - IJavaScriptExecutor? executor = GetDriverAs(driver) + IJavaScriptExecutor executor = GetDriverAs(driver) ?? throw new WebDriverException("Driver does not implement IJavaScriptExecutor"); return executor.ExecuteScript(script, args); @@ -142,4 +143,18 @@ public static void ExecuteJavaScript(this IWebDriver driver, string script, para return convertedDriver; } + + /// + /// Performs an action that opens a new browser window, and returns an object that allows to switch between + /// the new and the old windows easily. + /// + /// The driver instance to extend. + /// The action delegate that should open the new browser window. + /// An instance of that allows to switch between the old and new windows. + public static WindowSwitcher WithWindowOpenedBy(this IWebDriver driver, Action actionThatOpensNewWindow) + { + var finder = new PopupWindowFinder(driver); + var newHandle = finder.Invoke(actionThatOpensNewWindow); + return new WindowSwitcher(driver, newHandle); + } } diff --git a/dotnet/src/support/UI/WindowSwitcher.cs b/dotnet/src/support/UI/WindowSwitcher.cs new file mode 100644 index 0000000000000..d200e79465631 --- /dev/null +++ b/dotnet/src/support/UI/WindowSwitcher.cs @@ -0,0 +1,132 @@ +using System; +using OpenQA.Selenium.Support.Extensions; + +namespace OpenQA.Selenium.Support.UI; + +/// +/// Provides a mechanism to easily switch between two browser windows. +/// +public class WindowSwitcher +{ + private readonly IWebDriver driver; + private readonly string newWindowHandle; + private readonly string originalHandle; + + /// + /// Initializes a new instance of the class. + /// + /// The instance that controls the two windows. + /// The handle of the new window. + /// Either or is null. + /// + /// + /// It is recommended to use the to instantiate this + /// class. + /// + /// + /// The current driver window handle is used to identify the existing window, while + /// should be the handle of another, newly opened, window. + /// + /// + public WindowSwitcher(IWebDriver driver, string newWindowHandle) + { + this.driver = driver ?? throw new ArgumentNullException(nameof(driver)); + this.newWindowHandle = newWindowHandle ?? throw new ArgumentNullException(nameof(newWindowHandle)); + originalHandle = this.driver.CurrentWindowHandle; + } + + /// + /// Performs the provided action on the newly opened window, and returns to use the original window afterward. + /// + /// The action to perform on the newly opened window. + /// + /// + /// driver.WithWindowOpenedBy(() => driver.FindElement(By.Id("openWindowId")).Click()) + /// .Do(() => { + /// // Perform whatever you want with the new window, for example: + /// driver.FindElement(By.Id("anElementOnTheNewWindow").Click(); + /// Assert.That(driver.Title, Is.EqualTo("The new window!")); + /// }); + /// // Then continue to do stuff on the original window: + /// driver.FindElement(By.Id("anElementOnTheOriginalWindow").Click(); + /// + /// + public void Do(Action action) + { + _ = Do(() => + { + action(); + return 0; + }); + } + + /// + /// Invokes the provided function on the newly opened window, and returns to use the original window afterward. This method returns whatever the function returns. + /// + /// The function to invoke on the newly opened window. + /// Anything that returns. + /// + /// + /// const string expectedResult = "abc"; + /// var result = driver.WithWindowOpenedBy(() => driver.FindElement(By.Id("openWindowId")).Click()) + /// .Do(() => { + /// // Perform whatever you want with the new window, for example: + /// driver.FindElement(By.Id("anElementOnTheNewWindow").Click(); + /// return driver.FindElement(By.Id("resultElement")).Text; + /// }); + /// Assert.That(result, Is.EqualTo(expected)); + /// // Then continue to do stuff on the original window: + /// driver.FindElement(By.Id("anElementOnTheOriginalWindow").Click(); + /// + /// + public T Do(Func func) + { + SwitchToNewWindow(); + try + { + return func(); + } + finally + { + SwitchToOriginalWindow(); + } + } + + /// + /// Switches to the new window. + /// + /// + /// The following example shows how to use and + /// to switch back and forth between the two windows: + /// + /// + /// var switcher = driver.WithWindowOpenedBy(() => driver.FindElement(By.Id("openWindowId")).Click()); + /// // Do some stuff on the original window: + /// driver.FindElement(By.Id("aButtonOnTheOriginalWindow")).Click(); + /// + /// // Do some stuff on the new window: + /// switcher.SwitchToNewWindow(); + /// driver.FindElement(By.Id("anInputOnTheNewWindow")).SendKeys("Hi"); + /// + /// // Do some more stuff on the original window: + /// switcher.SwitchToOriginalWindow(); + /// driver.FindElement(By.Id("aButtonOnTheOriginalWindow")).Click(); + /// + /// // And you can continue switching between the windows as you need... + /// + /// + /// + public void SwitchToNewWindow() + { + driver.SwitchTo().Window(newWindowHandle); + } + + /// + /// Switches to the original window. + /// + /// + public void SwitchToOriginalWindow() + { + driver.SwitchTo().Window(originalHandle); + } +} diff --git a/dotnet/test/support/UI/WindowSwitcherTest.cs b/dotnet/test/support/UI/WindowSwitcherTest.cs new file mode 100644 index 0000000000000..185267ca60975 --- /dev/null +++ b/dotnet/test/support/UI/WindowSwitcherTest.cs @@ -0,0 +1,49 @@ +using NUnit.Framework; +using OpenQA.Selenium.Environment; +using System.Threading.Tasks; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.Extensions; + +namespace Selenium.WebDriver.Support.Tests.UI; + +internal class WindowSwitcherTest : DriverTestFixture +{ + [OneTimeSetUp] + public async Task RunBeforeAnyTestAsync() + { + await EnvironmentManager.Instance.WebServer.StartAsync(); + } + + [OneTimeTearDown] + public async Task RunAfterAnyTestsAsync() + { + EnvironmentManager.Instance.CloseCurrentDriver(); + await EnvironmentManager.Instance.WebServer.StopAsync(); + } + + [Test] + public void SwitchesToNewWindowAndBack() + { + driver.Url = xhtmlTestPage; + var originalWindowHandle = driver.CurrentWindowHandle; + var delegateCalled = false; + var openWindowLink = driver.FindElement(By.LinkText("Open new window")); + driver.WithWindowOpenedBy(() => openWindowLink.Click()) + .Do(() => + { + delegateCalled = true; + Assert.Multiple(() => + { + Assert.That(driver.CurrentWindowHandle, Is.Not.EqualTo(originalWindowHandle)); + Assert.That(driver.Title, Is.EqualTo("We Arrive Here")); + }); + }); + + Assert.Multiple(() => + { + Assert.That(delegateCalled); + Assert.That(driver.CurrentWindowHandle, Is.EqualTo(originalWindowHandle)); + Assert.That(driver.Title, Is.EqualTo("XHTML Test Page")); + }); + } +}