diff --git a/src/common/UITestAutomation/Element/Button.cs b/src/common/UITestAutomation/Element/Button.cs
index b5915031be92..8225971b4e32 100644
--- a/src/common/UITestAutomation/Element/Button.cs
+++ b/src/common/UITestAutomation/Element/Button.cs
@@ -9,5 +9,14 @@ namespace Microsoft.PowerToys.UITest
///
public class Button : Element
{
+ private static readonly string ExpectedControlType = "ControlType.Button";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Button()
+ {
+ this.TargetControlType = Button.ExpectedControlType;
+ }
}
}
diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs
index f2f3d4ee29bd..59c799e401dc 100644
--- a/src/common/UITestAutomation/Element/Element.cs
+++ b/src/common/UITestAutomation/Element/Element.cs
@@ -22,6 +22,14 @@ public class Element
private WindowsDriver? driver;
+ protected string? TargetControlType { get; set; }
+
+ internal bool IsMatchingTarget()
+ {
+ var ct = this.ControlType;
+ return string.IsNullOrEmpty(this.TargetControlType) || this.TargetControlType == this.ControlType;
+ }
+
internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement;
internal void SetSession(WindowsDriver driver) => this.driver = driver;
@@ -91,7 +99,7 @@ public string ControlType
/// Click the UI element.
///
/// If true, performs a right-click; otherwise, performs a left-click. Default value is false
- public void Click(bool rightClick = false)
+ public virtual void Click(bool rightClick = false)
{
PerformAction((actions, windowElement) =>
{
@@ -116,7 +124,7 @@ public void Click(bool rightClick = false)
///
/// Double Click the UI element.
///
- public void DoubleClick()
+ public virtual void DoubleClick()
{
PerformAction((actions, windowElement) =>
{
@@ -139,7 +147,6 @@ public string GetAttribute(string attributeName)
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method GetAttribute with parameter: attributeName = {attributeName}");
var attributeValue = this.windowsElement.GetAttribute(attributeName);
- Assert.IsNotNull(attributeValue, $"Attribute '{attributeName}' is null.");
return attributeValue;
}
@@ -154,17 +161,51 @@ public T Find(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
- var foundElement = FindHelper.Find(
- () =>
- {
- var element = this.windowsElement.FindElement(by.ToSeleniumBy());
- Assert.IsNotNull(element, $"Element not found using selector: {by}");
- return element;
- },
- this.driver,
- timeoutMS);
- return foundElement;
+ // leverage findAll to filter out mismatched elements
+ var collection = this.FindAll(by, timeoutMS);
+
+ Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
+
+ return collection[0];
+ }
+
+ ///
+ /// Finds an element by the selector.
+ /// Shortcut for this.Find(By.Name(name), timeoutMS)
+ ///
+ /// The class type of the element to find.
+ /// The name for finding the element.
+ /// The timeout in milliseconds.
+ /// The found element.
+ public T Find(string name, int timeoutMS = 3000)
+ where T : Element, new()
+ {
+ return this.Find(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Finds an element by the selector.
+ /// Shortcut for this.Find(by, timeoutMS)
+ ///
+ /// The selector to use for finding the element.
+ /// The timeout in milliseconds.
+ /// The found element.
+ public Element Find(By by, int timeoutMS = 3000)
+ {
+ return this.Find(by, timeoutMS);
+ }
+
+ ///
+ /// Finds an element by the selector.
+ /// Shortcut for this.Find(By.Name(name), timeoutMS)
+ ///
+ /// The name for finding the element.
+ /// The timeout in milliseconds.
+ /// The found element.
+ public Element Find(string name, int timeoutMS = 3000)
+ {
+ return this.Find(By.Name(name), timeoutMS);
}
///
@@ -174,7 +215,7 @@ public T Find(By by, int timeoutMS = 3000)
/// The selector to use for finding the elements.
/// The timeout in milliseconds.
/// A read-only collection of the found elements.
- public ReadOnlyCollection? FindAll(By by, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -182,22 +223,59 @@ public T Find(By by, int timeoutMS = 3000)
() =>
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
- Assert.IsTrue(elements.Count > 0, $"Elements not found using selector: {by}");
return elements;
},
this.driver,
timeoutMS);
- return foundElements;
+ return foundElements ?? new ReadOnlyCollection([]);
+ }
+
+ ///
+ /// Finds all elements by the selector.
+ /// Shortcut for this.FindAll(By.Name(name), timeoutMS)
+ ///
+ /// The class type of the elements to find.
+ /// The name for finding the element.
+ /// The timeout in milliseconds.
+ /// A read-only collection of the found elements.
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ where T : Element, new()
+ {
+ return this.FindAll(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Finds all elements by the selector.
+ /// Shortcut for this.FindAll(by, timeoutMS)
+ ///
+ /// The selector to use for finding the elements.
+ /// The timeout in milliseconds.
+ /// A read-only collection of the found elements.
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ {
+ return this.FindAll(by, timeoutMS);
+ }
+
+ ///
+ /// Finds all elements by the selector.
+ /// Shortcut for this.FindAll(By.Name(name), timeoutMS)
+ ///
+ /// The name for finding the element.
+ /// The timeout in milliseconds.
+ /// A read-only collection of the found elements.
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ {
+ return this.FindAll(By.Name(name), timeoutMS);
}
///
/// Simulates a manual operation on the element.
///
/// The action to perform on the element.
- /// The number of milliseconds to wait before the action. Default value is 100 ms
- /// The number of milliseconds to wait after the action. Default value is 100 ms
- protected void PerformAction(Action action, int msPreAction = 100, int msPostAction = 100)
+ /// The number of milliseconds to wait before the action. Default value is 500 ms
+ /// The number of milliseconds to wait after the action. Default value is 500 ms
+ protected void PerformAction(Action action, int msPreAction = 500, int msPostAction = 500)
{
if (msPreAction > 0)
{
diff --git a/src/common/UITestAutomation/Element/HyperlinkButton.cs b/src/common/UITestAutomation/Element/HyperlinkButton.cs
new file mode 100644
index 000000000000..deef577b295a
--- /dev/null
+++ b/src/common/UITestAutomation/Element/HyperlinkButton.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.PowerToys.UITest
+{
+ ///
+ /// Represents a HyperLinkButton in the UI test environment.
+ /// HyperLinkButton represents a button control that functions as a hyperlink.
+ ///
+ public class HyperlinkButton : Button
+ {
+ private static readonly string ExpectedControlType = "ControlType.HyperLink";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HyperlinkButton()
+ {
+ this.TargetControlType = HyperlinkButton.ExpectedControlType;
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Element/NavigationViewItem.cs b/src/common/UITestAutomation/Element/NavigationViewItem.cs
new file mode 100644
index 000000000000..0a71d9a32100
--- /dev/null
+++ b/src/common/UITestAutomation/Element/NavigationViewItem.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.PowerToys.UITest
+{
+ ///
+ /// Represents a NavigationViewItem in the UI test environment.
+ /// NavigationViewItem represents the container for an item in a NavigationView control.
+ ///
+ public class NavigationViewItem : Element
+ {
+ private static readonly string ExpectedControlType = "ControlType.ListItem";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NavigationViewItem()
+ {
+ this.TargetControlType = NavigationViewItem.ExpectedControlType;
+ }
+
+ ///
+ /// Click the ListItem element.
+ ///
+ /// If true, performs a right-click; otherwise, performs a left-click. Default value is false
+ public override void Click(bool rightClick = false)
+ {
+ PerformAction((actions, windowElement) =>
+ {
+ actions.MoveToElement(windowElement, 10, 10);
+
+ if (rightClick)
+ {
+ actions.ContextClick();
+ }
+ else
+ {
+ actions.Click();
+ }
+
+ actions.Build().Perform();
+ });
+ }
+
+ ///
+ /// Double Click the ListItem element.
+ ///
+ public override void DoubleClick()
+ {
+ PerformAction((actions, windowElement) =>
+ {
+ actions.MoveToElement(windowElement, 10, 10);
+ actions.DoubleClick();
+ actions.Build().Perform();
+ });
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Element/TextBlock.cs b/src/common/UITestAutomation/Element/TextBlock.cs
new file mode 100644
index 000000000000..883b104e2623
--- /dev/null
+++ b/src/common/UITestAutomation/Element/TextBlock.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.PowerToys.UITest
+{
+ ///
+ /// Represents a TextBlock in the UI test environment.
+ /// TextBlock provides a lightweight control for displaying small amounts of flow content.
+ ///
+ public class TextBlock : Element
+ {
+ private static readonly string ExpectedControlType = "ControlType.Text";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TextBlock()
+ {
+ this.TargetControlType = TextBlock.ExpectedControlType;
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs
index e427f7c9d316..71f833625dba 100644
--- a/src/common/UITestAutomation/Element/TextBox.cs
+++ b/src/common/UITestAutomation/Element/TextBox.cs
@@ -7,10 +7,21 @@
namespace Microsoft.PowerToys.UITest
{
///
- /// Represents a textbox in the UI test environment.
+ /// Represents a TextBox in the UI test environment.
+ /// TextBox represents a control that can be used to display and edit plain text (single or multi-line).
///
public class TextBox : Element
{
+ private static readonly string ExpectedControlType = "ControlType.Edit";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TextBox()
+ {
+ this.TargetControlType = TextBox.ExpectedControlType;
+ }
+
///
/// Sets the text of the textbox.
///
diff --git a/src/common/UITestAutomation/Element/Window.cs b/src/common/UITestAutomation/Element/Window.cs
index eceb1fcf4793..5fc5fc6e266c 100644
--- a/src/common/UITestAutomation/Element/Window.cs
+++ b/src/common/UITestAutomation/Element/Window.cs
@@ -18,7 +18,7 @@ public Window Maximize(bool byClickButton = true)
{
if (byClickButton)
{
- Find
internal static class FindHelper
{
- public static T Find(Func findElementFunc, WindowsDriver? driver, int timeoutMS)
- where T : Element, new()
- {
- var item = findElementFunc() as WindowsElement;
- return NewElement(item, driver, timeoutMS);
- }
-
public static ReadOnlyCollection? FindAll(Func> findElementsFunc, WindowsDriver? driver, int timeoutMS)
where T : Element, new()
{
@@ -32,7 +25,7 @@ public static T Find(Func findElementFunc, WindowsDriver(element, driver, timeoutMS);
- }).ToList();
+ }).Where(item => item.IsMatchingTarget()).ToList();
return new ReadOnlyCollection(res);
}
diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs
index 7071f6d2e236..ef0a6fff3fad 100644
--- a/src/common/UITestAutomation/Session.cs
+++ b/src/common/UITestAutomation/Session.cs
@@ -4,6 +4,7 @@
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
+using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
@@ -39,17 +40,48 @@ public T Find(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
- var foundElement = FindHelper.Find(
- () =>
- {
- var element = this.WindowsDriver.FindElement(by.ToSeleniumBy());
- Assert.IsNotNull(element, $"Element not found using selector: {by}");
- return element;
- },
- this.WindowsDriver,
- timeoutMS);
- return foundElement;
+ // leverage findAll to filter out mismatched elements
+ var collection = this.FindAll(by, timeoutMS);
+
+ Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
+
+ return collection[0];
+ }
+
+ ///
+ /// Shortcut for this.Find(By.Name(name), timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 3000).
+ /// The found element.
+ public T Find(string name, int timeoutMS = 3000)
+ where T : Element, new()
+ {
+ return this.Find(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Find(by, timeoutMS)
+ ///
+ /// The selector to find the element.
+ /// The timeout in milliseconds (default is 3000).
+ /// The found element.
+ public Element Find(By by, int timeoutMS = 3000)
+ {
+ return this.Find(by, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Find(By.Name(name), timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 3000).
+ /// The found element.
+ public Element Find(string name, int timeoutMS = 3000)
+ {
+ return this.Find(By.Name(name), timeoutMS);
}
///
@@ -72,7 +104,45 @@ public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
this.WindowsDriver,
timeoutMS);
- return foundElements ?? new ReadOnlyCollection(new List());
+ return foundElements ?? new ReadOnlyCollection([]);
+ }
+
+ ///
+ /// Finds all elements by selector.
+ /// Shortcut for this.FindAll(By.Name(name), timeoutMS)
+ ///
+ /// The class of the elements, should be Element or its derived class.
+ /// The name to find the elements.
+ /// The timeout in milliseconds (default is 3000).
+ /// A read-only collection of the found elements.
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ where T : Element, new()
+ {
+ return this.FindAll(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Finds all elements by selector.
+ /// Shortcut for this.FindAll(by, timeoutMS)
+ ///
+ /// The selector to find the elements.
+ /// The timeout in milliseconds (default is 3000).
+ /// A read-only collection of the found elements.
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ {
+ return this.FindAll(by, timeoutMS);
+ }
+
+ ///
+ /// Finds all elements by selector.
+ /// Shortcut for this.FindAll(By.Name(name), timeoutMS)
+ ///
+ /// The name to find the elements.
+ /// The timeout in milliseconds (default is 3000).
+ /// A read-only collection of the found elements.
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ {
+ return this.FindAll(By.Name(name), timeoutMS);
}
///
diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs
new file mode 100644
index 000000000000..7bb1f6e7a6fd
--- /dev/null
+++ b/src/common/UITestAutomation/SessionHelper.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OpenQA.Selenium.Appium;
+using OpenQA.Selenium.Appium.Windows;
+
+namespace Microsoft.PowerToys.UITest
+{
+ ///
+ /// Nested class for test initialization.
+ ///
+ internal class SessionHelper
+ {
+ // Default session path is PowerToys settings dashboard
+ private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
+
+ private WindowsDriver Root { get; set; }
+
+ private WindowsDriver? Driver { get; set; }
+
+ private Process? appDriver;
+
+ public SessionHelper(PowerToysModule scope)
+ {
+ this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
+
+ var winAppDriverProcessInfo = new ProcessStartInfo
+ {
+ FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
+ Verb = "runas",
+ };
+
+ this.appDriver = Process.Start(winAppDriverProcessInfo);
+
+ var desktopCapabilities = new AppiumOptions();
+ desktopCapabilities.AddAdditionalCapability("app", "Root");
+ this.Root = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
+
+ // Set default timeout to 5 seconds
+ this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
+ }
+
+ ///
+ /// Initializes the test environment.
+ ///
+ /// The PowerToys module to start.
+ [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")]
+ public SessionHelper Init()
+ {
+ string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ this.StartExe(path + this.sessionPath);
+
+ Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
+
+ // Set default timeout to 5 seconds
+ this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
+
+ return this;
+ }
+
+ ///
+ /// Cleans up the test environment.
+ ///
+ public void Cleanup()
+ {
+ try
+ {
+ appDriver?.Kill();
+ }
+ catch (Exception ex)
+ {
+ // Handle exceptions if needed
+ Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Starts a new exe and takes control of it.
+ ///
+ /// The path to the application executable.
+ public void StartExe(string appPath)
+ {
+ var opts = new AppiumOptions();
+ opts.AddAdditionalCapability("app", appPath);
+ this.Driver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
+ }
+
+ public WindowsDriver GetRoot() => this.Root;
+
+ public WindowsDriver GetDriver()
+ {
+ Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
+ return this.Driver;
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs
index 4ce432f23a07..1d6502ac5447 100644
--- a/src/common/UITestAutomation/UITestBase.cs
+++ b/src/common/UITestAutomation/UITestBase.cs
@@ -15,22 +15,42 @@ namespace Microsoft.PowerToys.UITest
///
/// Base class that should be inherited by all Test Classes.
///
+ [TestClass]
public class UITestBase
{
public Session Session { get; set; }
- private readonly TestInit testInit = new TestInit();
+ private readonly SessionHelper sessionHelper;
+
+ private readonly PowerToysModule scope;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
{
- this.testInit.SetScope(scope);
- this.testInit.Init();
- this.Session = new Session(this.testInit.GetRoot(), this.testInit.GetDriver());
+ this.scope = scope;
+ this.sessionHelper = new SessionHelper(scope).Init();
+ this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
}
~UITestBase()
{
- this.testInit.Cleanup();
+ this.sessionHelper.Cleanup();
+ }
+
+ ///
+ /// Initializes the test.
+ ///
+ [TestInitialize]
+ public void TestInit()
+ {
+ if (this.scope == PowerToysModule.PowerToysSettings)
+ {
+ // close Debug warning dialog if any
+ // Such debug warning dialog seems only appear in PowerToys Settings
+ if (this.FindAll("DEBUG").Count > 0)
+ {
+ this.Find("DEBUG").Find("Close").Click();
+ }
+ }
}
///
@@ -47,6 +67,41 @@ protected T Find(By by, int timeoutMS = 3000)
return this.Session.Find(by, timeoutMS);
}
+ ///
+ /// Shortcut for this.Session.Find(By.Name(name), timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 3000).
+ /// The found element.
+ protected T Find(string name, int timeoutMS = 3000)
+ where T : Element, new()
+ {
+ return this.Session.Find(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.Find(by, timeoutMS)
+ ///
+ /// The selector to find the element.
+ /// The timeout in milliseconds (default is 3000).
+ /// The found element.
+ protected Element Find(By by, int timeoutMS = 3000)
+ {
+ return this.Session.Find(by, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.Find(By.Name(name), timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 3000).
+ /// The found element.
+ protected Element Find(string name, int timeoutMS = 3000)
+ {
+ return this.Session.Find(name, timeoutMS);
+ }
+
///
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll(by, timeoutMS)
@@ -62,93 +117,41 @@ protected ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
}
///
- /// Nested class for test initialization.
+ /// Finds all elements by selector.
+ /// Shortcut for this.Session.FindAll(By.Name(name), timeoutMS)
///
- private sealed class TestInit
+ /// The class of the elements, should be Element or its derived class.
+ /// The name of the elements.
+ /// The timeout in milliseconds (default is 3000).
+ /// A read-only collection of the found elements.
+ protected ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ where T : Element, new()
{
- private WindowsDriver Root { get; set; }
-
- private WindowsDriver? Driver { get; set; }
-
- private static Process? appDriver;
-
- // Default session path is PowerToys settings dashboard
- private static string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
-
- public TestInit()
- {
- appDriver = Process.Start(new ProcessStartInfo
- {
- FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
- Verb = "runas",
- });
-
- var desktopCapabilities = new AppiumOptions();
- desktopCapabilities.AddAdditionalCapability("app", "Root");
- this.Root = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
-
- // Set default timeout to 5 seconds
- this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
- }
-
- ///
- /// Initializes the test environment.
- ///
- [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")]
- public void Init()
- {
- string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
- this.StartExe(path + sessionPath);
-
- Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
-
- // Set default timeout to 5 seconds
- this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
- }
-
- ///
- /// Cleans up the test environment.
- ///
- public void Cleanup()
- {
- try
- {
- appDriver?.Kill();
- }
- catch (Exception ex)
- {
- // Handle exceptions if needed
- Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
- }
- }
-
- ///
- /// Starts a new exe and takes control of it.
- ///
- /// The path to the application executable.
- public void StartExe(string appPath)
- {
- var opts = new AppiumOptions();
- opts.AddAdditionalCapability("app", appPath);
- this.Driver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
- }
-
- ///
- /// Sets scope to the Test Class.
- ///
- /// The PowerToys module to start.
- public void SetScope(PowerToysModule scope)
- {
- sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
- }
+ return this.Session.FindAll(By.Name(name), timeoutMS);
+ }
- public WindowsDriver GetRoot() => this.Root;
+ ///
+ /// Finds all elements by selector.
+ /// Shortcut for this.Session.FindAll(by, timeoutMS)
+ ///
+ /// The selector to find the elements.
+ /// The timeout in milliseconds (default is 3000).
+ /// A read-only collection of the found elements.
+ protected ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ {
+ return this.Session.FindAll(by, timeoutMS);
+ }
- public WindowsDriver GetDriver()
- {
- Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
- return this.Driver;
- }
+ ///
+ /// Finds all elements by selector.
+ /// Shortcut for this.Session.FindAll(By.Name(name), timeoutMS)
+ ///
+ /// The name of the elements.
+ /// The timeout in milliseconds (default is 3000).
+ /// A read-only collection of the found elements.
+ protected ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ {
+ return this.Session.FindAll(By.Name(name), timeoutMS);
}
}
}
diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs
index 35d49a5b64cd..c50bbe988ef5 100644
--- a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs
+++ b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs
@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -18,8 +18,18 @@ public HostModuleTests()
}
///
- /// Test if Empty-view is shown when no entries are present.
- /// And 'Add an entry' button from Empty-view is functional.
+ /// Test Empty-view in the Hosts-File-Editor
+ ///
+ ///
+ /// Validating Empty-view is shown if no entries in the list.
+ ///
+ ///
+ /// Validating Empty-view is NOT shown if 1 or more entries in the list.
+ ///
+ ///
+ /// Validating Add-an-entry HyperlinkButton in Empty-view works correctly.
+ ///
+ ///
///
[TestMethod]
public void TestEmptyView()
@@ -28,20 +38,25 @@ public void TestEmptyView()
this.RemoveAllEntries();
// 'Add an entry' button (only show-up when list is empty) should be visible
- Assert.IsTrue(this.FindAll(By.Name("Add an entry")).Count == 1, "'Add an entry' button should be visible in the empty view");
+ Assert.IsTrue(this.FindAll("Add an entry").Count == 1, "'Add an entry' button should be visible in the empty view");
// Click 'Add an entry' from empty-view for adding Host override rule
- this.Find(By.Name("Add an entry")).Click();
+ this.Find("Add an entry").Click();
this.AddEntry("192.168.0.1", "localhost", false, false);
// Should have one row now and not more empty view
- Assert.IsTrue(this.FindAll(By.Name("Delete")).Count == 1, "Should have one row now");
- Assert.IsTrue(this.FindAll(By.Name("Add an entry")).Count == 0, "'Add an entry' button should be invisible if not empty view");
+ Assert.IsTrue(this.FindAll("Delete").Count == 1, "Should have one row now");
+ Assert.IsTrue(this.FindAll("Add an entry").Count == 0, "'Add an entry' button should be invisible if not empty view");
}
///
- /// Test if 'New entry' button is functional
+ /// Test Adding-entry Button in the Hosts-File-Editor
+ ///
+ ///
+ /// Validating Adding-entry Button works correctly.
+ ///
+ ///
///
[TestMethod]
public void TestAddingEntry()
@@ -49,11 +64,155 @@ public void TestAddingEntry()
this.CloseWarningDialog();
this.RemoveAllEntries();
- Assert.IsTrue(this.FindAll(By.Name("Delete")).Count == 0, "Should have no row after removing all");
+ Assert.IsTrue(this.FindAll("Delete").Count == 0, "Should have no row after removing all");
this.AddEntry("192.168.0.1", "localhost", true);
- Assert.IsTrue(this.FindAll(By.Name("Delete")).Count == 1, "Should have one row now");
+ Assert.IsTrue(this.FindAll("Delete").Count == 1, "Should have one row now");
+ }
+
+ ///
+ /// Test Multiple-hosts validation logic
+ ///
+ ///
+ /// Validating the Add button should be Disabled if more than 9 hosts in one entry.
+ ///
+ ///
+ /// Validating the Add button should be Enabled if less or equal 9 hosts in one entry.
+ ///
+ ///
+ ///
+ [TestMethod]
+ public void TestTooManyHosts()
+ {
+ this.CloseWarningDialog();
+
+ // only at most 9 hosts allowed in one entry
+ string validHosts = string.Join(" ", "host_1", "host_2", "host_3", "host_4", "host_5", "host_6", "host_7", "host_8", "host_9");
+
+ // should not allow to add more than 9 hosts in one entry, hosts are separated by space
+ string inValidHosts = validHosts + " more_host";
+
+ this.Find("New entry").Click();
+
+ Assert.IsFalse(this.Find("Add").Enabled, "Add button should be Disabled by default");
+
+ this.Find("Address").SetText("127.0.0.1");
+
+ this.Find("Hosts").SetText(validHosts);
+ Assert.IsTrue(this.Find("Add").Enabled, "Add button should be Enabled with validHosts");
+
+ this.Find("Hosts").SetText(inValidHosts);
+ Assert.IsFalse(this.Find("Add").Enabled, "Add button should be Disabled with inValidHosts");
+
+ this.Find("Cancel").Click();
+ }
+
+ ///
+ /// Test Error-message in the Hosts-File-Editor
+ ///
+ ///
+ /// Validating error message should be shown if not run as admin.
+ ///
+ ///
+ ///
+ [TestMethod]
+ public void TestErrorMessageWithNonAdminPermission()
+ {
+ this.CloseWarningDialog();
+ this.RemoveAllEntries();
+
+ // Add new URL override and a warning tip should be shown
+ this.AddEntry("192.168.0.1", "localhost", true);
+
+ Assert.IsTrue(
+ this.FindAll("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1,
+ "Should display host-file saving error if not run as administrator");
+ }
+
+ ///
+ /// Test Filter-panel function in the Hosts-File-Editor
+ ///
+ ///
+ /// Validating Address filter matching pattern: contains, endsWith, startsWith, exactly-match.
+ ///
+ ///
+ /// Validating Hosts filter matching pattern: contains, endsWith, startsWith, exactly-match.
+ ///
+ ///
+ /// Validating click Filters Button to open filter-panel, and click Filter Button again to close filter-panel.
+ ///
+ ///
+ ///
+ [TestMethod]
+ public void TestFilterControl()
+ {
+ this.CloseWarningDialog();
+ this.RemoveAllEntries();
+
+ for (int i = 0; i < 10; i++)
+ {
+ this.AddEntry("192.168.0." + i, "localhost_" + i, true);
+ }
+
+ // Open-filter-panel
+ this.Find("Filters").Click();
+ Assert.IsTrue(this.FindAll("Clear filters").Count == 1, "Filter panel should be opened afer click Filter Button");
+
+ var addressFilterCases = new KeyValuePair[]
+ {
+ // contains text, expected matched more rows
+ new("168.0", 10),
+
+ // ends with text, expected matched 1 row
+ new("168.0.1", 1),
+
+ // starts with text, expected matched more rows
+ new("192.168.", 10),
+
+ // full text, expected matched 1 row
+ new("192.168.0.1", 1),
+
+ // no-matching text, expected matched no row
+ new("127.0.0", 0),
+
+ // empty filter, should display all rows
+ new(string.Empty, 10),
+ };
+
+ foreach (var (addressFilter, expectedCount) in addressFilterCases)
+ {
+ this.Find("Address").SetText(addressFilter);
+ Assert.IsTrue(this.Find("Entries").FindAll("Delete").Count == expectedCount);
+ }
+
+ var hostFilterCases = new KeyValuePair[]
+ {
+ // contains text, expected matched more rows
+ new("host_", 10),
+
+ // ends with text, expected matched 1 row
+ new("host_4", 1),
+
+ // starts with text, expected matched more rows
+ new("localhost", 10),
+
+ // full text, expected matched 1 row
+ new("localhost_5", 1),
+
+ // empty filter, should display all rows
+ new(string.Empty, 10),
+ };
+
+ foreach (var (hostFilterCase, expectedCount) in hostFilterCases)
+ {
+ this.Find("Hosts").SetText(hostFilterCase);
+ Assert.IsTrue(this.Find("Entries").FindAll("Delete").Count == expectedCount);
+ }
+
+ // Close-filter-panel
+ this.Find("Filters").Click();
+ Assert.IsFalse(this.FindAll("Clear filters").Count == 1, "Filter panel should be closed afer click Filter Button");
}
private void AddEntry(string ip, string host, bool active = true, bool clickAddEntryButton = true)
@@ -61,21 +220,21 @@ private void AddEntry(string ip, string host, bool active = true, bool clickAddE
if (clickAddEntryButton)
{
// Click 'Add an entry' for adding Host override rule
- this.Find(By.Name("New entry")).Click();
+ this.Find("New entry").Click();
}
// Adding a new host override localhost -> 192.168.0.1
- Assert.IsFalse(this.Find(By.Name("Add")).Enabled, "Add button should be Disabled by default");
+ Assert.IsFalse(this.Find("Add").Enabled, "Add button should be Disabled by default");
- Assert.IsTrue(this.Find(By.Name("Address")).SetText(ip, false).Text == ip);
- Assert.IsTrue(this.Find(By.Name("Hosts")).SetText(host, false).Text == host);
+ Assert.IsTrue(this.Find("Address").SetText(ip).Text == ip);
+ Assert.IsTrue(this.Find("Hosts").SetText(host).Text == host);
- this.Find(By.Name("Active")).Toggle(active);
+ this.Find("Active").Toggle(active);
- Assert.IsTrue(this.Find(By.Name("Add")).Enabled, "Add button should be Enabled after providing valid inputs");
+ Assert.IsTrue(this.Find("Add").Enabled, "Add button should be Enabled after providing valid inputs");
// Add the entry
- this.Find(By.Name("Add")).Click();
+ this.Find("Add").Click();
// 0.5 second delay after adding an entry
Task.Delay(500).Wait();
@@ -84,25 +243,25 @@ private void AddEntry(string ip, string host, bool active = true, bool clickAddE
private void CloseWarningDialog()
{
// Find 'Accept' button which come in 'Warning' dialog
- if (this.FindAll(By.Name("Warning")).Count > 0 &&
- this.FindAll(By.Name("Accept")).Count > 0)
+ if (this.FindAll("Warning").Count > 0 &&
+ this.FindAll("Accept").Count > 0)
{
// Hide Warning dialog if any
- this.Find(By.Name("Accept")).Click();
+ this.Find("Accept").Click();
}
}
private void RemoveAllEntries()
{
// Delete all existing host-override rules
- foreach (var deleteBtn in this.FindAll(By.Name("Delete")))
+ foreach (var deleteBtn in this.FindAll("Delete"))
{
deleteBtn.Click();
- this.Find(By.Name("Yes")).Click();
+ this.Find("Yes").Click();
}
// Should have no row left, and no more delete button
- Assert.IsTrue(this.FindAll(By.Name("Delete")).Count == 0);
+ Assert.IsTrue(this.FindAll("Delete").Count == 0);
}
}
}
diff --git a/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs b/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs
new file mode 100644
index 000000000000..c8ab562602ab
--- /dev/null
+++ b/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Hosts.UITests
+{
+ [TestClass]
+ public class HostsSettingTests : UITestBase
+ {
+ ///
+ /// Test Warning Dialog at startup
+ ///
+ ///
+ /// Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is On.
+ ///
+ ///
+ /// Validating Warning-Dialog will NOT be shown if 'Show a warning at startup' toggle is Off.
+ ///
+ ///
+ /// Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed.
+ ///
+ ///
+ /// Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed.
+ ///
+ ///
+ ///
+ [TestMethod]
+ public void TestWarningDialog()
+ {
+ this.LaunchFromSetting(showWarning: true);
+
+ // Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is on
+ Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog");
+
+ // Quit Hosts File Editor
+ this.Find("Quit").Click();
+
+ // Wait for 500 ms to make sure Hosts File Editor is closed
+ Task.Delay(500).Wait();
+
+ // Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed
+ Assert.IsTrue(this.IsHostsFileEditorClosed(), "Hosts File Editor should be closed after click Quit button in Warning Dialog");
+
+ // Re-attaching to Setting Windows
+ this.Session.Attach(PowerToysModule.PowerToysSettings);
+
+ this.Find("Launch Hosts File Editor").Click();
+
+ // wait for 500 ms to make sure Hosts File Editor is launched
+ Task.Delay(500).Wait();
+
+ this.Session.Attach(PowerToysModule.Hosts);
+
+ // Should show warning dialog
+ Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog");
+
+ // Quit Hosts File Editor
+ this.Find("Accept").Click();
+
+ Task.Delay(500).Wait();
+
+ // Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed
+ Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed after click Accept button in Warning Dialog");
+
+ // Close Hosts File Editor window
+ this.Session.Find("Hosts File Editor").Close();
+
+ // Restore back to PowerToysSettings Session
+ this.Session.Attach(PowerToysModule.PowerToysSettings);
+
+ this.LaunchFromSetting(showWarning: false);
+
+ // Should NOT show warning dialog
+ Assert.IsTrue(this.FindAll("Warning").Count == 0, "Should NOT show warning dialog");
+
+ // Host Editor Window should not be closed
+ Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed");
+
+ // Close Hosts File Editor window
+ this.Session.Find("Hosts File Editor").Close();
+
+ // Restore back to PowerToysSettings Session
+ this.Session.Attach(PowerToysModule.PowerToysSettings);
+ }
+
+ private bool IsHostsFileEditorClosed()
+ {
+ try
+ {
+ this.Session.FindAll("Hosts File Editor");
+ }
+ catch (Exception ex)
+ {
+ // Validate if editor window closed by checking exception.Message
+ return ex.Message.Contains("Currently selected window has been closed");
+ }
+
+ return false;
+ }
+
+ private void LaunchFromSetting(bool showWarning = false, bool launchAsAdmin = false)
+ {
+ // Goto Hosts File Editor setting page
+ if (this.FindAll("Hosts File Editor").Count == 0)
+ {
+ // Expand Advanced list-group if needed
+ this.Find("Advanced").Click();
+ }
+
+ this.Find("Hosts File Editor").Click();
+
+ this.Find("Enable Hosts File Editor").Toggle(true);
+ this.Find("Launch as administrator").Toggle(launchAsAdmin);
+ this.Find("Show a warning at startup").Toggle(showWarning);
+
+ // launch Hosts File Editor
+ this.Find("Launch Hosts File Editor").Click();
+
+ Task.Delay(500).Wait();
+
+ this.Session.Attach(PowerToysModule.Hosts);
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts.UITests/Release-Test-Checklist-Migration-Progress.md b/src/modules/Hosts/Hosts.UITests/Release-Test-Checklist-Migration-Progress.md
new file mode 100644
index 000000000000..dbe05b7b39e9
--- /dev/null
+++ b/src/modules/Hosts/Hosts.UITests/Release-Test-Checklist-Migration-Progress.md
@@ -0,0 +1,26 @@
+## This is for tracking UI-Tests migration progress for Hosts File Editor Module
+Refer to [release check list] (https://github.com/microsoft/PowerToys/blob/releaseChecklist/doc/releases/tests-checklist-template.md#hosts-file-editor) for all manual tests.
+
+### Existing Manual Test-cases run by previous PowerToys owner
+For existing manual test-cases, we will convert them to UI-Tests and run them in CI and Release pipeline
+
+ * Launch Host File Editor:
+ - [x] Verify the application exits if "Quit" is clicked on the initial warning. (**HostsSettingTests.TestWarningDialog**)
+ - [x] Launch Host File Editor again and click "Accept". The module should not close. (**HostModuleTests.TestEmptyView**)
+ - [ ] Launch Host File Editor again and click "Accept". The module should not close. Open the hosts file (`%WinDir%\System32\Drivers\Etc`) in a text editor that auto-refreshes so you can see the changes applied by the editor in real time. (VSCode is an editor like this, for example)
+ - [ ] Enable and disable lines and verify they are applied to the file.
+ - [ ] Add a new entry and verify it's applied.
+ - [ ] Add manually an entry with more than 9 hosts in hosts file (Windows limitation) and verify it is split correctly on loading and the info bar is shown.
+ - [x] Try to filter for lines and verify you can find them. (**HostModuleTests.TestFilterControl**)
+ - [ ] Click the "Open hosts file" button and verify it opens in your default editor. (likely Notepad)
+ * Test the different settings and verify they are applied:
+ - [ ] Launch as Administrator.
+ - [x] Show a warning at startup. (**HostsSettingTests.TestWarningDialog**)
+ - [ ] Additional lines position.
+
+### Additional UI-Tests cases
+ - [x] Add manually an entry with more than 9 hosts and Add button should be disabled. (**HostModuleTests.TestTooManyHosts**)
+ - [x] Add manually an entry with less or equal 9 hosts and Add button should be enabled. (**HostModuleTests.TestTooManyHosts**)
+ - [x] Should show empty view if no entries. (**HostModuleTests.TestEmptyView**)
+ - [x] Add a new entry with valid or invalid input (**HostModuleTests.TestAddHost**)
+ - [x] Show save host file error if not run as Administrator. (**HostModuleTests.TestErrorMessageWithNonAdminPermission**)
\ No newline at end of file