diff --git a/docs/articles/building-with-legerity.md b/docs/articles/building-with-legerity.md index 9e3c061d..e39ba073 100644 --- a/docs/articles/building-with-legerity.md +++ b/docs/articles/building-with-legerity.md @@ -49,22 +49,23 @@ namespace MyWindowsApplication.UiTests using Legerity; using Legerity.Windows; + using Legerity.Windows.Extensions; using NUnit.Framework; using OpenQA.Selenium.Appium.Windows; using OpenQA.Selenium.Remote; - public abstract class BaseTestClass + public abstract class BaseTestClass : LegerityTestClass { public const string WindowsApplication = "com.madeapps.sampleapp_7mzr475ysvhxg!App"; - protected static WindowsDriver App => AppManager.WindowsApp; + protected WindowsDriver WindowsApp { get; private set; } [SetUp] - public void StartApp() + public void Initialize() { - AppManager.StartApp( + WindowsApp = this.StartWindowsApp( new WindowsAppManagerOptions(WindowsApplication) { DriverUri = "http://127.0.0.1:4723", @@ -74,9 +75,9 @@ namespace MyWindowsApplication.UiTests } [TearDown] - public void StopApp() + public void Cleanup() { - AppManager.StopApp(); + StopApp(WindowsApp); } } } @@ -84,7 +85,7 @@ namespace MyWindowsApplication.UiTests ### Example base test class for an Android application -This example showcases how to start your Android application using a path to a compiled APK. You can also choose to launch your application by application ID and activity if the application is already installed. +This example showcases how to start your Android application using the ID and activity of an app that is already installed on a device. You can also choose to launch your application by APK if the application is not installed. When your tests start, the `LaunchAppiumServer` property of the `AndroidAppManagerOptions` will automatically start a new Appium server process, if one is not already running. This support allows you to easily run your tests as part of a CI build without additional overhead of scripting the process to run. @@ -101,33 +102,94 @@ namespace MyAndroidApplication.UiTests using Legerity; using Legerity.Android; + using Legerity.Android.Extensions; using NUnit.Framework; using OpenQA.Selenium.Appium.Android; using OpenQA.Selenium.Remote; - public abstract class BaseTestClass + public abstract class BaseTestClass : LegerityTestClass { - public const string AndroidApplication = "Tools\\Android\\com.made.sampleapp.apk"; + public const string AndroidApplication = "com.made.sampleapp"; + + public const string AndroidApplicationActivity = $"{AndroidApplication}.MainActivity"; - protected static AndroidDriver App => AppManager.AndroidApp; + protected AndroidDriver AndroidApp { get; private set; } [SetUp] - public void StartApp() + public void Initialize() { - AppManager.StartApp( - new AndroidAppManagerOptions(Path.Combine(Environment.CurrentDirectory, AndroidApplication)) + AndroidApp = this.StartAndroidApp( + new AndroidAppManagerOptions { + AppId = AndroidApplication, + AppActivity = AndroidApplicationActivity, LaunchAppiumServer = true, DriverUri = "http://localhost:4723/wd/hub" }); } [TearDown] - public void StopApp() + public void Cleanup() { - AppManager.StopApp(); + StopApp(AndroidApp); + } + } +} +``` + +### Example base test class for an iOS application + +This example showcases how to start your iOS application using the ID of an app that is already installed on a device. + +When your tests start, the `LaunchAppiumServer` property of the `IOSAppManagerOptions` will automatically start a new Appium server process, if one is not already running. This support allows you to easily run your tests as part of a CI build without additional overhead of scripting the process to run. + +You also have to provide additional properties that allows you to choose the device that the application should be under test on using the `DeviceName` or `DeviceId` property. This is useful if you wish to pick a specific emulator or physical device. + +```csharp +namespace MyIOSApplication.UiTests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using Legerity; + using Legerity.IOS; + using Legerity.IOS.Extensions; + + using NUnit.Framework; + + using OpenQA.Selenium.Appium.IOS; + using OpenQA.Selenium.Remote; + + public abstract class BaseTestClass : LegerityTestClass + { + public const string IOSApplication = "com.made.sampleapp"; + + protected IOSDriver IOSApp { get; private set; } + + [SetUp] + public void Initialize() + { + IOSApp = this.StartIOSApp( + new IOSAppManagerOptions + { + AppId = IOSApplication, + DeviceName = "iPhone SE (3rd generation) Simulator", + DeviceId = "56755E6F-741B-478F-BB1B-A48E05ACFE8A", + OSVersion = "15.4", + LaunchAppiumServer = true, + DriverUri = "http://localhost:4723/wd/hub" + }); + } + + [TearDown] + public void Cleanup() + { + StopApp(IOSApp); } } } @@ -137,6 +199,8 @@ namespace MyAndroidApplication.UiTests This example showcases how to start your web application using a URL using a specific browser and associated driver. This example is using the Microsoft Edge browser with a path to the [Microsoft Edge Web Driver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/). +You can also use the web driver NuGet packages in your test project to automatically pull in the specific version of a web drive you require. + When your tests start, the web browser will launch the URL provided to begin running the test scenarios. You also have additional properties that allows you to maximize the browser window, or set the size. This is useful in scenarios where your application has responsive UI and you wish to test it under those views. @@ -159,16 +223,14 @@ namespace MyWebApplication.UiTests using OpenQA.Selenium.Remote; - public abstract class BaseTestClass + public abstract class BaseTestClass : LegerityTestClass { public const string WebApplication = "http://localhost:5000"; - protected static RemoteWebDriver App => AppManager.WebApp; - [SetUp] - public void StartApp() + public void Initialize() { - AppManager.StartApp( + this.StartApp( new WebAppManagerOptions(WebAppDriverType.Edge, Path.Combine(Environment.CurrentDirectory, "Tools\\Edge")) { Maximize = true, @@ -178,9 +240,9 @@ namespace MyWebApplication.UiTests } [TearDown] - public void StopApp() + public void Cleanup() { - AppManager.StopApp(); + this.StopApp(); } } } @@ -216,7 +278,7 @@ namespace MyApplication.UiTests using OpenQA.Selenium.Appium.Windows; using OpenQA.Selenium.Remote; - public abstract class BaseTestClass + public abstract class BaseTestClass : LegerityTestClass { public const string AndroidApplication = "Tools\\Android\\com.made.sampleapp.apk"; @@ -225,36 +287,11 @@ namespace MyApplication.UiTests public const string WindowsApplication = "com.madeapps.sampleapp_7mzr475ysvhxg!App"; protected BaseTestClass(AppManagerOptions options) + : base(options) { - Options = options; - } - - protected static RemoteWebDriver App => AppManager.App; - - protected AppManagerOptions Options { get; } - - [SetUp] - public void StartApp() - { - AppManager.StartApp(Options); } - [TearDown] - public void StopApp() - { - AppManager.StopApp(); - Options = null; - } - } - - [TestFixtureSource(nameof(TestPlatformOptions))] - public class LoginPageTests : BaseTestClass - { - public LoginPageTests(AppManagerOptions options) : base(options) - { - } - - static IEnumerable TestPlatformOptions => new List + protected static IEnumerable TestPlatformOptions => new List { new AndroidAppManagerOptions(Path.Combine(Environment.CurrentDirectory, AndroidApplication)) { @@ -276,6 +313,26 @@ namespace MyApplication.UiTests Maximize = true } }; + + [SetUp] + public void Initialize() + { + this.StartApp(); + } + + [TearDown] + public void Cleanup() + { + this.StopApp(); + } + } + + [TestFixtureSource(nameof(TestPlatformOptions))] + public class LoginPageTests : BaseTestClass + { + public LoginPageTests(AppManagerOptions options) : base(options) + { + } } } ``` diff --git a/samples/W3SchoolsWebTests/BaseTestClass.cs b/samples/W3SchoolsWebTests/BaseTestClass.cs index f415b3c6..cc669661 100644 --- a/samples/W3SchoolsWebTests/BaseTestClass.cs +++ b/samples/W3SchoolsWebTests/BaseTestClass.cs @@ -4,6 +4,7 @@ namespace W3SchoolsWebTests using Legerity; using NUnit.Framework; using OpenQA.Selenium; + using OpenQA.Selenium.Remote; public abstract class BaseTestClass : LegerityTestClass { @@ -11,18 +12,32 @@ public abstract class BaseTestClass : LegerityTestClass /// Initializes a new instance of the class with application launch option. /// /// The application launch options. - protected BaseTestClass(AppManagerOptions options) : base(options) + protected BaseTestClass(AppManagerOptions options) + : base(options) { } + public bool IsParallelized { get; set; } = false; + [SetUp] public virtual void Initialize() { - base.StartApp(); + if (!this.IsParallelized) + { + this.StartApp(); + } + } + + public override RemoteWebDriver StartApp( + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + RemoteWebDriver app = base.StartApp(waitUntil, waitUntilTimeout, waitUntilRetries); try { - IWebElement closePopup = AppManager.WebApp.FindElement(By.Id("accept-choices")); + IWebElement closePopup = app.FindElement(By.Id("accept-choices")); closePopup?.Click(); } catch (Exception) @@ -30,13 +45,18 @@ public virtual void Initialize() // Ignored. } - AppManager.WebApp.SwitchTo().Frame("iframeResult"); + app.SwitchTo().Frame("iframeResult"); + + return app; } [TearDown] public virtual void Cleanup() { - base.StopApp(); + if (!this.IsParallelized) + { + base.StopApp(); + } } } } \ No newline at end of file diff --git a/samples/W3SchoolsWebTests/Tests/ButtonTests.cs b/samples/W3SchoolsWebTests/Tests/ButtonTests.cs index 53ce97b3..facc7425 100644 --- a/samples/W3SchoolsWebTests/Tests/ButtonTests.cs +++ b/samples/W3SchoolsWebTests/Tests/ButtonTests.cs @@ -15,7 +15,8 @@ public class ButtonTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_button_test"; - public ButtonTests(AppManagerOptions options) : base(options) + public ButtonTests(AppManagerOptions options) + : base(options) { } @@ -38,7 +39,7 @@ public ButtonTests(AppManagerOptions options) : base(options) [Test] public void ShouldClickButton() { - Button button = AppManager.WebApp.FindElementByTagName("button") as RemoteWebElement; + Button button = this.App.FindElementByTagName("button") as RemoteWebElement; button.Click(); } } diff --git a/samples/W3SchoolsWebTests/Tests/CheckBoxTests.cs b/samples/W3SchoolsWebTests/Tests/CheckBoxTests.cs index 232ffc26..9a4da35e 100644 --- a/samples/W3SchoolsWebTests/Tests/CheckBoxTests.cs +++ b/samples/W3SchoolsWebTests/Tests/CheckBoxTests.cs @@ -12,12 +12,15 @@ namespace W3SchoolsWebTests.Tests using W3SchoolsWebTests; [TestFixtureSource(nameof(TestPlatformOptions))] + [Parallelizable(ParallelScope.Fixtures)] public class CheckBoxTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_checkbox"; - public CheckBoxTests(AppManagerOptions options) : base(options) + public CheckBoxTests(AppManagerOptions options) + : base(options) { + this.IsParallelized = true; } static IEnumerable TestPlatformOptions => new List @@ -39,21 +42,31 @@ public CheckBoxTests(AppManagerOptions options) : base(options) [TestCase("vehicle1")] [TestCase("vehicle2")] [TestCase("vehicle3")] + [Parallelizable(ParallelScope.Children)] public void ShouldCheckOn(string checkboxId) { - CheckBox checkbox = AppManager.WebApp.FindElementById(checkboxId) as RemoteWebElement; + RemoteWebDriver app = this.StartApp(); + + CheckBox checkbox = app.FindElementById(checkboxId) as RemoteWebElement; checkbox.CheckOn(); checkbox.IsChecked.ShouldBeTrue(); + + this.StopApp(app); } [TestCase("vehicle1")] [TestCase("vehicle2")] [TestCase("vehicle3")] + [Parallelizable(ParallelScope.Children)] public void ShouldCheckOff(string checkboxId) { - CheckBox checkbox = AppManager.WebApp.FindElementById(checkboxId) as RemoteWebElement; + RemoteWebDriver app = this.StartApp(); + + CheckBox checkbox = app.FindElementById(checkboxId) as RemoteWebElement; checkbox.CheckOff(); checkbox.IsChecked.ShouldBeFalse(); + + this.StopApp(app); } } } \ No newline at end of file diff --git a/samples/W3SchoolsWebTests/Tests/FileInputTests.cs b/samples/W3SchoolsWebTests/Tests/FileInputTests.cs index 36c47fb4..a100fd79 100644 --- a/samples/W3SchoolsWebTests/Tests/FileInputTests.cs +++ b/samples/W3SchoolsWebTests/Tests/FileInputTests.cs @@ -6,6 +6,7 @@ namespace W3SchoolsWebTests.Tests using Legerity; using Legerity.Web; using Legerity.Web.Elements.Core; + using Legerity.Web.Extensions; using NUnit.Framework; using OpenQA.Selenium.Remote; using Shouldly; @@ -16,7 +17,8 @@ public class FileInputTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_file"; - public FileInputTests(AppManagerOptions options) : base(options) + public FileInputTests(AppManagerOptions options) + : base(options) { } @@ -41,9 +43,11 @@ public void ShouldSetAbsoluteFilePath() { string filePath = Path.Combine(Environment.CurrentDirectory, @"msedgedriver.exe"); - FileInput fileInput = AppManager.WebApp.FindElementById("myfile") as RemoteWebElement; + FileInput fileInput = this.App.FindElementById("myfile") as RemoteWebElement; fileInput.SetAbsoluteFilePath(filePath); + fileInput.WaitUntil(e => e.FilePath.Contains("msedgedriver"), TimeSpan.FromSeconds(5)); + // Cannot check absolute file path as browser security feature prevents seeing full URI. fileInput.FilePath.ShouldContain("msedgedriver.exe"); } @@ -53,7 +57,7 @@ public void ShouldClearFile() { string filePath = Path.Combine(Environment.CurrentDirectory, @"msedgedriver.exe"); - FileInput fileInput = AppManager.WebApp.FindElementById("myfile") as RemoteWebElement; + FileInput fileInput = this.App.FindElementById("myfile") as RemoteWebElement; fileInput.SetAbsoluteFilePath(filePath); fileInput.ClearFile(); diff --git a/samples/W3SchoolsWebTests/Tests/ImageTests.cs b/samples/W3SchoolsWebTests/Tests/ImageTests.cs index d3c176bb..1855826f 100644 --- a/samples/W3SchoolsWebTests/Tests/ImageTests.cs +++ b/samples/W3SchoolsWebTests/Tests/ImageTests.cs @@ -16,7 +16,8 @@ public class ImageTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_image_test"; - public ImageTests(AppManagerOptions options) : base(options) + public ImageTests(AppManagerOptions options) + : base(options) { } @@ -39,21 +40,21 @@ public ImageTests(AppManagerOptions options) : base(options) [Test] public void ShouldGetImageSource() { - Image image = AppManager.WebApp.FindElementByTagName("img") as RemoteWebElement; + Image image = this.App.FindElementByTagName("img") as RemoteWebElement; image.Source.ShouldContain("img_girl.jpg"); } [Test] public void ShouldGetAltText() { - Image image = AppManager.WebApp.FindElementByTagName("img") as RemoteWebElement; + Image image = this.App.FindElementByTagName("img") as RemoteWebElement; image.AltText.ShouldBe("Girl in a jacket"); } [Test] public void ShouldGetSize() { - Image image = AppManager.WebApp.FindElementByTagName("img") as RemoteWebElement; + Image image = this.App.FindElementByTagName("img") as RemoteWebElement; image.Width.ShouldBe(500); image.Height.ShouldBe(600); } diff --git a/samples/W3SchoolsWebTests/Tests/NumberInputTests.cs b/samples/W3SchoolsWebTests/Tests/NumberInputTests.cs index f397445d..ebab1220 100644 --- a/samples/W3SchoolsWebTests/Tests/NumberInputTests.cs +++ b/samples/W3SchoolsWebTests/Tests/NumberInputTests.cs @@ -16,7 +16,8 @@ public class NumberInputTests : BaseTestClass { private const string Url = "https://www.w3schools.com/html/tryit.asp?filename=tryhtml_input_number"; - public NumberInputTests(AppManagerOptions options) : base(options) + public NumberInputTests(AppManagerOptions options) + : base(options) { } @@ -39,7 +40,7 @@ public NumberInputTests(AppManagerOptions options) : base(options) [Test] public void ShouldGetValueRange() { - NumberInput quantity = AppManager.WebApp.FindElementById("quantity") as RemoteWebElement; + NumberInput quantity = this.App.FindElementById("quantity") as RemoteWebElement; quantity.Minimum.ShouldBe(1); quantity.Maximum.ShouldBe(5); } @@ -47,7 +48,7 @@ public void ShouldGetValueRange() [Test] public void ShouldSetValue() { - NumberInput quantity = AppManager.WebApp.FindElementById("quantity") as RemoteWebElement; + NumberInput quantity = this.App.FindElementById("quantity") as RemoteWebElement; quantity.SetValue(4); quantity.Value.ShouldBe(4); } @@ -55,7 +56,7 @@ public void ShouldSetValue() [Test] public void ShouldIncrement() { - NumberInput quantity = AppManager.WebApp.FindElementById("quantity") as RemoteWebElement; + NumberInput quantity = this.App.FindElementById("quantity") as RemoteWebElement; quantity.SetValue(3); quantity.Increment(); quantity.Value.ShouldBe(4); @@ -64,7 +65,7 @@ public void ShouldIncrement() [Test] public void ShouldDecrement() { - NumberInput quantity = AppManager.WebApp.FindElementById("quantity") as RemoteWebElement; + NumberInput quantity = this.App.FindElementById("quantity") as RemoteWebElement; quantity.SetValue(3); quantity.Decrement(); quantity.Value.ShouldBe(2); diff --git a/samples/W3SchoolsWebTests/Tests/OrderedListTests.cs b/samples/W3SchoolsWebTests/Tests/OrderedListTests.cs index e6936957..1255182f 100644 --- a/samples/W3SchoolsWebTests/Tests/OrderedListTests.cs +++ b/samples/W3SchoolsWebTests/Tests/OrderedListTests.cs @@ -19,7 +19,8 @@ public class OrderedListTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_lists"; - public OrderedListTests(AppManagerOptions options) : base(options) + public OrderedListTests(AppManagerOptions options) + : base(options) { } @@ -42,7 +43,7 @@ public OrderedListTests(AppManagerOptions options) : base(options) [Test] public void ShouldContainItems() { - ReadOnlyCollection lists = AppManager.WebApp.FindElementsByTagName("ol"); + ReadOnlyCollection lists = this.App.FindElementsByTagName("ol"); List list = lists.FirstOrDefault() as RemoteWebElement; list.Items.Count.ShouldBe(3); diff --git a/samples/W3SchoolsWebTests/Tests/RadioButtonTests.cs b/samples/W3SchoolsWebTests/Tests/RadioButtonTests.cs index 0ef54d3e..8d697832 100644 --- a/samples/W3SchoolsWebTests/Tests/RadioButtonTests.cs +++ b/samples/W3SchoolsWebTests/Tests/RadioButtonTests.cs @@ -15,12 +15,15 @@ namespace W3SchoolsWebTests.Tests using W3SchoolsWebTests; [TestFixtureSource(nameof(TestPlatformOptions))] + [Parallelizable(ParallelScope.Fixtures)] public class RadioButtonTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_radio"; - public RadioButtonTests(AppManagerOptions options) : base(options) + public RadioButtonTests(AppManagerOptions options) + : base(options) { + this.IsParallelized = true; } static IEnumerable TestPlatformOptions => new List @@ -45,18 +48,26 @@ public RadioButtonTests(AppManagerOptions options) : base(options) [TestCase("age1")] [TestCase("age2")] [TestCase("age3")] + [Parallelizable(ParallelScope.Children)] public void ShouldSelect(string radioId) { - RadioButton radioButton = AppManager.WebApp.FindElementById(radioId) as RemoteWebElement; + RemoteWebDriver app = this.StartApp(); + + RadioButton radioButton = app.FindElementById(radioId) as RemoteWebElement; radioButton.Click(); radioButton.IsSelected.ShouldBeTrue(); + + this.StopApp(app); } [TestCase("fav_language", "html", "javascript")] [TestCase("age", "age3", "age2")] + [Parallelizable(ParallelScope.Children)] public void ShouldOnlySelectOneInGroup(string groupName, string initialRadioId, string expectedRadioId) { - ReadOnlyCollection radioButtons = AppManager.WebApp.FindElements(By.Name(groupName)); + RemoteWebDriver app = this.StartApp(); + + ReadOnlyCollection radioButtons = app.FindElements(By.Name(groupName)); RadioButton initialRadioButton = radioButtons.FirstOrDefault(x => x.GetAttribute("id") == initialRadioId) as RemoteWebElement; initialRadioButton.Click(); initialRadioButton.IsSelected.ShouldBeTrue(); @@ -65,6 +76,8 @@ public void ShouldOnlySelectOneInGroup(string groupName, string initialRadioId, expectedRadioButton.Click(); expectedRadioButton.IsSelected.ShouldBeTrue(); initialRadioButton.IsSelected.ShouldBeFalse(); + + this.StopApp(app); } } } \ No newline at end of file diff --git a/samples/W3SchoolsWebTests/Tests/RangeInputTests.cs b/samples/W3SchoolsWebTests/Tests/RangeInputTests.cs index e527d480..f9537b7b 100644 --- a/samples/W3SchoolsWebTests/Tests/RangeInputTests.cs +++ b/samples/W3SchoolsWebTests/Tests/RangeInputTests.cs @@ -16,7 +16,8 @@ public class RangeInputTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_range"; - public RangeInputTests(AppManagerOptions options) : base(options) + public RangeInputTests(AppManagerOptions options) + : base(options) { } @@ -39,7 +40,7 @@ public RangeInputTests(AppManagerOptions options) : base(options) [Test] public void ShouldGetValueRange() { - RangeInput volume = AppManager.WebApp.FindElementById("vol") as RemoteWebElement; + RangeInput volume = this.App.FindElementById("vol") as RemoteWebElement; volume.Minimum.ShouldBe(0); volume.Maximum.ShouldBe(50); } @@ -47,7 +48,7 @@ public void ShouldGetValueRange() [Test] public void ShouldSetValue() { - RangeInput volume = AppManager.WebApp.FindElementById("vol") as RemoteWebElement; + RangeInput volume = this.App.FindElementById("vol") as RemoteWebElement; volume.SetValue(20); volume.Value.ShouldBe(20); } @@ -55,7 +56,7 @@ public void ShouldSetValue() [Test] public void ShouldIncrement() { - RangeInput volume = AppManager.WebApp.FindElementById("vol") as RemoteWebElement; + RangeInput volume = this.App.FindElementById("vol") as RemoteWebElement; volume.SetValue(30); volume.Increment(); volume.Value.ShouldBe(31); @@ -64,7 +65,7 @@ public void ShouldIncrement() [Test] public void ShouldDecrement() { - RangeInput volume = AppManager.WebApp.FindElementById("vol") as RemoteWebElement; + RangeInput volume = this.App.FindElementById("vol") as RemoteWebElement; volume.SetValue(20); volume.Decrement(); volume.Value.ShouldBe(19); diff --git a/samples/W3SchoolsWebTests/Tests/SelectTests.cs b/samples/W3SchoolsWebTests/Tests/SelectTests.cs index 143e0d03..fb4d941d 100644 --- a/samples/W3SchoolsWebTests/Tests/SelectTests.cs +++ b/samples/W3SchoolsWebTests/Tests/SelectTests.cs @@ -17,7 +17,8 @@ public class SelectTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_select"; - public SelectTests(AppManagerOptions options) : base(options) + public SelectTests(AppManagerOptions options) + : base(options) { } @@ -40,7 +41,7 @@ public SelectTests(AppManagerOptions options) : base(options) [Test] public void ShouldGetItems() { - Select carsSelect = AppManager.WebApp.FindElementById("cars") as RemoteWebElement; + Select carsSelect = this.App.FindElementById("cars") as RemoteWebElement; carsSelect.Options.Count().ShouldBe(4); var itemValues = carsSelect.Options.Select(e => e.Value).ToList(); @@ -53,7 +54,7 @@ public void ShouldGetItems() [Test] public void ShouldGetIsMultiple() { - Select carsSelect = AppManager.WebApp.FindElementById("cars") as RemoteWebElement; + Select carsSelect = this.App.FindElementById("cars") as RemoteWebElement; carsSelect.IsMultiple.ShouldBe(false); } @@ -63,7 +64,7 @@ public void ShouldGetIsMultiple() [TestCase("audi")] public void ShouldSelectOptionByValue(string value) { - Select carsSelect = AppManager.WebApp.FindElementById("cars") as RemoteWebElement; + Select carsSelect = this.App.FindElementById("cars") as RemoteWebElement; carsSelect.SelectOptionByValue(value); Option selectedOption = carsSelect.SelectedOption; @@ -76,7 +77,7 @@ public void ShouldSelectOptionByValue(string value) [TestCase("Audi")] public void ShouldSelectOptionByDisplayValue(string value) { - Select carsSelect = AppManager.WebApp.FindElementById("cars") as RemoteWebElement; + Select carsSelect = this.App.FindElementById("cars") as RemoteWebElement; carsSelect.SelectOptionByDisplayValue(value); Option selectedOption = carsSelect.SelectedOption; diff --git a/samples/W3SchoolsWebTests/Tests/TableTests.cs b/samples/W3SchoolsWebTests/Tests/TableTests.cs index 961fc07b..24db02e1 100644 --- a/samples/W3SchoolsWebTests/Tests/TableTests.cs +++ b/samples/W3SchoolsWebTests/Tests/TableTests.cs @@ -17,7 +17,8 @@ public class TableTests : BaseTestClass { private const string Url = "https://www.w3schools.com/html/tryit.asp?filename=tryhtml_table_intro"; - public TableTests(AppManagerOptions options) : base(options) + public TableTests(AppManagerOptions options) + : base(options) { } @@ -41,7 +42,7 @@ public TableTests(AppManagerOptions options) : base(options) public void ShouldGetHeaders() { // Arrange - Table table = AppManager.WebApp.FindWebElement(By.TagName("table")); + Table table = this.App.FindWebElement(By.TagName("table")); // Act var headers = table.Headers.ToList(); @@ -65,7 +66,7 @@ public void ShouldGetRows() new List {"Magazzini Alimentari Riuniti", "Giovanni Rovelli", "Italy"}, }; - Table table = AppManager.WebApp.FindWebElement(By.TagName("table")); + Table table = this.App.FindWebElement(By.TagName("table")); // Act var rows = table.Rows.ToList(); @@ -92,7 +93,7 @@ public void ShouldGetDataRows() new List {"Magazzini Alimentari Riuniti", "Giovanni Rovelli", "Italy"}, }; - Table table = AppManager.WebApp.FindWebElement(By.TagName("table")); + Table table = this.App.FindWebElement(By.TagName("table")); // Act var rows = table.DataRows.ToList(); @@ -116,7 +117,7 @@ public void ShouldGetRowValuesByRowIndex() {"Company", "Alfreds Futterkiste"}, {"Contact", "Maria Anders"}, {"Country", "Germany"} }; - Table table = AppManager.WebApp.FindWebElement(By.TagName("table")); + Table table = this.App.FindWebElement(By.TagName("table")); // Act IReadOnlyDictionary rowData = table.GetRowDataByIndex(0); @@ -145,7 +146,7 @@ public void ShouldGetColumnValuesByColumnHeader() "Magazzini Alimentari Riuniti" }; - Table table = AppManager.WebApp.FindWebElement(By.TagName("table")); + Table table = this.App.FindWebElement(By.TagName("table")); // Act var columnData = table.GetColumnDataByHeader("Company").ToList(); @@ -173,7 +174,7 @@ public void ShouldGetColumnValuesByColumnIndex() "Magazzini Alimentari Riuniti" }; - Table table = AppManager.WebApp.FindWebElement(By.TagName("table")); + Table table = this.App.FindWebElement(By.TagName("table")); // Act var columnData = table.GetColumnDataByIndex(0).ToList(); diff --git a/samples/W3SchoolsWebTests/Tests/TextAreaTests.cs b/samples/W3SchoolsWebTests/Tests/TextAreaTests.cs index 4883b8b7..bdf3689f 100644 --- a/samples/W3SchoolsWebTests/Tests/TextAreaTests.cs +++ b/samples/W3SchoolsWebTests/Tests/TextAreaTests.cs @@ -16,7 +16,8 @@ public class TextAreaTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_textarea"; - public TextAreaTests(AppManagerOptions options) : base(options) + public TextAreaTests(AppManagerOptions options) + : base(options) { } @@ -39,7 +40,7 @@ public TextAreaTests(AppManagerOptions options) : base(options) [Test] public void ShouldSetText() { - TextArea review = AppManager.WebApp.FindElementById("w3review") as RemoteWebElement; + TextArea review = this.App.FindElementById("w3review") as RemoteWebElement; review.SetText("James"); review.Text.ShouldBe("James"); } @@ -47,7 +48,7 @@ public void ShouldSetText() [Test] public void ShouldAppendText() { - TextArea review = AppManager.WebApp.FindElementById("w3review") as RemoteWebElement; + TextArea review = this.App.FindElementById("w3review") as RemoteWebElement; review.SetText("James"); review.AppendText(" Croft"); review.Text.ShouldBe("James Croft"); @@ -56,7 +57,7 @@ public void ShouldAppendText() [Test] public void ShouldClearText() { - TextArea review = AppManager.WebApp.FindElementById("w3review") as RemoteWebElement; + TextArea review = this.App.FindElementById("w3review") as RemoteWebElement; review.SetText("James"); review.ClearText(); review.Text.ShouldBe(string.Empty); @@ -65,14 +66,14 @@ public void ShouldClearText() [Test] public void ShouldGetRows() { - TextArea review = AppManager.WebApp.FindElementById("w3review") as RemoteWebElement; + TextArea review = this.App.FindElementById("w3review") as RemoteWebElement; review.Rows.ShouldBe(4); } [Test] public void ShouldGetCols() { - TextArea review = AppManager.WebApp.FindElementById("w3review") as RemoteWebElement; + TextArea review = this.App.FindElementById("w3review") as RemoteWebElement; review.Cols.ShouldBe(50); } } diff --git a/samples/W3SchoolsWebTests/Tests/TextInputTests.cs b/samples/W3SchoolsWebTests/Tests/TextInputTests.cs index 0982b506..ed7deb14 100644 --- a/samples/W3SchoolsWebTests/Tests/TextInputTests.cs +++ b/samples/W3SchoolsWebTests/Tests/TextInputTests.cs @@ -16,7 +16,8 @@ public class TextInputTests : BaseTestClass { private const string Url = "https://www.w3schools.com/html/tryit.asp?filename=tryhtml_input_text"; - public TextInputTests(AppManagerOptions options) : base(options) + public TextInputTests(AppManagerOptions options) + : base(options) { } @@ -39,7 +40,7 @@ public TextInputTests(AppManagerOptions options) : base(options) [Test] public void ShouldSetText() { - TextInput firstName = AppManager.WebApp.FindElementById("fname") as RemoteWebElement; + TextInput firstName = this.App.FindElementById("fname") as RemoteWebElement; firstName.SetText("James"); firstName.Text.ShouldBe("James"); } @@ -47,7 +48,7 @@ public void ShouldSetText() [Test] public void ShouldAppendText() { - TextInput firstName = AppManager.WebApp.FindElementById("fname") as RemoteWebElement; + TextInput firstName = this.App.FindElementById("fname") as RemoteWebElement; firstName.SetText("James"); firstName.AppendText(" Croft"); firstName.Text.ShouldBe("James Croft"); @@ -56,7 +57,7 @@ public void ShouldAppendText() [Test] public void ShouldClearText() { - TextInput firstName = AppManager.WebApp.FindElementById("fname") as RemoteWebElement; + TextInput firstName = this.App.FindElementById("fname") as RemoteWebElement; firstName.SetText("James"); firstName.ClearText(); firstName.Text.ShouldBe(string.Empty); diff --git a/samples/W3SchoolsWebTests/Tests/UnorderedListTests.cs b/samples/W3SchoolsWebTests/Tests/UnorderedListTests.cs index e81a9d49..5ff00e92 100644 --- a/samples/W3SchoolsWebTests/Tests/UnorderedListTests.cs +++ b/samples/W3SchoolsWebTests/Tests/UnorderedListTests.cs @@ -19,7 +19,8 @@ public class UnorderedListTests : BaseTestClass { private const string Url = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_lists4"; - public UnorderedListTests(AppManagerOptions options) : base(options) + public UnorderedListTests(AppManagerOptions options) + : base(options) { } @@ -42,7 +43,7 @@ public UnorderedListTests(AppManagerOptions options) : base(options) [Test] public void ShouldContainItems() { - ReadOnlyCollection lists = AppManager.WebApp.FindElementsByTagName("ul"); + ReadOnlyCollection lists = this.App.FindElementsByTagName("ul"); List list = lists.FirstOrDefault() as RemoteWebElement; list.Items.Count.ShouldBe(3); diff --git a/samples/WebTests/BaseTestClass.cs b/samples/WebTests/BaseTestClass.cs new file mode 100644 index 00000000..7196914d --- /dev/null +++ b/samples/WebTests/BaseTestClass.cs @@ -0,0 +1,55 @@ +namespace WebTests +{ + using System; + using System.Collections.Generic; + using System.IO; + using Legerity; + using Legerity.Web; + using NUnit.Framework; + + public abstract class BaseTestClass : LegerityTestClass + { + private const string Url = "https://www.jamescroft.co.uk"; + + /// + /// Initializes a new instance of the class with application launch option. + /// + /// The application launch options. + protected BaseTestClass(AppManagerOptions options) + : base(options) + { + } + + protected static IEnumerable TestPlatformOptions => new List + { + new WebAppManagerOptions( + WebAppDriverType.EdgeChromium, + Path.Combine(Environment.CurrentDirectory)) + { + Maximize = true, Url = Url, ImplicitWait = TimeSpan.FromSeconds(10) + }, + new WebAppManagerOptions( + WebAppDriverType.Chrome, + Path.Combine(Environment.CurrentDirectory)) + { + Maximize = true, Url = Url, ImplicitWait = TimeSpan.FromSeconds(10) + } + }; + + [SetUp] + public virtual void Initialize() + { + } + + [TearDown] + public virtual void Cleanup() + { + } + + [OneTimeTearDown] + public virtual void FinalCleanup() + { + this.StopApps(); + } + } +} \ No newline at end of file diff --git a/samples/WebTests/EdgeBaseTestClass.cs b/samples/WebTests/EdgeBaseTestClass.cs deleted file mode 100644 index 4d728103..00000000 --- a/samples/WebTests/EdgeBaseTestClass.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace WebTests -{ - using System; - using Legerity; - using Legerity.Web; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - public abstract class EdgeBaseTestClass - { - [TestInitialize] - public virtual void Initialize() - { - AppManager.StartApp( - new WebAppManagerOptions(WebAppDriverType.EdgeChromium, Environment.CurrentDirectory) - { - Maximize = true, - Url = "https://www.jamescroft.co.uk" - }); - } - - [TestCleanup] - public virtual void Cleanup() - { - AppManager.StopApp(); - } - } -} \ No newline at end of file diff --git a/samples/WebTests/Pages/HomePage.cs b/samples/WebTests/Pages/HomePage.cs index 985b7cdb..d9a8d760 100644 --- a/samples/WebTests/Pages/HomePage.cs +++ b/samples/WebTests/Pages/HomePage.cs @@ -1,10 +1,13 @@ namespace WebTests.Pages { + using System; using System.Collections.ObjectModel; using System.Linq; + using Legerity; using Legerity.Pages; - using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; + using OpenQA.Selenium.Remote; + using Shouldly; /// /// Defines the home page of the website. @@ -17,6 +20,57 @@ public class HomePage : BasePage private readonly By readPostButtonLocator = By.ClassName("nectar-button"); + /// + /// Initializes a new instance of the class using the instance that verifies the page has loaded within 2 seconds. + /// + /// Thrown if AppManager.StartApp() has not been called. + /// Thrown if the page is not shown in 2 seconds. + public HomePage() + : this(AppManager.App, TimeSpan.FromSeconds(2)) + { + } + + /// + /// Initializes a new instance of the class using a instance that verifies the page has loaded within 2 seconds. + /// + /// + /// The instance of the started application driver that will be used to drive the page interaction. + /// + /// Thrown if AppManager.StartApp() has not been called. + /// Thrown if the page is not shown in 2 seconds. + public HomePage(RemoteWebDriver app) + : this(app, TimeSpan.FromSeconds(2)) + { + } + + /// + /// Initializes a new instance of the class using the instance that verifies the page has loaded within the given timeout. + /// + /// + /// The amount of time the driver should wait when searching for the if it is not immediately present. + /// + /// Thrown if AppManager.StartApp() has not been called. + /// Thrown if the page is not shown in the given timeout. + public HomePage(TimeSpan? traitTimeout) + : this(AppManager.App, traitTimeout) + { + } + + /// + /// Initializes a new instance of the class using a instance that verifies the page has loaded within the given timeout. + /// + /// + /// The instance of the started application driver that will be used to drive the page interaction. + /// + /// + /// The amount of time the driver should wait when searching for the if it is not immediately present. + /// + /// Thrown if AppManager.StartApp() has not been called. + /// Thrown if the page is not shown in the given timeout. + public HomePage(RemoteWebDriver app, TimeSpan? traitTimeout) : base(app, traitTimeout) + { + } + /// /// Gets a given trait of the page to verify that the page is in view. /// @@ -27,13 +81,12 @@ public HomePage VerifyHeroPostsShown() IWebElement heroPostsContainer = this.WebApp.FindElement(this.heroPostsLocator); ReadOnlyCollection heroPosts = heroPostsContainer.FindElements(this.heroPostLocator); - - Assert.AreEqual(3, heroPosts.Count); + heroPosts.Count.ShouldBe(3); return this; } - public PostPage NavigateToHeroPost() + public PostPage NavigateToFirstHeroPost() { IWebElement heroPostsContainer = this.WebApp.FindElement(this.heroPostsLocator); @@ -46,5 +99,19 @@ public PostPage NavigateToHeroPost() return new PostPage(); } + + public PostPage NavigateToHeroPostByIndex(int idx) + { + IWebElement heroPostsContainer = this.WebApp.FindElement(this.heroPostsLocator); + + ReadOnlyCollection heroPosts = heroPostsContainer.FindElements(this.heroPostLocator); + + IWebElement heroPost = heroPosts[idx]; + IWebElement readButton = heroPost.FindElement(this.readPostButtonLocator); + + readButton.Click(); + + return new PostPage(); + } } } \ No newline at end of file diff --git a/samples/WebTests/Tests/Edge/HomePageTests.cs b/samples/WebTests/Tests/Edge/HomePageTests.cs index 8013315d..9f9f355b 100644 --- a/samples/WebTests/Tests/Edge/HomePageTests.cs +++ b/samples/WebTests/Tests/Edge/HomePageTests.cs @@ -1,24 +1,27 @@ namespace WebTests.Tests.Edge { - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Pages; + using Legerity; + using NUnit.Framework; + using OpenQA.Selenium.Remote; + using WebTests.Pages; - [TestClass] - public class HomePageTests : EdgeBaseTestClass + [TestFixtureSource(nameof(TestPlatformOptions))] + [Parallelizable(ParallelScope.Fixtures)] + public class HomePageTests : BaseTestClass { - private HomePage Homepage { get; set; } - - [TestInitialize] - public override void Initialize() + public HomePageTests(AppManagerOptions options) + : base(options) { - base.Initialize(); - this.Homepage = new HomePage(); } - [TestMethod] - public void ReadHeroPost() + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [Parallelizable(ParallelScope.Children)] + public void ReadHeroPost(int idx) { - this.Homepage.NavigateToHeroPost().ReadPost(1); + RemoteWebDriver app = this.StartApp(); + new HomePage(app).NavigateToHeroPostByIndex(idx).ReadPost(0.25); } } -} +} \ No newline at end of file diff --git a/samples/WebTests/WebTests.csproj b/samples/WebTests/WebTests.csproj index 962a5d48..bab50c52 100644 --- a/samples/WebTests/WebTests.csproj +++ b/samples/WebTests/WebTests.csproj @@ -8,9 +8,10 @@ - - - + + + + diff --git a/src/Legerity.Android/Elements/AndroidElementWrapper.cs b/src/Legerity.Android/Elements/AndroidElementWrapper.cs index cf59f91d..a67f2837 100644 --- a/src/Legerity.Android/Elements/AndroidElementWrapper.cs +++ b/src/Legerity.Android/Elements/AndroidElementWrapper.cs @@ -27,7 +27,7 @@ protected AndroidElementWrapper(AndroidElement element) /// /// Gets the instance of the Appium driver for the Android application. /// - public AndroidDriver Driver => AppManager.AndroidApp; + public AndroidDriver Driver => this.ElementDriver as AndroidDriver; /// /// Determines whether the specified element is shown with the specified timeout. diff --git a/src/Legerity.Android/Extensions/AndroidElementWrapperExtensions.cs b/src/Legerity.Android/Extensions/AndroidElementWrapperExtensions.cs index 06451eb9..6c5994de 100644 --- a/src/Legerity.Android/Extensions/AndroidElementWrapperExtensions.cs +++ b/src/Legerity.Android/Extensions/AndroidElementWrapperExtensions.cs @@ -20,7 +20,7 @@ public static class AndroidElementWrapperExtensions public static void WaitUntil(this TElementWrapper element, Func condition, TimeSpan? timeout = default) where TElementWrapper : AndroidElementWrapper { - new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => + new WebDriverWait(element.ElementDriver, timeout ?? TimeSpan.Zero).Until(driver => { try { diff --git a/src/Legerity.Android/Extensions/LegerityTestClassExtensions.cs b/src/Legerity.Android/Extensions/LegerityTestClassExtensions.cs new file mode 100644 index 00000000..30464e9a --- /dev/null +++ b/src/Legerity.Android/Extensions/LegerityTestClassExtensions.cs @@ -0,0 +1,70 @@ +namespace Legerity.Android.Extensions +{ + using System; + using OpenQA.Selenium; + using OpenQA.Selenium.Appium.Android; + + /// + /// Defines a collection of extensions for instances. + /// + public static class LegerityTestClassExtensions + { + /// + /// Starts the Android application ready for testing. + /// + /// + /// The test class to launch an Android application for. + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public static AndroidDriver StartAndroidApp( + this LegerityTestClass testClass, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + return testClass.StartApp(waitUntil, waitUntilTimeout, waitUntilRetries) as AndroidDriver; + } + + /// + /// Starts the Android application ready for testing. + /// + /// + /// The test class to launch an Android application for. + /// + /// + /// The optional options to configure the driver with. + /// + /// Settings this will override the if previously set. + /// + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public static AndroidDriver StartAndroidApp( + this LegerityTestClass testClass, + AndroidAppManagerOptions options, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + return testClass.StartApp(options, waitUntil, waitUntilTimeout, waitUntilRetries) as + AndroidDriver; + } + } +} \ No newline at end of file diff --git a/src/Legerity.Core/AppManager.cs b/src/Legerity.Core/AppManager.cs index 2dc9ec34..be87b46d 100644 --- a/src/Legerity.Core/AppManager.cs +++ b/src/Legerity.Core/AppManager.cs @@ -1,6 +1,7 @@ namespace Legerity { using System; + using System.Collections.Generic; using Legerity.Android; using Legerity.Exceptions; using Legerity.Extensions; @@ -26,24 +27,38 @@ namespace Legerity /// public static class AppManager { + private static readonly List StartedApps = new(); + /// /// Gets the instance of the started Windows application. /// + /// + /// This instance should not be used in parallelized test runs. Instead, use the instance returned by the method. + /// public static WindowsDriver WindowsApp => App as WindowsDriver; /// /// Gets the instance of the started Android application. /// + /// + /// This instance should not be used in parallelized test runs. Instead, use the instance returned by the method. + /// public static AndroidDriver AndroidApp => App as AndroidDriver; /// /// Gets the instance of the started iOS application. /// + /// + /// This instance should not be used in parallelized test runs. Instead, use the instance returned by the method. + /// public static IOSDriver IOSApp => App as IOSDriver; /// /// Gets the instance of the started web application. /// + /// + /// This instance should not be used in parallelized test runs. Instead, use the instance returned by the method. + /// public static RemoteWebDriver WebApp => App; /// @@ -52,8 +67,16 @@ public static class AppManager /// This could be a , , , or web driver. /// /// + /// + /// This instance should not be used in parallelized test runs. Instead, use the instance returned by the method. + /// public static RemoteWebDriver App { get; set; } + /// + /// Gets or sets the instances of started applications. + /// + public static IReadOnlyCollection Apps => StartedApps; + /// /// Starts the application ready for testing. /// @@ -76,9 +99,14 @@ public static class AppManager /// Thrown if the WinAppDriver could not be found when running with true. /// Thrown if the WinAppDriver failed to load when running with true. /// Thrown if the wait until condition is not met in the allocated timeout period if provided. - public static void StartApp(AppManagerOptions opts, Func waitUntil = default, TimeSpan? waitUntilTimeout = default, int waitUntilRetries = 0) + /// The configured and running application driver. + public static RemoteWebDriver StartApp( + AppManagerOptions opts, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) { - StopApp(); + RemoteWebDriver app = null; if (opts is AppiumManagerOptions appiumOpts) { @@ -88,105 +116,157 @@ public static void StartApp(AppManagerOptions opts, Func waitU switch (opts) { case WebAppManagerOptions webOpts: + { + app = webOpts.DriverType switch + { + WebAppDriverType.Chrome => new ChromeDriver( + webOpts.DriverUri, + webOpts.DriverOptions as ChromeOptions ?? new ChromeOptions()), + WebAppDriverType.Firefox => new FirefoxDriver( + webOpts.DriverUri, + webOpts.DriverOptions as FirefoxOptions ?? new FirefoxOptions()), + WebAppDriverType.Opera => new OperaDriver( + webOpts.DriverUri, + webOpts.DriverOptions as OperaOptions ?? new OperaOptions()), + WebAppDriverType.Safari => new SafariDriver( + webOpts.DriverUri, + webOpts.DriverOptions as SafariOptions ?? new SafariOptions()), + WebAppDriverType.Edge => new EdgeDriver( + webOpts.DriverUri, + webOpts.DriverOptions as EdgeOptions ?? new EdgeOptions()), + WebAppDriverType.InternetExplorer => new InternetExplorerDriver( + webOpts.DriverUri, + webOpts.DriverOptions as InternetExplorerOptions ?? new InternetExplorerOptions()), + WebAppDriverType.EdgeChromium => new Microsoft.Edge.SeleniumTools.EdgeDriver( + webOpts.DriverUri, + webOpts.DriverOptions as Microsoft.Edge.SeleniumTools.EdgeOptions ?? + new Microsoft.Edge.SeleniumTools.EdgeOptions { UseChromium = true }), + _ => null + }; + + VerifyAppDriver(app, webOpts); + + if (webOpts.Maximize) + { + app.Manage().Window.Maximize(); + } + else { - App = webOpts.DriverType switch - { - WebAppDriverType.Chrome => new ChromeDriver(webOpts.DriverUri, webOpts.DriverOptions as ChromeOptions ?? new ChromeOptions()), - WebAppDriverType.Firefox => new FirefoxDriver(webOpts.DriverUri, webOpts.DriverOptions as FirefoxOptions ?? new FirefoxOptions()), - WebAppDriverType.Opera => new OperaDriver(webOpts.DriverUri, webOpts.DriverOptions as OperaOptions ?? new OperaOptions()), - WebAppDriverType.Safari => new SafariDriver(webOpts.DriverUri, webOpts.DriverOptions as SafariOptions ?? new SafariOptions()), - WebAppDriverType.Edge => new EdgeDriver(webOpts.DriverUri, webOpts.DriverOptions as EdgeOptions ?? new EdgeOptions()), - WebAppDriverType.InternetExplorer => new InternetExplorerDriver(webOpts.DriverUri, webOpts.DriverOptions as InternetExplorerOptions ?? new InternetExplorerOptions()), - WebAppDriverType.EdgeChromium => new Microsoft.Edge.SeleniumTools.EdgeDriver(webOpts.DriverUri, webOpts.DriverOptions as Microsoft.Edge.SeleniumTools.EdgeOptions ?? new Microsoft.Edge.SeleniumTools.EdgeOptions { UseChromium = true }), - _ => App - }; - - VerifyAppDriver(App, webOpts); - - if (webOpts.Maximize) - { - App.Manage().Window.Maximize(); - } - else - { - App.Manage().Window.Size = webOpts.DesiredSize; - } - - App.Url = webOpts.Url; - break; + app.Manage().Window.Size = webOpts.DesiredSize; } + app.Url = webOpts.Url; + break; + } + case WindowsAppManagerOptions winOpts: + { + if (winOpts.LaunchWinAppDriver) { - if (winOpts.LaunchWinAppDriver) - { - WinAppDriverHelper.Run(); - } - - App = new WindowsDriver( - new Uri(winOpts.DriverUri), - winOpts.AppiumOptions); + WinAppDriverHelper.Run(); + } - VerifyAppDriver(WindowsApp, winOpts); + app = new WindowsDriver( + new Uri(winOpts.DriverUri), + winOpts.AppiumOptions); - if (winOpts.Maximize) - { - App.Manage().Window.Maximize(); - } + VerifyAppDriver(app, winOpts); - break; + if (winOpts.Maximize) + { + app.Manage().Window.Maximize(); } + break; + } + case AndroidAppManagerOptions androidOpts: + { + if (androidOpts.LaunchAppiumServer) { - if (androidOpts.LaunchAppiumServer) - { - AppiumServerHelper.Run(); - } + AppiumServerHelper.Run(); + } - App = new AndroidDriver( - new Uri(androidOpts.DriverUri), - androidOpts.AppiumOptions); + app = new AndroidDriver( + new Uri(androidOpts.DriverUri), + androidOpts.AppiumOptions); - VerifyAppDriver(AndroidApp, androidOpts); - break; - } + VerifyAppDriver(app, androidOpts); + break; + } case IOSAppManagerOptions iosOpts: + { + if (iosOpts.LaunchAppiumServer) { - if (iosOpts.LaunchAppiumServer) - { - AppiumServerHelper.Run(); - } + AppiumServerHelper.Run(); + } - App = new IOSDriver(new Uri(iosOpts.DriverUri), iosOpts.AppiumOptions); + app = new IOSDriver(new Uri(iosOpts.DriverUri), iosOpts.AppiumOptions); - VerifyAppDriver(IOSApp, iosOpts); - break; - } + VerifyAppDriver(app, iosOpts); + break; + } + + default: + VerifyAppDriver(null, opts); + break; } if (waitUntil != null) { - App.WaitUntil(waitUntil, waitUntilTimeout, waitUntilRetries); + app.WaitUntil(waitUntil, waitUntilTimeout, waitUntilRetries); } + + App = app; + StartedApps.Add(app); + return app; } /// - /// Stops the application. + /// Stops the , with an option to stop the running Appium or WinAppDriver server. /// - public static void StopApp() + /// + /// An optional value indicating whether to stop the running Appium or WinAppDriver server. Default, true. + /// + public static void StopApp(bool stopServer = true) { - if (App != null) + StopApp(App, stopServer); + App = null; + } + + /// + /// Stops an application driver, with an option to stop the running Appium or WinAppDriver server. + /// + /// + /// The instance to stop running. + /// + /// + /// An optional value indicating whether to stop the running Appium or WinAppDriver server. Default, false. + /// + public static void StopApp(RemoteWebDriver app, bool stopServer = false) + { + app?.Quit(); + StartedApps.Remove(app); + + if (!stopServer) { - App.Quit(); - App = null; + return; } WinAppDriverHelper.Stop(); AppiumServerHelper.Stop(); } + /// + /// Stops all running application drivers. + /// + public static void StopApps() + { + StartedApps.ForEach(driver => driver?.Quit()); + StartedApps.Clear(); + } + /// Condition. private static void VerifyAppDriver(RemoteWebDriver app, AppManagerOptions opts) { diff --git a/src/Legerity.Core/ElementWrapper`1.cs b/src/Legerity.Core/ElementWrapper`1.cs index 5f5f9fe1..d4ac567f 100644 --- a/src/Legerity.Core/ElementWrapper`1.cs +++ b/src/Legerity.Core/ElementWrapper`1.cs @@ -36,6 +36,11 @@ protected ElementWrapper(TElement element) ? this.elementReference.Target as TElement : null; + /// + /// Gets the driver used to find this element. + /// + public IWebDriver ElementDriver => this.Element.WrappedDriver; + /// /// Gets a value indicating whether the element is visible. /// diff --git a/src/Legerity.Core/Extensions/ElementExtensions.cs b/src/Legerity.Core/Extensions/ElementExtensions.cs index 25dcdebe..526b081c 100644 --- a/src/Legerity.Core/Extensions/ElementExtensions.cs +++ b/src/Legerity.Core/Extensions/ElementExtensions.cs @@ -104,11 +104,19 @@ public static ReadOnlyCollection GetAllChildElements(this IWebEleme /// /// Waits until a specified element condition is met, with an optional timeout. /// + /// + /// This uses the AppManager.App instance to perform a wait. + /// /// The element to wait on. /// The condition of the element to wait on. /// The optional timeout wait on the condition being true. /// The type of . - public static void WaitUntil(this TElement element, Func condition, TimeSpan? timeout = default) + [Obsolete( + "WaitUntil for IWebElement instances will be removed in a future major release as it has a dependency on the static AppManager driver instance.")] + public static void WaitUntil( + this TElement element, + Func condition, + TimeSpan? timeout = default) where TElement : IWebElement { new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => diff --git a/src/Legerity.Core/Extensions/ElementWrapperExtensions.cs b/src/Legerity.Core/Extensions/ElementWrapperExtensions.cs index 8875dc8f..f142dbd6 100644 --- a/src/Legerity.Core/Extensions/ElementWrapperExtensions.cs +++ b/src/Legerity.Core/Extensions/ElementWrapperExtensions.cs @@ -140,7 +140,7 @@ public static ReadOnlyCollection GetAllChildElements(this public static void WaitUntil(this IElementWrapper element, Func, bool> condition, TimeSpan? timeout = default) where TElement : IWebElement { - new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => + new WebDriverWait(element.ElementDriver, timeout ?? TimeSpan.Zero).Until(driver => { try { diff --git a/src/Legerity.Core/Extensions/PageExtensions.cs b/src/Legerity.Core/Extensions/PageExtensions.cs index 576d6bf7..11f319e5 100644 --- a/src/Legerity.Core/Extensions/PageExtensions.cs +++ b/src/Legerity.Core/Extensions/PageExtensions.cs @@ -23,7 +23,7 @@ public static class PageExtensions public static TPage WaitUntil(this TPage page, Func condition, TimeSpan? timeout = default) where TPage : BasePage { - new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => + new WebDriverWait(page.App, timeout ?? TimeSpan.Zero).Until(driver => { try { diff --git a/src/Legerity.Core/IElementWrapper.cs b/src/Legerity.Core/IElementWrapper.cs index a8124960..05bc4d65 100644 --- a/src/Legerity.Core/IElementWrapper.cs +++ b/src/Legerity.Core/IElementWrapper.cs @@ -15,6 +15,11 @@ public interface IElementWrapper /// Gets the original reference object. TElement Element { get; } + /// + /// Gets the driver used to find this element. + /// + IWebDriver ElementDriver { get; } + /// /// Gets a value indicating whether the element is visible. /// diff --git a/src/Legerity.Core/LegerityTestClass.cs b/src/Legerity.Core/LegerityTestClass.cs index d859b99d..cff641e1 100644 --- a/src/Legerity.Core/LegerityTestClass.cs +++ b/src/Legerity.Core/LegerityTestClass.cs @@ -1,5 +1,8 @@ namespace Legerity { + using System; + using System.Collections.Generic; + using OpenQA.Selenium; using OpenQA.Selenium.Appium.Android; using OpenQA.Selenium.Appium.iOS; using OpenQA.Selenium.Appium.Windows; @@ -10,6 +13,8 @@ namespace Legerity /// public abstract class LegerityTestClass { + private readonly List apps = new(); + /// /// Initializes a new instance of the class. /// @@ -35,7 +40,18 @@ protected LegerityTestClass(AppManagerOptions options) /// This could be a , , , or web driver. /// /// - protected static RemoteWebDriver App => AppManager.App; + /// + /// This instance should not be used in parallelized test runs. Instead, use the instance returned by the method. + /// + protected RemoteWebDriver App { get; private set; } + + /// + /// Gets or sets the instances of started applications. + /// + /// + /// This is useful for accessing drivers in parallelized tests. + /// + protected IReadOnlyCollection Apps => this.apps; /// /// Gets or sets the model that represents the configuration options for the . @@ -45,17 +61,102 @@ protected LegerityTestClass(AppManagerOptions options) /// /// Starts the application ready for testing. /// - public virtual void StartApp() + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public virtual RemoteWebDriver StartApp( + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) { - AppManager.StartApp(this.Options); + return this.StartApp(this.Options, waitUntil, waitUntilTimeout, waitUntilRetries); } /// - /// Stops the application. + /// Starts the application ready for testing. + /// + /// + /// The optional options to configure the driver with. + /// + /// Settings this will override the if previously set. + /// + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public virtual RemoteWebDriver StartApp( + AppManagerOptions options, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + if (options != default && this.Options != options) + { + this.Options = options; + } + + RemoteWebDriver app = AppManager.StartApp(this.Options, waitUntil, waitUntilTimeout, waitUntilRetries); + this.App = app; + this.apps.Add(app); + return app; + } + + + /// + /// Stops the and any running Appium or WinAppDriver server. /// public virtual void StopApp() { - AppManager.StopApp(); + this.StopApp(true); + } + + /// + /// Stops the , with an option to stop the running Appium or WinAppDriver server. + /// + /// + /// An optional value indicating whether to stop the running Appium or WinAppDriver server. + /// + public virtual void StopApp(bool stopServer) + { + this.StopApp(this.App, stopServer); + } + + /// + /// Stops an application, with an option to stop the running Appium or WinAppDriver server. + /// + /// + /// The instance to stop running. + /// + /// + /// An optional value indicating whether to stop the running Appium or WinAppDriver server. Default, false. + /// + public virtual void StopApp(RemoteWebDriver app, bool stopServer = false) + { + this.apps.Remove(app); + AppManager.StopApp(app, stopServer); + } + + /// + /// Stops all running application drivers. + /// + public virtual void StopApps() + { + this.apps.ForEach(app => this.StopApp(app)); + this.apps.Clear(); } } -} +} \ No newline at end of file diff --git a/src/Legerity.Core/Pages/BasePage.cs b/src/Legerity.Core/Pages/BasePage.cs index 74f0cb8b..6d681fcb 100644 --- a/src/Legerity.Core/Pages/BasePage.cs +++ b/src/Legerity.Core/Pages/BasePage.cs @@ -18,17 +18,30 @@ namespace Legerity.Pages public abstract class BasePage { /// - /// Initializes a new instance of the class that verifies the page has loaded within 2 seconds. + /// Initializes a new instance of the class using the instance that verifies the page has loaded within 2 seconds. /// /// Thrown if AppManager.StartApp() has not been called. /// Thrown if the page is not shown in 2 seconds. protected BasePage() - : this(TimeSpan.FromSeconds(2)) + : this(AppManager.App, TimeSpan.FromSeconds(2)) { } /// - /// Initializes a new instance of the class that verifies the page has loaded within the given timeout. + /// Initializes a new instance of the class using a instance that verifies the page has loaded within 2 seconds. + /// + /// + /// The instance of the started application driver that will be used to drive the page interaction. + /// + /// Thrown if AppManager.StartApp() has not been called. + /// Thrown if the page is not shown in 2 seconds. + protected BasePage(RemoteWebDriver app) + : this(app, TimeSpan.FromSeconds(2)) + { + } + + /// + /// Initializes a new instance of the class using the instance that verifies the page has loaded within the given timeout. /// /// /// The amount of time the driver should wait when searching for the if it is not immediately present. @@ -36,7 +49,24 @@ protected BasePage() /// Thrown if AppManager.StartApp() has not been called. /// Thrown if the page is not shown in the given timeout. protected BasePage(TimeSpan? traitTimeout) + : this(AppManager.App, traitTimeout) + { + } + + /// + /// Initializes a new instance of the class using a instance that verifies the page has loaded within the given timeout. + /// + /// + /// The instance of the started application driver that will be used to drive the page interaction. + /// + /// + /// The amount of time the driver should wait when searching for the if it is not immediately present. + /// + /// Thrown if AppManager.StartApp() has not been called. + /// Thrown if the page is not shown in the given timeout. + protected BasePage(RemoteWebDriver app, TimeSpan? traitTimeout) { + this.App = app; this.VerifyPageShown(traitTimeout ?? TimeSpan.FromSeconds(2)); } @@ -49,27 +79,27 @@ protected BasePage(TimeSpan? traitTimeout) /// This could be a , , , or web driver. /// /// - public RemoteWebDriver App => AppManager.App; + public RemoteWebDriver App { get; } /// /// Gets the instance of the started Windows application. /// - protected WindowsDriver WindowsApp => AppManager.WindowsApp; + protected WindowsDriver WindowsApp => this.App as WindowsDriver; /// /// Gets the instance of the started Android application. /// - protected AndroidDriver AndroidApp => AppManager.AndroidApp; + protected AndroidDriver AndroidApp => this.App as AndroidDriver; /// /// Gets the instance of the started iOS application. /// - protected IOSDriver IOSApp => AppManager.IOSApp; + protected IOSDriver IOSApp => this.App as IOSDriver; /// /// Gets the instance of the started web application. /// - protected RemoteWebDriver WebApp => AppManager.WebApp; + protected RemoteWebDriver WebApp => this.App; /// /// Gets a given trait of the page to verify that the page is in view. diff --git a/src/Legerity.IOS/Elements/IOSElementWrapper.cs b/src/Legerity.IOS/Elements/IOSElementWrapper.cs index 038f4c64..571155c7 100644 --- a/src/Legerity.IOS/Elements/IOSElementWrapper.cs +++ b/src/Legerity.IOS/Elements/IOSElementWrapper.cs @@ -27,7 +27,7 @@ protected IOSElementWrapper(IOSElement element) /// /// Gets the instance of the Appium driver for the iOS application. /// - public IOSDriver Driver => AppManager.IOSApp; + public IOSDriver Driver => this.ElementDriver as IOSDriver; /// /// Determines whether the specified element is shown with the specified timeout. diff --git a/src/Legerity.IOS/Extensions/IOSElementWrapperExtensions.cs b/src/Legerity.IOS/Extensions/IOSElementWrapperExtensions.cs index 8049741a..b4cde6e8 100644 --- a/src/Legerity.IOS/Extensions/IOSElementWrapperExtensions.cs +++ b/src/Legerity.IOS/Extensions/IOSElementWrapperExtensions.cs @@ -20,7 +20,7 @@ public static class IOSElementWrapperExtensions public static void WaitUntil(this TElementWrapper element, Func condition, TimeSpan? timeout = default) where TElementWrapper : IOSElementWrapper { - new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => + new WebDriverWait(element.ElementDriver, timeout ?? TimeSpan.Zero).Until(driver => { try { diff --git a/src/Legerity.IOS/Extensions/LegerityTestClassExtensions.cs b/src/Legerity.IOS/Extensions/LegerityTestClassExtensions.cs new file mode 100644 index 00000000..0a0c474d --- /dev/null +++ b/src/Legerity.IOS/Extensions/LegerityTestClassExtensions.cs @@ -0,0 +1,70 @@ +namespace Legerity.IOS.Extensions +{ + using System; + using OpenQA.Selenium; + using OpenQA.Selenium.Appium.iOS; + + /// + /// Defines a collection of extensions for instances. + /// + public static class LegerityTestClassExtensions + { + /// + /// Starts the iOS application ready for testing. + /// + /// + /// The test class to launch an Android application for. + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public static IOSDriver StartIOSApp( + this LegerityTestClass testClass, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + return testClass.StartApp(waitUntil, waitUntilTimeout, waitUntilRetries) as IOSDriver; + } + + /// + /// Starts the iOS application ready for testing. + /// + /// + /// The test class to launch an Android application for. + /// + /// + /// The optional options to configure the driver with. + /// + /// Settings this will override the if previously set. + /// + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public static IOSDriver StartIOSApp( + this LegerityTestClass testClass, + IOSAppManagerOptions options, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + return testClass.StartApp(options, waitUntil, waitUntilTimeout, waitUntilRetries) as + IOSDriver; + } + } +} \ No newline at end of file diff --git a/src/Legerity.Web/Elements/WebElementWrapper.cs b/src/Legerity.Web/Elements/WebElementWrapper.cs index 84611f6c..c4172283 100644 --- a/src/Legerity.Web/Elements/WebElementWrapper.cs +++ b/src/Legerity.Web/Elements/WebElementWrapper.cs @@ -43,10 +43,15 @@ protected WebElementWrapper(RemoteWebElement element) ? this.elementReference.Target as RemoteWebElement : null; + /// + /// Gets the driver used to find this element. + /// + public IWebDriver ElementDriver => this.Element.WrappedDriver; + /// /// Gets the instance of the driver for the web application. /// - public RemoteWebDriver Driver => AppManager.WebApp; + public RemoteWebDriver Driver => this.ElementDriver as RemoteWebDriver; /// /// Gets a value indicating whether the element is enabled. diff --git a/src/Legerity.Web/Extensions/WebElementWrapperExtensions.cs b/src/Legerity.Web/Extensions/WebElementWrapperExtensions.cs index fa169ad6..f4d71d47 100644 --- a/src/Legerity.Web/Extensions/WebElementWrapperExtensions.cs +++ b/src/Legerity.Web/Extensions/WebElementWrapperExtensions.cs @@ -20,7 +20,7 @@ public static class WebElementWrapperExtensions public static void WaitUntil(this TElementWrapper element, Func condition, TimeSpan? timeout = default) where TElementWrapper : WebElementWrapper { - new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => + new WebDriverWait(element.ElementDriver, timeout ?? TimeSpan.Zero).Until(driver => { try { diff --git a/src/Legerity.Windows/Elements/WindowsElementWrapper.cs b/src/Legerity.Windows/Elements/WindowsElementWrapper.cs index b14971c4..7d84801e 100644 --- a/src/Legerity.Windows/Elements/WindowsElementWrapper.cs +++ b/src/Legerity.Windows/Elements/WindowsElementWrapper.cs @@ -26,7 +26,7 @@ protected WindowsElementWrapper(WindowsElement element) /// /// Gets the instance of the Appium driver for the Windows application. /// - public WindowsDriver Driver => AppManager.WindowsApp; + public WindowsDriver Driver => this.ElementDriver as WindowsDriver; /// /// Determines whether the specified element is shown with the specified timeout. diff --git a/src/Legerity.Windows/Extensions/LegerityTestClassExtensions.cs b/src/Legerity.Windows/Extensions/LegerityTestClassExtensions.cs new file mode 100644 index 00000000..93201130 --- /dev/null +++ b/src/Legerity.Windows/Extensions/LegerityTestClassExtensions.cs @@ -0,0 +1,71 @@ +namespace Legerity.Windows.Extensions +{ + using System; + using OpenQA.Selenium; + using OpenQA.Selenium.Appium.iOS; + using OpenQA.Selenium.Appium.Windows; + + /// + /// Defines a collection of extensions for instances. + /// + public static class LegerityTestClassExtensions + { + /// + /// Starts the Windows application ready for testing. + /// + /// + /// The test class to launch an Android application for. + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public static WindowsDriver StartWindowsApp( + this LegerityTestClass testClass, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + return testClass.StartApp(waitUntil, waitUntilTimeout, waitUntilRetries) as WindowsDriver; + } + + /// + /// Starts the Windows application ready for testing. + /// + /// + /// The test class to launch an Android application for. + /// + /// + /// The optional options to configure the driver with. + /// + /// Settings this will override the if previously set. + /// + /// + /// + /// An optional condition of the driver to wait on until it is met. + /// + /// + /// An optional timeout wait on the conditional wait until being true. If not set, the wait will run immediately, and if not valid, will throw an exception. + /// + /// + /// An optional count of retries after a timeout on the wait until condition before accepting the failure. + /// + /// The configured and running application driver. + public static WindowsDriver StartWindowsApp( + this LegerityTestClass testClass, + WindowsAppManagerOptions options, + Func waitUntil = default, + TimeSpan? waitUntilTimeout = default, + int waitUntilRetries = 0) + { + return testClass.StartApp(options, waitUntil, waitUntilTimeout, waitUntilRetries) as + WindowsDriver; + } + } +} \ No newline at end of file diff --git a/src/Legerity.Windows/Extensions/WindowsElementWrapperExtensions.cs b/src/Legerity.Windows/Extensions/WindowsElementWrapperExtensions.cs index d4259625..7aee2ab7 100644 --- a/src/Legerity.Windows/Extensions/WindowsElementWrapperExtensions.cs +++ b/src/Legerity.Windows/Extensions/WindowsElementWrapperExtensions.cs @@ -20,7 +20,7 @@ public static class WindowsElementWrapperExtensions public static void WaitUntil(this TElementWrapper element, Func condition, TimeSpan? timeout = default) where TElementWrapper : WindowsElementWrapper { - new WebDriverWait(AppManager.App, timeout ?? TimeSpan.Zero).Until(driver => + new WebDriverWait(element.ElementDriver, timeout ?? TimeSpan.Zero).Until(driver => { try {