Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UITestAutomation Framework #37461

Merged
merged 37 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8d334e4
Add UITestAutomation framework
Feb 14, 2025
3f5e7d5
add code comments
Feb 14, 2025
785086a
Optimized code format
Feb 14, 2025
f6b1765
Optimized code format
Feb 14, 2025
8e542b0
Update commons and add keyboard manager ui test project
Feb 14, 2025
a5bb9a1
Optimized code format
Feb 14, 2025
d934628
fix mergeing
Feb 14, 2025
89d5ffb
test scope and fix fancyzone exe path
Feb 14, 2025
4f15b63
Add readme
Feb 14, 2025
931cf95
Optimize helper functions and UI test method
Feb 14, 2025
430702e
Fix spelling errors and restore module UI tests
Feb 17, 2025
3a1e57f
Restore Indent
Feb 17, 2025
a0cecb8
Update NOTICE.md
Feb 17, 2025
aba029d
Update comments to Session and Elements
Feb 18, 2025
0785c67
Update comments for Button and Window
Feb 18, 2025
d535199
delete unnecessary code
Feb 18, 2025
708b975
Merge branch 'zhaopengwang/UITestAutomation' of https://github.com/mi…
Feb 18, 2025
3ae2397
change FindElementByName to FindElmenet
Feb 18, 2025
3b47f5e
Update comments for ModuleConfigData
Feb 18, 2025
7ed5765
Merge branch 'zhaopengwang/UITestAutomation' of https://github.com/mi…
Feb 18, 2025
81a4a7a
Update readme and comments
Feb 18, 2025
6dfe364
Remove extra comments
Feb 18, 2025
41ffa68
change public property
Feb 18, 2025
5841086
Merge branch 'zhaopengwang/UITestAutomation' of https://github.com/mi…
Feb 18, 2025
5753adc
Optimize code readability
Feb 18, 2025
bd394f0
add default Attach Function
Feb 18, 2025
a50b085
change attach function name
Feb 19, 2025
36bbb62
Update comments to XML format
Feb 19, 2025
ae1e313
Hide by internal functions
Feb 19, 2025
6c08723
Update readme
Feb 19, 2025
89b8046
Refine the framework
Feb 19, 2025
680a314
Fix process start position and update readme
Feb 20, 2025
e435490
Merge branch 'main' into zhaopengwang/UITestAutomation
urnotdfs Feb 20, 2025
e008f17
Merge branch 'main' into zhaopengwang/UITestAutomation
urnotdfs Feb 20, 2025
3653568
Remove Enum PowerToysModuleWindow
Feb 20, 2025
9da02e4
Update attach comments
Feb 20, 2025
4d58b05
Update ModuleConfigData comments
Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,7 @@ EXHIBIT A -Mozilla Public License.
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
- Microsoft.Data.Sqlite 9.0.2
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
- Microsoft.DotNet.ILCompiler (A)
- Microsoft.Extensions.DependencyInjection 9.0.2
- Microsoft.Extensions.Hosting 9.0.2
- Microsoft.Extensions.Hosting.WindowsServices 9.0.2
Expand Down
15 changes: 15 additions & 0 deletions PowerToys.sln
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItModuleInterface", "sr
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITestAutomation", "src\common\UITestAutomation\UITestAutomation.csproj", "{A558C25D-2007-498E-8B6F-43405AFAE9E2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Expand Down Expand Up @@ -2834,6 +2836,18 @@ Global
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.Build.0 = Release|x64
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x86.ActiveCfg = Release|x64
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x86.Build.0 = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.Build.0 = Debug|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.ActiveCfg = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.Build.0 = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x86.ActiveCfg = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x86.Build.0 = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.ActiveCfg = Release|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.Build.0 = Release|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.ActiveCfg = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.Build.0 = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x86.ActiveCfg = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -3069,6 +3083,7 @@ Global
{0A84F764-3A88-44CD-AA96-41BDBD48627B} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}
{E4585179-2AC1-4D5F-A3FF-CFC5392F694C} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}
{A558C25D-2007-498E-8B6F-43405AFAE9E2} = {1AFB6476-670D-4E80-A464-657E01DFF482}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
Expand Down
101 changes: 101 additions & 0 deletions doc/devdocs/UITests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# UI tests framework

