Skip to content

Commit

Permalink
[Hosts] Converting manual release-check-list tests to UI-Test Automat…
Browse files Browse the repository at this point in the history
…ion and Adding more UI-Test-cases (#37657)

* Add more UI-Test, refactor UITestAutomation

* Convert manual test-case to automation

UI-Tests:

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.
Validating Adding-entry Button works correctly.
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.
Validating error message should be shown if not run as admin.
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.

---------

Co-authored-by: Jerry Xu <[email protected]>
Co-authored-by: Copilot <[email protected]>
  • Loading branch information
3 people authored Feb 28, 2025
1 parent 8a2d474 commit 22e29d1
Show file tree
Hide file tree
Showing 14 changed files with 838 additions and 154 deletions.
9 changes: 9 additions & 0 deletions src/common/UITestAutomation/Element/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public class Button : Element
{
private static readonly string ExpectedControlType = "ControlType.Button";

/// <summary>
/// Initializes a new instance of the <see cref="Button"/> class.
/// </summary>
public Button()
{
this.TargetControlType = Button.ExpectedControlType;
}
}
}
116 changes: 97 additions & 19 deletions src/common/UITestAutomation/Element/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public class Element

private WindowsDriver<WindowsElement>? 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<WindowsElement> driver) => this.driver = driver;
Expand Down Expand Up @@ -91,7 +99,7 @@ public string ControlType
/// Click the UI element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
public void Click(bool rightClick = false)
public virtual void Click(bool rightClick = false)
{
PerformAction((actions, windowElement) =>
{
Expand All @@ -116,7 +124,7 @@ public void Click(bool rightClick = false)
/// <summary>
/// Double Click the UI element.
/// </summary>
public void DoubleClick()
public virtual void DoubleClick()
{
PerformAction((actions, windowElement) =>
{
Expand All @@ -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;
}

Expand All @@ -154,17 +161,51 @@ public T Find<T>(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<T, AppiumWebElement>(
() =>
{
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<T>(by, timeoutMS);

Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");

return collection[0];
}

/// <summary>
/// Finds an element by the selector.
/// Shortcut for this.Find<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class type of the element to find.</typeparam>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Find<T>(By.Name(name), timeoutMS);
}

/// <summary>
/// Finds an element by the selector.
/// Shortcut for this.Find<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to use for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public Element Find(By by, int timeoutMS = 3000)
{
return this.Find<Element>(by, timeoutMS);
}

/// <summary>
/// Finds an element by the selector.
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public Element Find(string name, int timeoutMS = 3000)
{
return this.Find<Element>(By.Name(name), timeoutMS);
}

/// <summary>
Expand All @@ -174,30 +215,67 @@ public T Find<T>(By by, int timeoutMS = 3000)
/// <param name="by">The selector to use for finding the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T>? FindAll<T>(By by, int timeoutMS = 3000)
public ReadOnlyCollection<T> FindAll<T>(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}");
var foundElements = FindHelper.FindAll<T, AppiumWebElement>(
() =>
{
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<T>([]);
}

/// <summary>
/// Finds all elements by the selector.
/// Shortcut for this.FindAll<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class type of the elements to find.</typeparam>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.FindAll<T>(By.Name(name), timeoutMS);
}

/// <summary>
/// Finds all elements by the selector.
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to use for finding the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.FindAll<Element>(by, timeoutMS);
}

/// <summary>
/// Finds all elements by the selector.
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.FindAll<Element>(By.Name(name), timeoutMS);
}

/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
/// <param name="action">The action to perform on the element.</param>
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 100 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 100 ms</param>
protected void PerformAction(Action<Actions, WindowsElement> action, int msPreAction = 100, int msPostAction = 100)
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 500 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 500 ms</param>
protected void PerformAction(Action<Actions, WindowsElement> action, int msPreAction = 500, int msPostAction = 500)
{
if (msPreAction > 0)
{
Expand Down
23 changes: 23 additions & 0 deletions src/common/UITestAutomation/Element/HyperlinkButton.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents a HyperLinkButton in the UI test environment.
/// HyperLinkButton represents a button control that functions as a hyperlink.
/// </summary>
public class HyperlinkButton : Button
{
private static readonly string ExpectedControlType = "ControlType.HyperLink";

/// <summary>
/// Initializes a new instance of the <see cref="HyperlinkButton"/> class.
/// </summary>
public HyperlinkButton()
{
this.TargetControlType = HyperlinkButton.ExpectedControlType;
}
}
}
59 changes: 59 additions & 0 deletions src/common/UITestAutomation/Element/NavigationViewItem.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents a NavigationViewItem in the UI test environment.
/// NavigationViewItem represents the container for an item in a NavigationView control.
/// </summary>
public class NavigationViewItem : Element
{
private static readonly string ExpectedControlType = "ControlType.ListItem";

/// <summary>
/// Initializes a new instance of the <see cref="NavigationViewItem"/> class.
/// </summary>
public NavigationViewItem()
{
this.TargetControlType = NavigationViewItem.ExpectedControlType;
}

/// <summary>
/// Click the ListItem element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
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();
});
}

/// <summary>
/// Double Click the ListItem element.
/// </summary>
public override void DoubleClick()
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement, 10, 10);
actions.DoubleClick();
actions.Build().Perform();
});
}
}
}
23 changes: 23 additions & 0 deletions src/common/UITestAutomation/Element/TextBlock.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents a TextBlock in the UI test environment.
/// TextBlock provides a lightweight control for displaying small amounts of flow content.
/// </summary>
public class TextBlock : Element
{
private static readonly string ExpectedControlType = "ControlType.Text";

/// <summary>
/// Initializes a new instance of the <see cref="TextBlock"/> class.
/// </summary>
public TextBlock()
{
this.TargetControlType = TextBlock.ExpectedControlType;
}
}
}
13 changes: 12 additions & 1 deletion src/common/UITestAutomation/Element/TextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// 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).
/// </summary>
public class TextBox : Element
{
private static readonly string ExpectedControlType = "ControlType.Edit";

/// <summary>
/// Initializes a new instance of the <see cref="TextBox"/> class.
/// </summary>
public TextBox()
{
this.TargetControlType = TextBox.ExpectedControlType;
}

/// <summary>
/// Sets the text of the textbox.
/// </summary>
Expand Down
8 changes: 4 additions & 4 deletions src/common/UITestAutomation/Element/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public Window Maximize(bool byClickButton = true)
{
if (byClickButton)
{
Find<Button>(By.Name("Maximize")).Click();
Find<Button>("Maximize").Click();
}
else
{
Expand All @@ -37,7 +37,7 @@ public Window Restore(bool byClickButton = true)
{
if (byClickButton)
{
Find<Button>(By.Name("Restore")).Click();
Find<Button>("Restore").Click();
}
else
{
Expand All @@ -56,7 +56,7 @@ public Window Minimize(bool byClickButton = true)
{
if (byClickButton)
{
Find<Button>(By.Name("Minimize")).Click();
Find<Button>("Minimize").Click();
}
else
{
Expand All @@ -74,7 +74,7 @@ public void Close(bool byClickButton = true)
{
if (byClickButton)
{
Find<Button>(By.Name("Close")).Click();
Find<Button>("Close").Click();
}
else
{
Expand Down
9 changes: 1 addition & 8 deletions src/common/UITestAutomation/FindHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
internal static class FindHelper
{
public static T Find<T, TW>(Func<TW> findElementFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
var item = findElementFunc() as WindowsElement;
return NewElement<T>(item, driver, timeoutMS);
}

public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
Expand All @@ -32,7 +25,7 @@ public static T Find<T, TW>(Func<TW> findElementFunc, WindowsDriver<WindowsEleme
{
var element = item as WindowsElement;
return NewElement<T>(element, driver, timeoutMS);
}).ToList();
}).Where(item => item.IsMatchingTarget()).ToList();

return new ReadOnlyCollection<T>(res);
}
Expand Down
Loading

0 comments on commit 22e29d1

Please sign in to comment.