A specialized UI test framework for PowerToys that makes it easy to write UI tests for PowerToys modules or settings. Let's start writing UI tests!

## Before running tests

- Install Windows Application Driver v1.2.1 from https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1 to the default directory (`C:\Program Files (x86)\Windows Application Driver`)

- Enable Developer Mode in Windows settings

## Running tests

- Exit PowerToys if it's running.

- Open `PowerToys.sln` in Visual Studio and build the solution.

- Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`).


## How to add the first UI tests for your modules

- Create a new project and add the following references to the project file. Change the OutputPath to your own module's path.
```
<PropertyGroup>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>

<PropertyGroup>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\KeyboardManagerUITests\</OutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
<Folder Include="Properties\" />
</ItemGroup>
```
- Inherit your test class from UITestBase.
>Set Scope: The default scope starts from the PowerToys settings UI. If you want to start from your own module, set the constructor as shown below:

- >Default Scope:
```
[TestClass]
public class RunFancyZonesTest : UITestBase
{
public RunFancyZonesTest()
: base()
{
}
}
```
>Specify Scope:
```
[TestClass]
public class RunFancyZonesTest : UITestBase
{
public RunFancyZonesTest()
: base(PowerToysModule.FancyZone)
{
}
}
```

- Then you can start using session to perform the UI operations.

**Example**
```
namespace UITests_KeyboardManager
{
[TestClass]
public class RunKeyboardManagerUITests : UITestBase
{
[TestMethod]
public void OpenKeyboardManagerEditor()
{
// Open KeyboardManagerEditor
Session.FindElement<Button>(By.Name("Remap a key")).Click();
Session.Attach(PowerToysModuleWindow.KeyboardManagerKeys);

// Maximize window
var window = Session.FindElementByName<Window>("Remap keys").Maximize();

// Click button
Session.FindElementByName<Button>("Add key remapping").Click();
Session.FindElementByName<Element>("Row 1, Select:").FindElementByName<Button>("Select").Click();
Session.FindElementByName<Window>("Select a key on selected keyboard").FindElementByName<Button>("Cancel").Click();
window.Close();

// Back to Settings
Session.Attach(PowerToysModuleWindow.PowerToysSettings);
}
}
}
```

## Extra tools and information

**Accessibility Tools**:
While working on tests, you may need a tool that helps you to view the element's accessibility data, e.g. for finding the button to click. For this purpose, you could use [AccessibilityInsights](https://accessibilityinsights.io/docs/windows/overview)
28 changes: 28 additions & 0 deletions src/common/UITestAutomation/Element/Button.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.Events;

namespace Microsoft.PowerToys.UITest
{
public class Button : Element
{
public Button()
: base()
{
}

// Get Button Type
public string GetButtonType()
{
Assert.IsNotNull(WindowsElement, "WindowsElement should not be null");
return WindowsElement.GetAttribute("ControlType");
}
}
}
36 changes: 36 additions & 0 deletions src/common/UITestAutomation/Element/By.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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.Xml.Linq;
using static OpenQA.Selenium.By;

namespace Microsoft.PowerToys.UITest
{
// This class is a wrapper around OpenQA.Selenium.By
#pragma warning disable SA1649 // File name should match first type name
public class By
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this change to private or internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, Users will use the By type, which cannot be changed to private or internal. There is no way to authorize a file that does not exist.

{
private readonly OpenQA.Selenium.By by;

private By(OpenQA.Selenium.By by)
{
this.by = by;
}

// Factory methods to create a By object
public static By Name(string name) => new By(OpenQA.Selenium.By.Name(name));

public static By Id(string id) => new By(OpenQA.Selenium.By.Id(id));

public static By XPath(string xpath) => new By(OpenQA.Selenium.By.XPath(xpath));

public static By CssSelector(string cssSelector) => new By(OpenQA.Selenium.By.CssSelector(cssSelector));

public static By LinkText(string linkText) => new By(OpenQA.Selenium.By.LinkText(linkText));

public static By TagName(string tagName) => new By(OpenQA.Selenium.By.TagName(tagName));

public OpenQA.Selenium.By ToSeleniumBy() => by;
}
}
104 changes: 104 additions & 0 deletions src/common/UITestAutomation/Element/Element.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.Events;
using static Microsoft.PowerToys.UITest.UITestBase;

[assembly: InternalsVisibleTo("Session")]

namespace Microsoft.PowerToys.UITest
{
// The basic class for all UI elements
public class Element
{
public WindowsElement? WindowsElement { get; set; }

private WindowsDriver<WindowsElement>? driver;

public Element() => WindowsElement = null;

internal void SetWindowsElement(WindowsElement windowsElement) => WindowsElement = windowsElement;

internal void SetSession(WindowsDriver<WindowsElement> driver) => this.driver = driver;

// Get the name of the element
public string GetName() => GetAttribute("Name");

// Get the text of the element
public string GetText() => GetAttribute("Value");

// Get the automation ID of the element
public string GetAutomationId() => GetAttribute("AutomationId");

// Get the class name of the element
public string GetClassName() => GetAttribute("ClassName");

// Get the help text of the element
public string GetHelpText() => GetAttribute("HelpText");

// Check if the element is enabled
public bool IsEnabled() => GetAttribute("IsEnabled") == "True";

// Check if the element is selected
public bool IsSelected() => GetAttribute("IsSelected") == "True";

// Click the element
public void Click() => PerformAction(actions => actions.Click());

// Right click the element
public void RightClick() => PerformAction(actions => actions.ContextClick());

// Get an attribute of the element
private string GetAttribute(string attributeName)
{
Assert.IsNotNull(WindowsElement, "WindowsElement should not be null");
return WindowsElement?.GetAttribute(attributeName) ?? string.Empty;
}

// Find element by Name
public T FindElementByName<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(WindowsElement, "WindowsElement is null");
return FindElementHelper.FindElement<T, AppiumWebElement>(() => WindowsElement.FindElementByName(name), timeoutMS, driver);
}

// Find element by AccessibilityId
public T? FindElementByAccessibilityId<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(WindowsElement, "WindowsElement is null");
return FindElementHelper.FindElement<T, AppiumWebElement>(() => WindowsElement.FindElementByAccessibilityId(name), timeoutMS, driver);
}

// Find elements by name
public ReadOnlyCollection<T>? FindElementsByName<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(WindowsElement, "WindowsElement is null");
return FindElementHelper.FindElements<T, AppiumWebElement>(() => WindowsElement.FindElementsByName(name), timeoutMS, driver);
}

public Screenshot? GetScreenShot() => WindowsElement?.GetScreenshot();

// Simulate manual operation
private void PerformAction(Action<Actions> action)
{
var element = WindowsElement;
Actions actions = new Actions(driver);
actions.MoveToElement(element);
action(actions);
actions.Build().Perform();
}
}
}
67 changes: 67 additions & 0 deletions src/common/UITestAutomation/Element/FindElementHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;

[assembly: InternalsVisibleTo("Element")]
[assembly: InternalsVisibleTo("Session")]

namespace Microsoft.PowerToys.UITest
{
internal static class FindElementHelper
{
public static T FindElement<T, TW>(Func<TW> findElementFunc, int timeoutMS, WindowsDriver<WindowsElement>? driver)
where T : Element, new()
{
Assert.IsNotNull(driver, "driver is null");
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
var item = findElementFunc() as WindowsElement;
Assert.IsNotNull(item, "Can't find this element");

return NewElement<T>(item, driver);
}

public static ReadOnlyCollection<T>? FindElements<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, int timeoutMS, WindowsDriver<WindowsElement>? driver)
where T : Element, new()
{
Assert.IsNotNull(driver, "driver is null");
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
var items = findElementsFunc();
var res = items.Select(item =>
{
var element = item as WindowsElement;
if (element != null)
{
NewElement<T>(element, driver);
}

return element;
}).Where(element => element != null).ToList();

return new ReadOnlyCollection<T>((IList<T>)res);
}

// Create a new element of type T
public static T NewElement<T>(WindowsElement element, WindowsDriver<WindowsElement>? driver)
where T : Element, new()
{
T newElement = new T();
Assert.IsNotNull(driver, "[FindElementHelper.cs] driver is null");
newElement.SetSession(driver);
Assert.IsNotNull(element, "[FindElementHelper] element is null");
newElement.SetWindowsElement(element);
return newElement;
}
}
}
Loading
Loading